diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d0ffce2..686a5d7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,15 +19,21 @@ jobs: - name: Checkout code uses: actions/checkout@v6 + - name: Install xcbeautify + run: brew install xcbeautify + - name: Build and Test run: | + set -o pipefail xcodebuild test \ -project wled.xcodeproj \ -scheme wled \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \ -skipPackagePluginValidation \ -skipMacroValidation \ - CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO + -resultBundlePath TestResults.xcresult \ + CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \ + 2>&1 | xcbeautify --report junit --report-path . --junit-report-filename junit.xml lint: name: Run Lint diff --git a/wled/View/DeviceListItemView.swift b/wled/View/DeviceListItemView.swift index 57fef3b..e661ea6 100644 --- a/wled/View/DeviceListItemView.swift +++ b/wled/View/DeviceListItemView.swift @@ -26,6 +26,11 @@ struct DeviceListItemView: View { Toggle("Turn On/Off", isOn: isOnBinding) .labelsHidden() .frame(alignment: .trailing) + // On iOS 16, tapping the Toggle also triggers the parent row's .onTapGesture. + // This empty handler "consumes" the SwiftUI tap at the child level, + // while the underlying UISwitch still receives the UIKit event. + // This can be removed once the minimum deployment target is iOS 17+. + .onTapGesture { } } Slider( diff --git a/wled/ViewModel/DeviceWebsocketListViewModel.swift b/wled/ViewModel/DeviceWebsocketListViewModel.swift index 94aff3c..a5b4d4e 100644 --- a/wled/ViewModel/DeviceWebsocketListViewModel.swift +++ b/wled/ViewModel/DeviceWebsocketListViewModel.swift @@ -49,6 +49,10 @@ class DeviceWebsocketListViewModel: NSObject, ObservableObject, NSFetchedResults private var activeClients: [String: ClientWrapper] = [:] private var isPaused = false private var backgroundTask: Task? + + /// Delay before disconnecting websockets after entering background. + /// Exposed as `internal` so tests can override with a shorter value. + var backgroundDisconnectDelay: Duration = .seconds(2) /// Amount of time after a device becomes offline before it is considered offline. private let offlineGracePeriod: TimeInterval = 60 @@ -265,9 +269,10 @@ class DeviceWebsocketListViewModel: NSObject, ObservableObject, NSFetchedResults func onPause() { print("[ListVM] onPause: Scheduling disconnect.") backgroundTask?.cancel() + let delay = backgroundDisconnectDelay backgroundTask = Task { @MainActor [weak self] in do { - try await Task.sleep(for: .seconds(2)) + try await Task.sleep(for: delay) } catch { // Task was cancelled (user came back quickly) return diff --git a/wledTests/DeviceWebsocketListViewModelTests.swift b/wledTests/DeviceWebsocketListViewModelTests.swift index 054b725..81ce613 100644 --- a/wledTests/DeviceWebsocketListViewModelTests.swift +++ b/wledTests/DeviceWebsocketListViewModelTests.swift @@ -105,6 +105,7 @@ struct DeviceWebsocketListViewModelTests { try context.save() let viewModel = DeviceWebsocketListViewModel(context: context) + viewModel.backgroundDisconnectDelay = .milliseconds(100) let mockClient = ManualMockWebsocketClient(device: device) viewModel.makeClient = { _ in mockClient } @@ -115,7 +116,7 @@ struct DeviceWebsocketListViewModelTests { // Simulate going to background and staying there viewModel.onPause() - try await Task.sleep(for: .seconds(4)) // Wait well past the 2s delay (generous for CI) + try await Task.sleep(for: .milliseconds(500)) // Wait well past the 100ms delay // Device should now be disconnected #expect(mockClient.deviceState.websocketStatus == .disconnected)