diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index db6c4dd..fac4990 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -34,6 +34,16 @@ jobs: buildConfig: ${{ matrix.buildConfig }} resultBundle: ${{ matrix.resultBundle }} artifactname: ${{ matrix.artifactname }} + buildandtest_ios_latest: + name: Build and Test Swift Package iOS Latest + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziOnboarding + xcodeversion: latest + swiftVersion: 6 + resultBundle: SpeziOnboarding-iOS-Latest.xcresult + artifactname: SpeziOnboarding-iOS-Latest.xcresult buildandtest_visionos: name: Build and Test Swift Package visionOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -91,26 +101,27 @@ jobs: buildConfig: ${{ matrix.buildConfig }} resultBundle: ${{ matrix.resultBundle }} artifactname: ${{ matrix.artifactname }} + buildandtestuitests_ios_latest: + name: Build and Test UI Tests iOS Latest + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + runsonlabels: '["macOS", "self-hosted"]' + path: Tests/UITests + scheme: TestApp + xcodeversion: latest + swiftVersion: 6 + resultBundle: TestApp-iOS-Latest.xcresult + artifactname: TestApp-iOS-Latest.xcresult buildandtestuitests_ipad: name: Build and Test UI Tests iPadOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - strategy: - matrix: - include: - - buildConfig: Debug - resultBundle: TestApp-iPad.xcresult - artifactname: TestApp-iPad.xcresult - - buildConfig: Release - resultBundle: TestApp-iPad-Release.xcresult - artifactname: TestApp-iPad-Release.xcresult with: runsonlabels: '["macOS", "self-hosted"]' path: 'Tests/UITests' scheme: TestApp - destination: 'platform=iOS Simulator,name=iPad Air (5th generation)' - buildConfig: ${{ matrix.buildConfig }} - resultBundle: ${{ matrix.resultBundle }} - artifactname: ${{ matrix.artifactname }} + destination: 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)' + resultBundle: TestApp-iPadOS.xcresult + artifactname: TestApp-iPadOS.xcresult buildandtestuitests_visionos: name: Build and Test UI Tests visionOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -136,6 +147,6 @@ jobs: needs: [buildandtest_ios, buildandtest_visionos, buildandtest_macos, buildandtestuitests_ios, buildandtestuitests_ipad, buildandtestuitests_visionos] uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 with: - coveragereports: 'SpeziOnboarding-iOS.xcresult SpeziOnboarding-visionOS.xcresult SpeziOnboarding-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult' + coveragereports: 'SpeziOnboarding-iOS.xcresult SpeziOnboarding-visionOS.xcresult SpeziOnboarding-macOS.xcresult TestApp-iOS.xcresult TestApp-iPadOS.xcresult TestApp-visionOS.xcresult' secrets: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Package.swift b/Package.swift index 855ee32..a410fb8 100644 --- a/Package.swift +++ b/Package.swift @@ -8,9 +8,17 @@ // SPDX-License-Identifier: MIT // +import class Foundation.ProcessInfo import PackageDescription +#if swift(<6) +let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency") +#else +let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency") +#endif + + let package = Package( name: "SpeziOnboarding", defaultLocalization: "en", @@ -26,7 +34,7 @@ let package = Package( .package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"), .package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.3.1"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0") - ], + ] + swiftLintPackage(), targets: [ .target( name: "SpeziOnboarding", @@ -35,13 +43,39 @@ let package = Package( .product(name: "SpeziViews", package: "SpeziViews"), .product(name: "SpeziPersonalInfo", package: "SpeziViews"), .product(name: "OrderedCollections", package: "swift-collections") - ] + ], + swiftSettings: [ + swiftConcurrency + ], + plugins: [] + swiftLintPlugin() ), .testTarget( name: "SpeziOnboardingTests", dependencies: [ .target(name: "SpeziOnboarding") - ] + ], + swiftSettings: [ + swiftConcurrency + ], + plugins: [] + swiftLintPlugin() ) ] ) + + +func swiftLintPlugin() -> [Target.PluginUsage] { + // Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app` + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")] + } else { + [] + } +} + +func swiftLintPackage() -> [PackageDescription.Package.Dependency] { + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMinor(from: "0.55.1"))] + } else { + [] + } +} diff --git a/Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift b/Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift index c18d198..f3c7546 100644 --- a/Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift +++ b/Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift @@ -16,7 +16,7 @@ extension ConsentDocument { #if !os(macOS) /// As the `PKDrawing.image()` function automatically converts the ink color dependent on the used color scheme (light or dark mode), /// force the ink used in the `UIImage` of the `PKDrawing` to always be black by adjusting the signature ink according to the color scheme. - private var blackInkSignatureImage: UIImage { + @MainActor private var blackInkSignatureImage: UIImage { var updatedDrawing = PKDrawing() for stroke in signature.strokes { @@ -54,6 +54,7 @@ extension ConsentDocument { /// - Note: This function avoids the use of asynchronous operations. /// Asynchronous tasks are incompatible with SwiftUI's `ImageRenderer`, /// which expects all rendering processes to be synchronous. + @MainActor private func exportBody(markdown: AttributedString) -> some View { VStack { if exportConfiguration.includingTimestamp { diff --git a/Sources/SpeziOnboarding/ConsentView/PDFDocument+Sendable.swift b/Sources/SpeziOnboarding/ConsentView/PDFDocument+Sendable.swift deleted file mode 100644 index 36e0f44..0000000 --- a/Sources/SpeziOnboarding/ConsentView/PDFDocument+Sendable.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import PDFKit - - -extension PDFDocument: @unchecked Sendable {} diff --git a/Sources/SpeziOnboarding/OnboardingConstraint.swift b/Sources/SpeziOnboarding/OnboardingConstraint.swift index 1b142ca..e36bdaa 100644 --- a/Sources/SpeziOnboarding/OnboardingConstraint.swift +++ b/Sources/SpeziOnboarding/OnboardingConstraint.swift @@ -16,5 +16,6 @@ public protocol OnboardingConstraint: Standard { /// Adds a new exported consent form represented as `PDFDocument` to the `Standard` conforming to ``OnboardingConstraint``. /// /// - Parameter consent: The exported consent form represented as `PDFDocument` that should be added. + @MainActor func store(consent: PDFDocument) async } diff --git a/Sources/SpeziOnboarding/OnboardingDataSource.swift b/Sources/SpeziOnboarding/OnboardingDataSource.swift index 3bb18c4..8347e82 100644 --- a/Sources/SpeziOnboarding/OnboardingDataSource.swift +++ b/Sources/SpeziOnboarding/OnboardingDataSource.swift @@ -43,9 +43,8 @@ public class OnboardingDataSource: Module, EnvironmentAccessible { /// Adds a new exported consent form represented as `PDFDocument` to the ``OnboardingDataSource``. /// /// - Parameter consent: The exported consent form represented as `PDFDocument` that should be added. + @MainActor public func store(_ consent: PDFDocument) async { - Task { @MainActor in - await standard.store(consent: consent) - } + await standard.store(consent: consent) } } diff --git a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStepIdentifier.swift b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStepIdentifier.swift index a5673b4..ded9b44 100644 --- a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStepIdentifier.swift +++ b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStepIdentifier.swift @@ -21,14 +21,18 @@ struct OnboardingStepIdentifier: Hashable, Codable { /// - Parameters: /// - view: The view used to initialize the identifier. /// - custom: A flag indicating whether the step is custom. + @MainActor init(view: V, custom: Bool = false) { self.custom = custom var hasher = Hasher() if let identifiable = view as? any Identifiable { let id = identifiable.id hasher.combine(id) + } else if let identifiable = view as? any OnboardingIdentifiable { + let id = identifiable.id + hasher.combine(id) } else { - hasher.combine(String(describing: type(of: view))) + hasher.combine(String(describing: type(of: view as Any))) } self.identifierHash = hasher.finalize() } diff --git a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingViewBuilder.swift b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingViewBuilder.swift index 1563e26..be0c545 100644 --- a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingViewBuilder.swift +++ b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingViewBuilder.swift @@ -14,7 +14,7 @@ import SwiftUI @resultBuilder public enum OnboardingViewBuilder { /// If declared, provides contextual type information for statement expressions to translate them into partial results. - public static func buildExpression(_ expression: any View) -> [any View] { + public static func buildExpression(_ expression: V) -> [any View] { [expression] } diff --git a/Sources/SpeziOnboarding/OnboardingIdentifiableViewModifier.swift b/Sources/SpeziOnboarding/OnboardingIdentifiableViewModifier.swift index 5e92a76..2003322 100644 --- a/Sources/SpeziOnboarding/OnboardingIdentifiableViewModifier.swift +++ b/Sources/SpeziOnboarding/OnboardingIdentifiableViewModifier.swift @@ -9,15 +9,30 @@ import SwiftUI -private struct OnboardingIdentifiableViewModifier: ViewModifier, Identifiable where ID: Hashable { +/// A `Identifiable` protocol that is isolated to the MainActor. +/// +/// The `id` property of the `Identifiable` protocol a non-isolation requirement we cannot fulfill. Therefore, we need to introduce our own requirement. +@MainActor +protocol OnboardingIdentifiable { + associatedtype ID: Hashable + + var id: ID { get } +} + + +@_documentation(visibility: internal) +public struct _OnboardingIdentifiableViewModifier: ViewModifier, OnboardingIdentifiable where ID: Hashable { + // swiftlint:disable:previous type_name let id: ID - func body(content: Content) -> some View { content } + public func body(content: Content) -> some View { + content + } } extension View { - /// Assign a unique identifier to a ``SwiftUI/View`` appearing in an ``OnboardingStack``. + /// Assign a unique identifier to a `View` appearing in an `OnboardingStack`. /// /// A `ViewModifier` assigning an identifier to the `View` it is applied to. /// When applying this modifier repeatedly, the outermost ``SwiftUI/View/onboardingIdentifier(_:)`` counts. @@ -29,7 +44,8 @@ extension View { /// /// ```swift /// struct Onboarding: View { - /// @AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false + /// @AppStorage(StorageKeys.onboardingFlowComplete) + /// var completedOnboardingFlow = false /// /// var body: some View { /// OnboardingStack(onboardingFlowComplete: $completedOnboardingFlow) { @@ -41,14 +57,17 @@ extension View { /// } /// } /// ``` - public func onboardingIdentifier(_ identifier: ID) -> some View where ID: Hashable { - modifier(OnboardingIdentifiableViewModifier(id: identifier)) + @MainActor + public func onboardingIdentifier(_ identifier: ID) -> ModifiedContent> { + // For some reason, we need to explicitly spell the return type, otherwise the type will be `AnyView`. + // Not sure how that happens, but it does with Xcode 16 toolchain. + modifier(_OnboardingIdentifiableViewModifier(id: identifier)) } } -extension ModifiedContent: Identifiable where Modifier: Identifiable { - public var id: Modifier.ID { +extension ModifiedContent: OnboardingIdentifiable where Modifier: OnboardingIdentifiable { + var id: Modifier.ID { self.modifier.id } } diff --git a/Tests/SpeziOnboardingTests/SpeziOnboardingTests.swift b/Tests/SpeziOnboardingTests/SpeziOnboardingTests.swift index 2c51a2e..5613633 100644 --- a/Tests/SpeziOnboardingTests/SpeziOnboardingTests.swift +++ b/Tests/SpeziOnboardingTests/SpeziOnboardingTests.swift @@ -16,6 +16,14 @@ final class SpeziOnboardingTests: XCTestCase { XCTAssert(true) } + @MainActor + func testAnyViewIssue() throws { + let view = Text("Hello World") + .onboardingIdentifier("Custom Identifier") + + XCTAssertFalse((view as Any) is AnyView) + } + @MainActor func testOnboardingIdentifierModifier() throws { let stack = OnboardingStack { diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift index f1791ed..a11b9a8 100644 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -14,19 +14,19 @@ import SwiftUI /// An example Standard used for the configuration. actor ExampleStandard: Standard, EnvironmentAccessible { - @Published @MainActor var consentData: PDFDocument = .init() + @MainActor private(set) var consentData: PDFDocument = .init() } extension ExampleStandard: OnboardingConstraint { + @MainActor func store(consent: PDFDocument) async { - await MainActor.run { - self.consentData = consent - } + self.consentData = consent try? await Task.sleep(for: .seconds(0.5)) } - + + @MainActor func loadConsent() async throws -> PDFDocument { - await self.consentData + self.consentData } } diff --git a/Tests/UITests/TestAppUITests/SpeziOnboardingTests.swift b/Tests/UITests/TestAppUITests/SpeziOnboardingTests.swift index 0cc1e14..8842ae5 100644 --- a/Tests/UITests/TestAppUITests/SpeziOnboardingTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziOnboardingTests.swift @@ -11,61 +11,69 @@ import XCTestExtensions final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_length + override func setUp() { + continueAfterFailure = false + } + + + @MainActor func testOverallOnboardingFlow() throws { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.buttons["Welcome View"].waitForExistence(timeout: 2)) app.buttons["Welcome View"].tap() // Check if on welcome page XCTAssert(app.staticTexts["Welcome"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["Spezi UI Tests"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Spezi UI Tests"].exists) - XCTAssert(app.buttons["Learn More"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Learn More"].exists) app.buttons["Learn More"].tap() // Check if on sequential onboarding view XCTAssert(app.staticTexts["Things to know"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["And you should pay close attention ..."].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["And you should pay close attention ..."].exists) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() - XCTAssert(app.buttons["Continue"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Continue"].waitForExistence(timeout: 2.0)) app.buttons["Continue"].tap() // Check if on consent (markdown) view XCTAssert(app.staticTexts["Consent"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["This is a markdown example"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["This is a markdown example"].exists) #if targetEnvironment(simulator) && (arch(i386) || arch(x86_64)) throw XCTSkip("PKCanvas view-related tests are currently skipped on Intel-based iOS simulators due to a metal bug on the simulator.") #endif - XCTAssert(app.staticTexts["First Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["First Name"].exists) try app.textFields["Enter your first name ..."].enter(value: "Leland") - XCTAssert(app.staticTexts["Last Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Last Name"].exists) try app.textFields["Enter your last name ..."].enter(value: "Stanford") XCTAssert(app.staticTexts["Name: Leland Stanford"].waitForExistence(timeout: 2)) #if !os(macOS) - XCTAssert(app.scrollViews["Signature Field"].waitForExistence(timeout: 2)) + XCTAssert(app.scrollViews["Signature Field"].exists) app.scrollViews["Signature Field"].swipeRight() #else - XCTAssert(app.textFields["Signature Field"].waitForExistence(timeout: 2)) + XCTAssert(app.textFields["Signature Field"].exists) try app.textFields["Signature Field"].enter(value: "Leland Stanford") #endif - hitConsentButton(app) + app.hitConsentButton() // Check if the consent export was successful XCTAssert(app.staticTexts["Consent PDF rendering exists"].waitForExistence(timeout: 2)) @@ -74,11 +82,11 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le app.buttons["Next"].tap() XCTAssert(app.staticTexts["Leland"].waitForExistence(timeout: 2)) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() XCTAssert(app.staticTexts["Stanford"].waitForExistence(timeout: 2)) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) @@ -88,124 +96,137 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(app.staticTexts["Onboarding complete"].waitForExistence(timeout: 2)) } + @MainActor func testOnboardingWelcomeView() throws { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.buttons["Welcome View"].waitForExistence(timeout: 2)) app.buttons["Welcome View"].tap() XCTAssert(app.staticTexts["Welcome"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["Spezi UI Tests"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Spezi UI Tests"].exists) - XCTAssert(app.staticTexts["Tortoise"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["A Tortoise!"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Tortoise"].exists) + XCTAssert(app.staticTexts["A Tortoise!"].exists) - XCTAssert(app.staticTexts["Tree"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["A Tree!"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Tree"].exists) + XCTAssert(app.staticTexts["A Tree!"].exists) - XCTAssert(app.staticTexts["Letter"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["A letter!"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Letter"].exists) + XCTAssert(app.staticTexts["A letter!"].exists) - XCTAssert(app.staticTexts["Circle"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["A circle!"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Circle"].exists) + XCTAssert(app.staticTexts["A circle!"].exists) - XCTAssert(app.buttons["Learn More"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Learn More"].exists) app.buttons["Learn More"].tap() XCTAssert(app.staticTexts["Things to know"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["And you should pay close attention ..."].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["And you should pay close attention ..."].exists) } + @MainActor func testSequentialOnboarding() throws { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.buttons["Sequential Onboarding"].waitForExistence(timeout: 2)) app.buttons["Sequential Onboarding"].tap() XCTAssert(app.staticTexts["Things to know"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["And you should pay close attention ..."].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["And you should pay close attention ..."].exists) - XCTAssert(app.staticTexts["1. A thing to know"].waitForExistence(timeout: 2)) - XCTAssertFalse(app.staticTexts["2. A second thing to know"].waitForExistence(timeout: 2)) - XCTAssertFalse(app.staticTexts["3. Third thing to know"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["1. A thing to know"].exists) + XCTAssertFalse(app.staticTexts["2. A second thing to know"].exists) + XCTAssertFalse(app.staticTexts["3. Third thing to know"].exists) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() XCTAssert(app.staticTexts["2. Second thing to know"].waitForExistence(timeout: 2)) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() XCTAssert(app.staticTexts["3. Third thing to know"].waitForExistence(timeout: 2)) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() XCTAssert(app.staticTexts["Now you should know all the things!"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["1. A thing to know"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["2. Second thing to know"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["3. Third thing to know"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["4."].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["1. A thing to know"].exists) + XCTAssert(app.staticTexts["2. Second thing to know"].exists) + XCTAssert(app.staticTexts["3. Third thing to know"].exists) + XCTAssert(app.staticTexts["4."].exists) - XCTAssert(app.buttons["Continue"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Continue"].exists) app.buttons["Continue"].tap() XCTAssert(app.staticTexts["Consent"].waitForExistence(timeout: 2)) } + @MainActor func testOnboardingConsentMarkdown() throws { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + // Test that the consent view can render markdown XCTAssert(app.buttons["Consent View (Markdown)"].waitForExistence(timeout: 2)) app.buttons["Consent View (Markdown)"].tap() XCTAssert(app.staticTexts["Consent"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["This is a markdown example"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["This is a markdown example"].exists) - XCTAssertFalse(app.staticTexts["Leland Stanford"].waitForExistence(timeout: 2)) - XCTAssertFalse(app.staticTexts["X"].waitForExistence(timeout: 2)) + XCTAssertFalse(app.staticTexts["Leland Stanford"].exists) + XCTAssertFalse(app.staticTexts["X"].exists) - hitConsentButton(app) + app.hitConsentButton() #if targetEnvironment(simulator) && (arch(i386) || arch(x86_64)) throw XCTSkip("PKCanvas view-related tests are currently skipped on Intel-based iOS simulators due to a metal bug on the simulator.") #endif - XCTAssert(app.staticTexts["First Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["First Name"].exists) try app.textFields["Enter your first name ..."].enter(value: "Leland") - XCTAssert(app.staticTexts["Last Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Last Name"].exists) try app.textFields["Enter your last name ..."].enter(value: "Stanford") - hitConsentButton(app) + app.hitConsentButton() XCTAssert(app.staticTexts["Name: Leland Stanford"].waitForExistence(timeout: 2)) #if !os(macOS) app.staticTexts["Name: Leland Stanford"].swipeRight() - XCTAssert(app.buttons["Undo"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Undo"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.buttons["Undo"].isEnabled) app.buttons["Undo"].tap() - XCTAssert(app.scrollViews["Signature Field"].waitForExistence(timeout: 2)) + XCTAssert(app.scrollViews["Signature Field"].exists) app.scrollViews["Signature Field"].swipeRight() #else - XCTAssert(app.textFields["Signature Field"].waitForExistence(timeout: 2)) + XCTAssert(app.textFields["Signature Field"].exists) try app.textFields["Signature Field"].enter(value: "Leland Stanford") #endif - hitConsentButton(app) + app.hitConsentButton() XCTAssert(app.staticTexts["Consent PDF rendering exists"].waitForExistence(timeout: 2)) } + @MainActor func testOnboardingConsentMarkdownRendering() throws { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + // Test that the consent view is not exported XCTAssert(app.buttons["Rendered Consent View (Markdown)"].waitForExistence(timeout: 2)) app.buttons["Rendered Consent View (Markdown)"].tap() @@ -213,7 +234,7 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(app.staticTexts["Consent PDF rendering doesn't exist"].waitForExistence(timeout: 2)) // Navigate back to start screen - XCTAssert(app.buttons["Back"].waitForExistence(timeout: 2)) + XCTAssert(app.navigationBars.buttons["Back"].exists) app.buttons["Back"].tap() // Go through markdown consent form and check rendering @@ -221,45 +242,45 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le app.buttons["Consent View (Markdown)"].tap() XCTAssert(app.staticTexts["Consent"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["This is a markdown example"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["This is a markdown example"].exists) - XCTAssertFalse(app.staticTexts["Leland Stanford"].waitForExistence(timeout: 2)) - XCTAssertFalse(app.staticTexts["X"].waitForExistence(timeout: 2)) + XCTAssertFalse(app.staticTexts["Leland Stanford"].exists) + XCTAssertFalse(app.staticTexts["X"].exists) - hitConsentButton(app) + app.hitConsentButton() #if targetEnvironment(simulator) && (arch(i386) || arch(x86_64)) throw XCTSkip("PKCanvas view-related tests are currently skipped on Intel-based iOS simulators due to a metal bug on the simulator.") #endif - XCTAssert(app.staticTexts["First Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["First Name"].exists) try app.textFields["Enter your first name ..."].enter(value: "Leland") - XCTAssert(app.staticTexts["Last Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Last Name"].exists) try app.textFields["Enter your last name ..."].enter(value: "Stanford") - hitConsentButton(app) + app.hitConsentButton() XCTAssert(app.staticTexts["Name: Leland Stanford"].waitForExistence(timeout: 2)) #if !os(macOS) - XCTAssert(app.scrollViews["Signature Field"].waitForExistence(timeout: 2)) + XCTAssert(app.scrollViews["Signature Field"].exists) app.scrollViews["Signature Field"].swipeRight() #else - XCTAssert(app.textFields["Signature Field"].waitForExistence(timeout: 2)) + XCTAssert(app.textFields["Signature Field"].exists) try app.textFields["Signature Field"].enter(value: "Leland Stanford") #endif - hitConsentButton(app) + app.hitConsentButton() XCTAssert(app.staticTexts["Consent PDF rendering exists"].waitForExistence(timeout: 2)) } #if !os(macOS) // Only test export on non macOS platforms + @MainActor func testOnboardingConsentPDFExport() throws { // swiftlint:disable:this function_body_length let app = XCUIApplication() let filesApp = XCUIApplication(bundleIdentifier: "com.apple.DocumentsApp") - let maxRetries = 10 app.launch() @@ -267,83 +288,106 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le app.buttons["Consent View (Markdown)"].tap() XCTAssert(app.staticTexts["Consent"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["This is a markdown example"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["This is a markdown example"].exists) - XCTAssert(app.staticTexts["First Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["First Name"].exists) try app.textFields["Enter your first name ..."].enter(value: "Leland") - XCTAssert(app.staticTexts["Last Name"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Last Name"].exists) try app.textFields["Enter your last name ..."].enter(value: "Stanford") XCTAssert(app.staticTexts["Name: Leland Stanford"].waitForExistence(timeout: 2)) - XCTAssert(app.scrollViews["Signature Field"].waitForExistence(timeout: 2)) + XCTAssert(app.scrollViews["Signature Field"].exists) app.scrollViews["Signature Field"].swipeRight() - sleep(1) + // Export consent form via share sheet button + XCTAssert(app.buttons["Share consent form"].waitForExistence(timeout: 4)) + app.buttons["Share consent form"].tap() - for _ in 0...maxRetries { - // Export consent form via share sheet button - XCTAssert(app.buttons["Share consent form"].waitForExistence(timeout: 2)) - app.buttons["Share consent form"].tap() - - // Store exported consent form in Files - #if os(visionOS) - // on visionOS the save to files button has no label + // Store exported consent form in Files +#if os(visionOS) + // on visionOS the save to files button has no label + if #available(visionOS 2.0, *) { + XCTAssert(app.cells["Save to Files"].waitForExistence(timeout: 10)) + app.cells["Save to Files"].tap() + } else { XCTAssert(app.cells["XCElementSnapshotPrivilegedValuePlaceholder"].waitForExistence(timeout: 10)) app.cells["XCElementSnapshotPrivilegedValuePlaceholder"].tap() - #else - XCTAssert(app.staticTexts["Save to Files"].waitForExistence(timeout: 10)) - app.staticTexts["Save to Files"].tap() - #endif - sleep(3) - XCTAssert(app.buttons["Save"].waitForExistence(timeout: 2)) - app.buttons["Save"].tap() - sleep(10) // Wait until file is saved - - if app.staticTexts["Replace Existing Items?"].waitForExistence(timeout: 5) { - XCTAssert(app.buttons["Replace"].waitForExistence(timeout: 2)) - app.buttons["Replace"].tap() - sleep(3) // Wait until file is saved - } + } +#else + XCTAssert(app.staticTexts["Save to Files"].waitForExistence(timeout: 10)) + app.staticTexts["Save to Files"].tap() +#endif + + XCTAssert(app.navigationBars.buttons["Save"].waitForExistence(timeout: 5)) + if !app.navigationBars.buttons["Save"].isEnabled { + throw XCTSkip("You currently cannot save anything in the files app in Xcode 16-based simulator.") + } + app.navigationBars.buttons["Save"].tap() + + if app.staticTexts["Replace Existing Items?"].waitForExistence(timeout: 2.0) { + XCTAssert(app.buttons["Replace"].exists) + app.buttons["Replace"].tap() + } - // Wait until share sheet closed and back on the consent form screen - XCTAssert(app.staticTexts["Consent"].waitForExistence(timeout: 10)) + // Wait until share sheet closed and back on the consent form screen + XCTAssertTrue(app.staticTexts["Name: Leland Stanford"].waitForExistence(timeout: 10)) - XCUIDevice.shared.press(.home) + XCUIDevice.shared.press(.home) - // Launch the Files app - filesApp.launch() + // Launch the Files app + filesApp.launch() + XCTAssertTrue(filesApp.wait(for: .runningForeground, timeout: 2.0)) - // Handle already open files - if filesApp.buttons["Done"].waitForExistence(timeout: 2) { - filesApp.buttons["Done"].tap() + // Handle already open files on iOS + if filesApp.navigationBars.buttons["Done"].waitForExistence(timeout: 2) { + filesApp.navigationBars.buttons["Done"].tap() + } + + // If the file already shows up in the Recents view, we are good. + // Otherwise navigate to "On My iPhone"/"On My iPad"/"On My Apple Vision Pro" view + if !filesApp.staticTexts["Signed Consent Form"].waitForExistence(timeout: 2) { +#if os(visionOS) + XCTAssertTrue(filesApp.staticTexts["On My Apple Vision Pro"].waitForExistence(timeout: 2.0)) + filesApp.staticTexts["On My Apple Vision Pro"].tap() + XCTAssertTrue(filesApp.navigationBars.staticTexts["On My Apple Vision Pro"].waitForExistence(timeout: 2.0)) +#else + if filesApp.navigationBars.buttons["Show Sidebar"].exists && !filesApp.buttons["Browse"].exists { + // we are running on iPad which is not iOS 18! + filesApp.navigationBars.buttons["Show Sidebar"].tap() + XCTAssertTrue(filesApp.staticTexts["On My iPad"].waitForExistence(timeout: 2.0)) + filesApp.staticTexts["On My iPad"].tap() + XCTAssertTrue(filesApp.navigationBars.staticTexts["On My iPad"].waitForExistence(timeout: 2.0)) } - // Check if file exists - If not, try the export procedure again - // Saving to files is very flakey on the runners, needs multiple attempts to succeed - if filesApp.staticTexts["Signed Consent Form"].waitForExistence(timeout: 2) { - break + if filesApp.tabBars.buttons["Browse"].exists { // iPhone + filesApp.tabBars.buttons["Browse"].tap() + XCTAssertTrue(filesApp.navigationBars.staticTexts["On My iPhone"].waitForExistence(timeout: 2.0)) + } else { // iPad + if !filesApp.navigationBars.staticTexts["On My iPad"].exists { // we aren't already in browse + XCTAssertTrue(filesApp.buttons["Browse"].exists) + filesApp.buttons["Browse"].tap() + XCTAssertTrue(filesApp.navigationBars.staticTexts["On My iPad"].waitForExistence(timeout: 2.0)) + } } +#endif - // Launch test app and try another export - app.launch() + XCTAssert(filesApp.staticTexts["Signed Consent Form"].waitForExistence(timeout: 2.0)) } + XCTAssert(filesApp.collectionViews["File View"].cells["Signed Consent Form, pdf"].exists) - // Open File - XCTAssert(filesApp.staticTexts["Signed Consent Form"].waitForExistence(timeout: 2)) - XCTAssert(filesApp.collectionViews["File View"].cells["Signed Consent Form, pdf"].waitForExistence(timeout: 2)) - - XCTAssert(filesApp.collectionViews["File View"].cells["Signed Consent Form, pdf"].images.firstMatch.waitForExistence(timeout: 2)) + XCTAssert(filesApp.collectionViews["File View"].cells["Signed Consent Form, pdf"].images.firstMatch.exists) filesApp.collectionViews["File View"].cells["Signed Consent Form, pdf"].images.firstMatch.tap() - sleep(3) // Wait until file is opened - - #if os(visionOS) let fileView = XCUIApplication(bundleIdentifier: "com.apple.MRQuickLook") + XCTAssertTrue(fileView.wait(for: .runningForeground, timeout: 5.0)) #else let fileView = filesApp + + // Wait until file is opened + XCTAssertTrue(fileView.navigationBars["Signed Consent Form"].waitForExistence(timeout: 5.0)) #endif // Check if PDF contains consent title, name, and markdown message @@ -360,55 +404,67 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le } #endif + @MainActor func testOnboardingCustomViews() throws { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.buttons["Custom Onboarding View 1"].waitForExistence(timeout: 2)) app.buttons["Custom Onboarding View 1"].tap() // Check if on custom test view 1 XCTAssert(app.staticTexts["Custom Test View 1: Hello Spezi!"].waitForExistence(timeout: 2)) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() // Check if on custom test view 2 XCTAssert(app.staticTexts["Custom Test View 2"].waitForExistence(timeout: 2)) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() // Check if on welcome onboarding view XCTAssert(app.staticTexts["Welcome"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["Spezi UI Tests"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Spezi UI Tests"].exists) } + @MainActor func testDynamicOnboardingFlow1() throws { let app = XCUIApplication() app.launch() - try dynamicOnboardingFlow(app: app, showConditionalView: false) + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + + try app.dynamicOnboardingFlow(showConditionalView: false) } + @MainActor func testDynamicOnboardingFlow2() throws { let app = XCUIApplication() app.launch() - try dynamicOnboardingFlow(app: app, showConditionalView: true) + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + + try app.dynamicOnboardingFlow(showConditionalView: true) } + @MainActor func testDynamicOnboardingFlow3() throws { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.buttons["Rendered Consent View (Markdown)"].waitForExistence(timeout: 2)) app.buttons["Rendered Consent View (Markdown)"].tap() // Check if on consent export page XCTAssert(app.staticTexts["Consent PDF rendering doesn't exist"].waitForExistence(timeout: 2)) - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) @@ -420,69 +476,25 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(app.buttons["Show Conditional View"].waitForExistence(timeout: 2)) app.buttons["Show Conditional View"].tap() - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() // Check if on conditional test view XCTAssert(app.staticTexts["Conditional Test View"].waitForExistence(timeout: 2)) - - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) - app.buttons["Next"].tap() - - // Check if on final page - XCTAssert(app.staticTexts["Onboarding complete"].waitForExistence(timeout: 2)) - } - - private func hitConsentButton(_ app: XCUIApplication) { - if app.staticTexts["This is a markdown example"].isHittable { - app.staticTexts["This is a markdown example"].swipeUp() - } else { - print("Can not scroll down.") - } - XCTAssert(app.buttons["I Consent"].waitForExistence(timeout: 2)) - app.buttons["I Consent"].tap() - } - - private func dynamicOnboardingFlow(app: XCUIApplication, showConditionalView: Bool) throws { - // Dynamically show onboarding views - if showConditionalView { - XCTAssert(app.buttons["Show Conditional View"].waitForExistence(timeout: 2)) - app.buttons["Show Conditional View"].tap() - } - - XCTAssert(app.buttons["Rendered Consent View (Markdown)"].waitForExistence(timeout: 2)) - app.buttons["Rendered Consent View (Markdown)"].tap() - - // Check if on consent export page - XCTAssert(app.staticTexts["Consent PDF rendering doesn't exist"].waitForExistence(timeout: 2)) - - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) - app.buttons["Next"].tap() - - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) + XCTAssert(app.buttons["Next"].exists) app.buttons["Next"].tap() - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) - app.buttons["Next"].tap() - - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) - app.buttons["Next"].tap() - - if showConditionalView { - // Check if on conditional test view - XCTAssert(app.staticTexts["Conditional Test View"].waitForExistence(timeout: 2)) - - XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) - app.buttons["Next"].tap() - } - // Check if on final page XCTAssert(app.staticTexts["Onboarding complete"].waitForExistence(timeout: 2)) } + @MainActor func testIdentifiableViews() throws { let app = XCUIApplication() app.launch() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + app.buttons["Onboarding Identifiable View"].tap() XCTAssert(app.staticTexts["ID: 1"].waitForExistence(timeout: 2)) @@ -490,5 +502,9 @@ final class OnboardingTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(app.staticTexts["ID: 2"].waitForExistence(timeout: 2)) app.buttons["Next"].tap() + + XCTAssert(app.staticTexts["Welcome"].waitForExistence(timeout: 2)) } } + +// swiftlint:disable:this file_length diff --git a/Tests/UITests/TestAppUITests/XCUIApplication+Onboarding.swift b/Tests/UITests/TestAppUITests/XCUIApplication+Onboarding.swift new file mode 100644 index 0000000..fc471d4 --- /dev/null +++ b/Tests/UITests/TestAppUITests/XCUIApplication+Onboarding.swift @@ -0,0 +1,59 @@ +// +// 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 + + +extension XCUIApplication { + func hitConsentButton() { + if staticTexts["This is a markdown example"].isHittable { + staticTexts["This is a markdown example"].swipeUp() + } else { + print("Can not scroll down.") + } + XCTAssert(buttons["I Consent"].waitForExistence(timeout: 2)) + buttons["I Consent"].tap() + } + + func dynamicOnboardingFlow(showConditionalView: Bool) throws { + // Dynamically show onboarding views + if showConditionalView { + XCTAssert(buttons["Show Conditional View"].waitForExistence(timeout: 2)) + buttons["Show Conditional View"].tap() + } + + XCTAssert(buttons["Rendered Consent View (Markdown)"].waitForExistence(timeout: 2)) + buttons["Rendered Consent View (Markdown)"].tap() + + // Check if on consent export page + XCTAssert(staticTexts["Consent PDF rendering doesn't exist"].waitForExistence(timeout: 2)) + + XCTAssert(buttons["Next"].exists) + buttons["Next"].tap() + + XCTAssert(buttons["Next"].waitForExistence(timeout: 2)) + buttons["Next"].tap() + + XCTAssert(buttons["Next"].waitForExistence(timeout: 2)) + buttons["Next"].tap() + + XCTAssert(buttons["Next"].waitForExistence(timeout: 2)) + buttons["Next"].tap() + + if showConditionalView { + // Check if on conditional test view + XCTAssert(staticTexts["Conditional Test View"].waitForExistence(timeout: 2)) + + XCTAssert(buttons["Next"].exists) + buttons["Next"].tap() + } + + // Check if on final page + XCTAssert(staticTexts["Onboarding complete"].waitForExistence(timeout: 2)) + } +} diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 97e7633..fca9f94 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -12,8 +12,8 @@ 2F61BDCB29DDE76D00D71D33 /* SpeziOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F61BDCA29DDE76D00D71D33 /* SpeziOnboardingTests.swift */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; - 61D77B542BC83F0100E3165F /* OnboardingCustomToggleTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D77B532BC83F0100E3165F /* OnboardingCustomToggleTestView.swift */; }; 61040A1D2BAFA2F600EDD4EC /* OnboardingIdentifiableTestViewCustom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61040A1B2BAFA2F600EDD4EC /* OnboardingIdentifiableTestViewCustom.swift */; }; + 61D77B542BC83F0100E3165F /* OnboardingCustomToggleTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D77B532BC83F0100E3165F /* OnboardingCustomToggleTestView.swift */; }; 61F1697E2BCA888600D1622B /* OnboardingTestViewNotIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1697D2BCA888600D1622B /* OnboardingTestViewNotIdentifiable.swift */; }; 970D444B2A6F031200756FE2 /* OnboardingConsentMarkdownTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970D444A2A6F031200756FE2 /* OnboardingConsentMarkdownTestView.swift */; }; 970D444F2A6F048A00756FE2 /* OnboardingWelcomeTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970D444E2A6F048A00756FE2 /* OnboardingWelcomeTestView.swift */; }; @@ -28,6 +28,7 @@ 97C6AF7B2ACC89000060155B /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 97C6AF7A2ACC89000060155B /* XCTestExtensions */; }; 97C6AF7D2ACC92CB0060155B /* OnboardingConsentMarkdownRenderingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C6AF7C2ACC92CB0060155B /* OnboardingConsentMarkdownRenderingView.swift */; }; 97C6AF7F2ACC94450060155B /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C6AF7E2ACC94450060155B /* OnboardingFlow+PreviewSimulator.swift */; }; + A950C9C02C68AFAD0052FA6D /* XCUIApplication+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A950C9BF2C68AFA80052FA6D /* XCUIApplication+Onboarding.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,8 +50,8 @@ 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; - 61D77B532BC83F0100E3165F /* OnboardingCustomToggleTestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingCustomToggleTestView.swift; sourceTree = ""; }; 61040A1B2BAFA2F600EDD4EC /* OnboardingIdentifiableTestViewCustom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingIdentifiableTestViewCustom.swift; sourceTree = ""; }; + 61D77B532BC83F0100E3165F /* OnboardingCustomToggleTestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingCustomToggleTestView.swift; sourceTree = ""; }; 61F1697D2BCA888600D1622B /* OnboardingTestViewNotIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTestViewNotIdentifiable.swift; sourceTree = ""; }; 970D444A2A6F031200756FE2 /* OnboardingConsentMarkdownTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingConsentMarkdownTestView.swift; sourceTree = ""; }; 970D444E2A6F048A00756FE2 /* OnboardingWelcomeTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingWelcomeTestView.swift; sourceTree = ""; }; @@ -64,6 +65,7 @@ 97C6AF782ACC88270060155B /* TestAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; 97C6AF7C2ACC92CB0060155B /* OnboardingConsentMarkdownRenderingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingConsentMarkdownRenderingView.swift; sourceTree = ""; }; 97C6AF7E2ACC94450060155B /* OnboardingFlow+PreviewSimulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingFlow+PreviewSimulator.swift"; sourceTree = ""; }; + A950C9BF2C68AFA80052FA6D /* XCUIApplication+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+Onboarding.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -123,6 +125,7 @@ 2F6D13AF28F5F386007C25D6 /* TestAppUITests */ = { isa = PBXGroup; children = ( + A950C9BF2C68AFA80052FA6D /* XCUIApplication+Onboarding.swift */, 2F61BDCA29DDE76D00D71D33 /* SpeziOnboardingTests.swift */, ); path = TestAppUITests; @@ -318,6 +321,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A950C9C02C68AFAD0052FA6D /* XCUIApplication+Onboarding.swift in Sources */, 2F61BDCB29DDE76D00D71D33 /* SpeziOnboardingTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -392,6 +396,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; @@ -448,6 +453,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; XROS_DEPLOYMENT_TARGET = 1.0; }; @@ -627,6 +633,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = TEST; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Test;