From 833e57d991142b2caeb06dd510e84e353f7b7191 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Thu, 27 Jun 2024 12:51:20 +0200 Subject: [PATCH] Add UI tests for generic views, fix unit tests --- .../Measurements/CloseButtonLayer.swift | 45 ----------- .../Resources/Localizable.xcstrings | 3 - .../HealthMeasurementsTests.swift | 3 + .../PairedDevicesTests.swift | 8 ++ Tests/SpeziOmronTests/SpeziOmronTests.swift | 4 +- .../UITests/TestApp/BluetoothViewsTest.swift | 55 ++++++++++++++ Tests/UITests/TestApp/ContentView.swift | 4 + .../Views/BluetoothUnavailableSection.swift | 42 +++++++++++ .../TestApp/Views/MockDeviceDetailsView.swift | 54 +++++++++++++ .../TestAppUITests/BluetoothViewsTests.swift | 75 +++++++++++++++++++ .../TestAppUITests/PairedDevicesTests.swift | 33 +++++++- .../UITests/UITests.xcodeproj/project.pbxproj | 28 ++++++- 12 files changed, 301 insertions(+), 53 deletions(-) delete mode 100644 Sources/SpeziDevicesUI/Measurements/CloseButtonLayer.swift create mode 100644 Tests/UITests/TestApp/BluetoothViewsTest.swift create mode 100644 Tests/UITests/TestApp/Views/BluetoothUnavailableSection.swift create mode 100644 Tests/UITests/TestApp/Views/MockDeviceDetailsView.swift create mode 100644 Tests/UITests/TestAppUITests/BluetoothViewsTests.swift diff --git a/Sources/SpeziDevicesUI/Measurements/CloseButtonLayer.swift b/Sources/SpeziDevicesUI/Measurements/CloseButtonLayer.swift deleted file mode 100644 index bee5763..0000000 --- a/Sources/SpeziDevicesUI/Measurements/CloseButtonLayer.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// This source file is part of the Stanford SpeziDevices open source project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SpeziViews -import SwiftUI - - -struct CloseButtonLayer: View { - @Environment(\.dismiss) private var dismiss - @Binding private var viewState: ViewState - - - var body: some View { - HStack { - Button( - action: { - dismiss() - }, - label: { - Text("Close", comment: "For closing sheets.") - .foregroundStyle(Color.accentColor) - } - ) - .buttonStyle(PlainButtonStyle()) - .disabled(viewState != .idle) - - Spacer() - } - .padding() - } - - - init(viewState: Binding) { - self._viewState = viewState - } -} - -#Preview { - CloseButtonLayer(viewState: .constant(.idle)) -} diff --git a/Sources/SpeziDevicesUI/Resources/Localizable.xcstrings b/Sources/SpeziDevicesUI/Resources/Localizable.xcstrings index 1d790d5..200af4b 100644 --- a/Sources/SpeziDevicesUI/Resources/Localizable.xcstrings +++ b/Sources/SpeziDevicesUI/Resources/Localizable.xcstrings @@ -147,9 +147,6 @@ } } }, - "Close" : { - "comment" : "For closing sheets." - }, "Connected" : { "localizations" : { "en" : { diff --git a/Tests/SpeziDevicesTests/HealthMeasurementsTests.swift b/Tests/SpeziDevicesTests/HealthMeasurementsTests.swift index 6c0f214..b38a838 100644 --- a/Tests/SpeziDevicesTests/HealthMeasurementsTests.swift +++ b/Tests/SpeziDevicesTests/HealthMeasurementsTests.swift @@ -161,6 +161,9 @@ final class HealthMeasurementsTests: XCTestCase { let measurement1 = try XCTUnwrap(device.weightScale.weightMeasurement) device.weightScale.$weightMeasurement.inject(measurement1) + + try await Task.sleep(for: .milliseconds(50)) + let measurement0 = try XCTUnwrap(device.bloodPressure.bloodPressureMeasurement) device.bloodPressure.$bloodPressureMeasurement.inject(measurement0) diff --git a/Tests/SpeziDevicesTests/PairedDevicesTests.swift b/Tests/SpeziDevicesTests/PairedDevicesTests.swift index ad78159..f3e9ee3 100644 --- a/Tests/SpeziDevicesTests/PairedDevicesTests.swift +++ b/Tests/SpeziDevicesTests/PairedDevicesTests.swift @@ -30,6 +30,8 @@ final class PairedDevicesTests: XCTestCase { devices } + device.isInPairingMode = true + XCTAssertFalse(devices.isConnected(device: device.id)) XCTAssertFalse(devices.isPaired(device)) @@ -106,6 +108,8 @@ final class PairedDevicesTests: XCTestCase { devices } + device.isInPairingMode = true + device.$nearby.inject(false) try await XCTAssertThrowsErrorAsync(await devices.pair(with: device)) { error in XCTAssertEqual(try XCTUnwrap(error as? DevicePairingError), .invalidState) @@ -141,6 +145,8 @@ final class PairedDevicesTests: XCTestCase { devices } + device.isInPairingMode = true + let task = Task { try await devices.pair(with: device) } @@ -168,6 +174,8 @@ final class PairedDevicesTests: XCTestCase { devices } + device.isInPairingMode = true + devices.configure(device: device, accessing: device.$state, device.$advertisementData, device.$nearby) let task = Task { diff --git a/Tests/SpeziOmronTests/SpeziOmronTests.swift b/Tests/SpeziOmronTests/SpeziOmronTests.swift index 2395d8f..83d3023 100644 --- a/Tests/SpeziOmronTests/SpeziOmronTests.swift +++ b/Tests/SpeziOmronTests/SpeziOmronTests.swift @@ -80,14 +80,14 @@ final class SpeziOmronTests: XCTestCase { CBAdvertisementDataManufacturerDataKey: manufacturerData.encode() ])) - XCTAssertTrue(device.isInPairingMode) + XCTAssertEqual(device.manufacturerData?.pairingMode, .pairingMode) let manufacturerData0 = OmronManufacturerData(pairingMode: .transferMode, users: [.init(id: 1, sequenceNumber: 3, recordsNumber: 8)]) device.$advertisementData.inject(AdvertisementData([ CBAdvertisementDataManufacturerDataKey: manufacturerData0.encode() ])) - XCTAssertFalse(device.isInPairingMode) + XCTAssertEqual(device.manufacturerData?.pairingMode, .transferMode) device.deviceInformation.$modelNumber.inject(OmronModel.bp5250.rawValue) diff --git a/Tests/UITests/TestApp/BluetoothViewsTest.swift b/Tests/UITests/TestApp/BluetoothViewsTest.swift new file mode 100644 index 0000000..284b26f --- /dev/null +++ b/Tests/UITests/TestApp/BluetoothViewsTest.swift @@ -0,0 +1,55 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +@_spi(TestingSupport) import SpeziDevices +import SpeziDevicesUI +import SwiftUI + + +struct BluetoothViewsTest: View { + @State private var device = MockDevice.createMockDevice() + @State private var presentDeviceDetails = false + + var body: some View { + NavigationStack { + List { + BluetoothUnavailableSection() + + Section { + NearbyDeviceRow(peripheral: device, primaryAction: tapAction) { + presentDeviceDetails = true + } + } header: { + LoadingSectionHeader("Devices", loading: true) + } + } + .navigationTitle("Views") + .navigationDestination(isPresented: $presentDeviceDetails) { + MockDeviceDetailsView(device) + } + } + } + + + @MainActor + private func tapAction() { + Task { + switch device.state { + case .disconnected, .disconnecting: + await device.connect() + case .connecting, .connected: + await device.disconnect() + } + } + } +} + + +#Preview { + BluetoothViewsTest() +} diff --git a/Tests/UITests/TestApp/ContentView.swift b/Tests/UITests/TestApp/ContentView.swift index 038c143..226611f 100644 --- a/Tests/UITests/TestApp/ContentView.swift +++ b/Tests/UITests/TestApp/ContentView.swift @@ -23,6 +23,10 @@ struct ContentView: View { .tabItem { Label("Measurements", systemImage: "list.bullet.clipboard.fill") } + BluetoothViewsTest() + .tabItem { + Label("Views", systemImage: "macwindow") + } } } } diff --git a/Tests/UITests/TestApp/Views/BluetoothUnavailableSection.swift b/Tests/UITests/TestApp/Views/BluetoothUnavailableSection.swift new file mode 100644 index 0000000..0654a78 --- /dev/null +++ b/Tests/UITests/TestApp/Views/BluetoothUnavailableSection.swift @@ -0,0 +1,42 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziDevicesUI +import SwiftUI + + +struct BluetoothUnavailableSection: View { + var body: some View { + Section("Bluetooth Unavailable") { + NavigationLink("Bluetooth Powered Off") { + BluetoothUnavailableView(.poweredOff) + } + NavigationLink("Bluetooth Powered On") { + BluetoothUnavailableView(.poweredOn) + } + NavigationLink("Bluetooth Unauthorized") { + BluetoothUnavailableView(.unauthorized) + } + NavigationLink("Bluetooth Unsupported") { + BluetoothUnavailableView(.unsupported) + } + NavigationLink("Bluetooth Unknown") { + BluetoothUnavailableView(.unknown) + } + } + } +} + + +#Preview { + NavigationStack { + List { + BluetoothUnavailableSection() + } + } +} diff --git a/Tests/UITests/TestApp/Views/MockDeviceDetailsView.swift b/Tests/UITests/TestApp/Views/MockDeviceDetailsView.swift new file mode 100644 index 0000000..aed67a3 --- /dev/null +++ b/Tests/UITests/TestApp/Views/MockDeviceDetailsView.swift @@ -0,0 +1,54 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +@_spi(TestingSupport) import SpeziDevices +import SpeziDevicesUI +import SpeziViews +import SwiftUI + + +struct MockDeviceDetailsView: View { + private let device: MockDevice + + var body: some View { + List { + ListRow("Name") { + Text(device.label) + } + if let model = device.deviceInformation.modelNumber { + ListRow("Model") { + Text(model) + } + } + if let firmwareVersion = device.deviceInformation.firmwareRevision { + ListRow("Firmware Version") { + Text(firmwareVersion) + } + } + if let battery = device.battery.batteryLevel { + ListRow("Battery") { + BatteryIcon(percentage: Int(battery)) + .labelStyle(.reverse) + } + } + } + .navigationTitle(device.label) + .navigationBarTitleDisplayMode(.inline) + } + + init(_ device: MockDevice) { + self.device = device + } +} + + +#Preview { + NavigationStack { + MockDeviceDetailsView(MockDevice.createMockDevice()) + } +} diff --git a/Tests/UITests/TestAppUITests/BluetoothViewsTests.swift b/Tests/UITests/TestAppUITests/BluetoothViewsTests.swift new file mode 100644 index 0000000..d67d3a4 --- /dev/null +++ b/Tests/UITests/TestAppUITests/BluetoothViewsTests.swift @@ -0,0 +1,75 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import XCTest + + +class BluetoothViewsTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + + continueAfterFailure = false + } + + @MainActor + func testBluetoothUnavailableViews() { + let app = XCUIApplication() + app.launch() + + XCTAssert(app.buttons["Views"].waitForExistence(timeout: 2.0)) + app.buttons["Views"].tap() + + func navigateUnavailableView(name: String, expected: String?, back: Bool = true) { + XCTAssert(app.buttons[name].waitForExistence(timeout: 2.0)) + app.buttons[name].tap() + if let expected { + XCTAssert(app.staticTexts[expected].waitForExistence(timeout: 2.0)) + } + if back { + XCTAssert(app.navigationBars.buttons["Views"].exists) + app.navigationBars.buttons["Views"].tap() + } + } + + navigateUnavailableView(name: "Bluetooth Powered On", expected: nil) + navigateUnavailableView(name: "Bluetooth Unauthorized", expected: "Bluetooth Prohibited") + navigateUnavailableView(name: "Bluetooth Unsupported", expected: "Bluetooth Unsupported") + navigateUnavailableView(name: "Bluetooth Unknown", expected: "Bluetooth Failure") + navigateUnavailableView(name: "Bluetooth Powered Off", expected: "Bluetooth Off", back: false) + + XCTAssert(app.buttons["Open Settings"].exists) + app.buttons["Open Settings"].tap() + + let settingsApp = XCUIApplication(bundleIdentifier: "com.apple.Preferences") + XCTAssertEqual(settingsApp.state, .runningForeground) + } + + @MainActor + func testNearbyDeviceRow() { + let app = XCUIApplication() + app.launch() + + XCTAssert(app.buttons["Views"].waitForExistence(timeout: 2.0)) + app.buttons["Views"].tap() + + XCTAssert(app.staticTexts["DEVICES"].exists) + + XCTAssert(app.buttons["Mock Device"].exists) + app.buttons["Mock Device"].tap() + + XCTAssert(app.buttons["Mock Device, Connected"].waitForExistence(timeout: 5.0)) + XCTAssert(app.buttons["Device Details"].exists) + app.buttons["Device Details"].tap() + + XCTAssert(app.navigationBars.staticTexts["Mock Device"].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Name, Mock Device"].exists) + XCTAssert(app.staticTexts["Model, MD1"].exists) + XCTAssert(app.staticTexts["Firmware Version, 1.0"].exists) + XCTAssert(app.staticTexts["Battery, 85 %"].exists) + } +} diff --git a/Tests/UITests/TestAppUITests/PairedDevicesTests.swift b/Tests/UITests/TestAppUITests/PairedDevicesTests.swift index b5dfe58..32691bc 100644 --- a/Tests/UITests/TestAppUITests/PairedDevicesTests.swift +++ b/Tests/UITests/TestAppUITests/PairedDevicesTests.swift @@ -162,5 +162,36 @@ class PairedDevicesTests: XCTestCase { XCTAssert(app.staticTexts["Pair Accessory"].waitForExistence(timeout: 2.0)) } - // TODO: forget devices test + @MainActor + func testPairingFailed() { + let app = XCUIApplication() + app.launch() + + XCTAssert(app.buttons["Devices"].waitForExistence(timeout: 2.0)) + app.buttons["Devices"].tap() + + XCTAssert(app.navigationBars.buttons["More"].exists) + app.navigationBars.buttons["More"].tap() + XCTAssert(app.buttons["Connect"].exists) + app.buttons["Connect"].tap() + + XCTAssert(app.navigationBars.buttons["More"].exists) + app.navigationBars.buttons["More"].tap() + XCTAssert(app.buttons["Discover Device"].exists) + app.buttons["Discover Device"].tap() + + XCTAssert(app.staticTexts["Pair Accessory"].waitForExistence(timeout: 2.0)) + XCTAssert(app.buttons["Pair"].exists) + app.buttons["Pair"].tap() + + XCTAssert(app.staticTexts["Pairing Failed"].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Failed to pair with device. Please try again."].exists) + XCTAssert(app.buttons["OK"].exists) + app.buttons["OK"].tap() + + XCTAssert(app.navigationBars.buttons["Add Device"].waitForExistence(timeout: 0.5)) + app.navigationBars.buttons["Add Device"].tap() + + XCTAssert(app.staticTexts["Pair Accessory"].waitForExistence(timeout: 2.0)) + } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index d5f62c3..15c8bcf 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -20,6 +20,10 @@ A959B7EB2C2CC05A00ACA775 /* HKCorrelationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7EA2C2CC05A00ACA775 /* HKCorrelationView.swift */; }; A959B7F02C2D602C00ACA775 /* PairedDevicesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7EF2C2D602C00ACA775 /* PairedDevicesTests.swift */; }; A959B7F32C2D646500ACA775 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = A959B7F22C2D646500ACA775 /* XCTestExtensions */; }; + A959B7F52C2D72A500ACA775 /* BluetoothViewsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7F42C2D72A500ACA775 /* BluetoothViewsTest.swift */; }; + A959B7F82C2D75B400ACA775 /* BluetoothUnavailableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7F72C2D75B400ACA775 /* BluetoothUnavailableSection.swift */; }; + A959B7FA2C2D75F300ACA775 /* MockDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7F92C2D75F300ACA775 /* MockDeviceDetailsView.swift */; }; + A959B7FC2C2D769A00ACA775 /* BluetoothViewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7FB2C2D769A00ACA775 /* BluetoothViewsTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,6 +64,10 @@ A959B7E72C2CBF1400ACA775 /* HKQuantitySampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKQuantitySampleView.swift; sourceTree = ""; }; A959B7EA2C2CC05A00ACA775 /* HKCorrelationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKCorrelationView.swift; sourceTree = ""; }; A959B7EF2C2D602C00ACA775 /* PairedDevicesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairedDevicesTests.swift; sourceTree = ""; }; + A959B7F42C2D72A500ACA775 /* BluetoothViewsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothViewsTest.swift; sourceTree = ""; }; + A959B7F72C2D75B400ACA775 /* BluetoothUnavailableSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothUnavailableSection.swift; sourceTree = ""; }; + A959B7F92C2D75F300ACA775 /* MockDeviceDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceDetailsView.swift; sourceTree = ""; }; + A959B7FB2C2D769A00ACA775 /* BluetoothViewsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothViewsTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -107,12 +115,14 @@ 2F6D139428F5F384007C25D6 /* TestApp */ = { isa = PBXGroup; children = ( + A959B7F42C2D72A500ACA775 /* BluetoothViewsTest.swift */, A922BB192C2CB072009DD0E1 /* ContentView.swift */, A922BB1D2C2CB276009DD0E1 /* DevicesTestView.swift */, A922BB1F2C2CB280009DD0E1 /* MeasurementsTestView.swift */, 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, A959B7E92C2CC04400ACA775 /* Health */, + A959B7F62C2D759700ACA775 /* Views */, ); path = TestApp; sourceTree = ""; @@ -122,6 +132,7 @@ children = ( 2F8A431229130A8C005D2B8F /* HealthMeasurementsTests.swift */, A959B7EF2C2D602C00ACA775 /* PairedDevicesTests.swift */, + A959B7FB2C2D769A00ACA775 /* BluetoothViewsTests.swift */, ); path = TestAppUITests; sourceTree = ""; @@ -136,13 +147,22 @@ A959B7E92C2CC04400ACA775 /* Health */ = { isa = PBXGroup; children = ( - A959B7E52C2CBF0900ACA775 /* HKSampleView.swift */, - A959B7E72C2CBF1400ACA775 /* HKQuantitySampleView.swift */, A959B7EA2C2CC05A00ACA775 /* HKCorrelationView.swift */, + A959B7E72C2CBF1400ACA775 /* HKQuantitySampleView.swift */, + A959B7E52C2CBF0900ACA775 /* HKSampleView.swift */, ); path = Health; sourceTree = ""; }; + A959B7F62C2D759700ACA775 /* Views */ = { + isa = PBXGroup; + children = ( + A959B7F72C2D75B400ACA775 /* BluetoothUnavailableSection.swift */, + A959B7F92C2D75F300ACA775 /* MockDeviceDetailsView.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -256,10 +276,13 @@ A959B7E82C2CBF1400ACA775 /* HKQuantitySampleView.swift in Sources */, A922BB1A2C2CB072009DD0E1 /* ContentView.swift in Sources */, A959B7EB2C2CC05A00ACA775 /* HKCorrelationView.swift in Sources */, + A959B7FA2C2D75F300ACA775 /* MockDeviceDetailsView.swift in Sources */, + A959B7F82C2D75B400ACA775 /* BluetoothUnavailableSection.swift in Sources */, A922BB202C2CB280009DD0E1 /* MeasurementsTestView.swift in Sources */, A922BB1E2C2CB276009DD0E1 /* DevicesTestView.swift in Sources */, A959B7E62C2CBF0900ACA775 /* HKSampleView.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, + A959B7F52C2D72A500ACA775 /* BluetoothViewsTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -267,6 +290,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A959B7FC2C2D769A00ACA775 /* BluetoothViewsTests.swift in Sources */, A959B7F02C2D602C00ACA775 /* PairedDevicesTests.swift in Sources */, 2F8A431329130A8C005D2B8F /* HealthMeasurementsTests.swift in Sources */, );