diff --git a/Package.swift b/Package.swift index 17c795a..885dbd8 100644 --- a/Package.swift +++ b/Package.swift @@ -98,7 +98,7 @@ func swiftLintPlugin() -> [Target.PluginUsage] { 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"))] + [.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")] } else { [] } diff --git a/Sources/SpeziValidation/SpeziValidation.docc/SpeziValidation.md b/Sources/SpeziValidation/SpeziValidation.docc/SpeziValidation.md index c161f3b..dbf3b2d 100644 --- a/Sources/SpeziValidation/SpeziValidation.docc/SpeziValidation.md +++ b/Sources/SpeziValidation/SpeziValidation.docc/SpeziValidation.md @@ -81,6 +81,7 @@ var body: some View { - ``ValidationRule`` - ``SwiftUI/View/validate(input:rules:)-5dac4`` - ``SwiftUI/View/validate(input:rules:)-9vks0`` +- ``SwiftUI/View/validate(_:message:)`` ### Managing Validation diff --git a/Sources/SpeziValidation/ValidationModifier.swift b/Sources/SpeziValidation/ValidationModifier.swift index f89dc58..42dc561 100644 --- a/Sources/SpeziValidation/ValidationModifier.swift +++ b/Sources/SpeziValidation/ValidationModifier.swift @@ -109,4 +109,59 @@ extension View { public func validate(input value: String, rules: ValidationRule...) -> some View { validate(input: value, rules: rules) } + + + /// Validate a `Bool` expression. + /// + /// This modifier can be used to validate a `Bool` predicate. If the expression doesn't evaluate to `true`, the `message` + /// is shown as a validation error. + /// + /// Validation is managed through a ``ValidationEngine`` instance that is injected as an `Observable` into the + /// environment. + /// + /// Below is an example that uses the `validate(_:message:)` modifier to validate the selection of a `Picker`. + /// - Note: The example uses the ``receiveValidation(in:)`` modifier to retrieve the validation results of the subview. + /// The ``ValidationResultsView`` is used to render the failure reason in red text below the picker. + /// + /// ```swift + /// struct MyView: View { + /// enum Selection: String, CaseIterable, Hashable { + /// case none = "Nothing selected" + /// case accept = "Accept" + /// case deny = "Deny" + /// } + /// + /// @State private var selection: Selection = .none + /// @ValidationState private var validationState + /// + /// var body: some View { + /// VStack(alignment: .leading) { + /// Picker(selection: $selection) { + /// ForEach(Selection.allCases, id: \.rawValue) { selection in + /// Text(selection.rawValue) + /// .tag(selection) + /// } + /// } label: { + /// Text("Cookies") + /// } + /// ValidationResultsView(results: validationState.allDisplayedValidationResults) + /// } + /// .validate(selection != .none, message: "This field must be selected") + /// .receiveValidation(in: $validationState) + /// } + /// } + /// ``` + /// + /// - Important: You shouldn't place multiple validate modifiers into the same view hierarchy branch. This creates + /// visibility problems in both direction. Both displaying validation results in the child view and receiving + /// validation state from the parent view. + /// + /// - Parameters: + /// - predicate: The predicate to validate. + /// - message: The validation message that is used as an failure reason, if the predicate evaluates to `false`. + /// - Returns: The modified view. + public func validate(_ predicate: Bool, message: LocalizedStringResource) -> some View { + let rule = ValidationRule(rule: { $0.isEmpty }, message: message) + return validate(input: predicate ? "" : "FALSE", rules: [rule]) + } } diff --git a/Tests/UITests/TestApp/Localizable.xcstrings b/Tests/UITests/TestApp/Localizable.xcstrings index 339edc0..df3f02e 100644 --- a/Tests/UITests/TestApp/Localizable.xcstrings +++ b/Tests/UITests/TestApp/Localizable.xcstrings @@ -31,6 +31,9 @@ }, "Condition present" : { + }, + "Cookies" : { + }, "Credentials" : { @@ -153,6 +156,9 @@ }, "Targets" : { + }, + "This field must be selected." : { + }, "This is a default error description!" : { diff --git a/Tests/UITests/TestApp/ValidationTests/SpeziValidationTests.swift b/Tests/UITests/TestApp/ValidationTests/SpeziValidationTests.swift index be9bd54..0414db5 100644 --- a/Tests/UITests/TestApp/ValidationTests/SpeziValidationTests.swift +++ b/Tests/UITests/TestApp/ValidationTests/SpeziValidationTests.swift @@ -6,7 +6,6 @@ // SPDX-License-Identifier: MIT // -import SpeziPersonalInfo import SwiftUI import XCTestApp @@ -14,6 +13,7 @@ import XCTestApp enum SpeziValidationTests: String, TestAppTests { case validation = "Validation" case validationRules = "ValidationRules" + case validationPredicate = "Validation Picker" func view(withNavigationPath path: Binding) -> some View { switch self { @@ -21,6 +21,8 @@ enum SpeziValidationTests: String, TestAppTests { FocusedValidationTests() case .validationRules: DefaultValidationRules() + case .validationPredicate: + ValidationPredicateTests() } } } diff --git a/Tests/UITests/TestApp/ValidationTests/ValidationPredicateTests.swift b/Tests/UITests/TestApp/ValidationTests/ValidationPredicateTests.swift new file mode 100644 index 0000000..f11ba11 --- /dev/null +++ b/Tests/UITests/TestApp/ValidationTests/ValidationPredicateTests.swift @@ -0,0 +1,40 @@ +// +// 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 SpeziValidation +import SwiftUI + + +struct ValidationPredicateTests: View { + enum Selection: String, CaseIterable, Hashable { + case none = "Nothing selected" + case accept = "Accept" + case deny = "Deny" + } + + @State private var selection: Selection = .none + @ValidationState private var validationState + + var body: some View { + List { + VStack(alignment: .leading) { + Picker(selection: $selection) { + ForEach(Selection.allCases, id: \.rawValue) { selection in + Text(selection.rawValue) + .tag(selection) + } + } label: { + Text("Cookies") + } + ValidationResultsView(results: validationState.allDisplayedValidationResults) + } + .validate(selection != .none, message: "This field must be selected.") + .receiveValidation(in: $validationState) + } + } +} diff --git a/Tests/UITests/TestApp/ViewsTests/MarkdownViewTestView.swift b/Tests/UITests/TestApp/ViewsTests/MarkdownViewTestView.swift index 9ce591b..63bd0a6 100644 --- a/Tests/UITests/TestApp/ViewsTests/MarkdownViewTestView.swift +++ b/Tests/UITests/TestApp/ViewsTests/MarkdownViewTestView.swift @@ -16,8 +16,8 @@ struct MarkdownViewTestView: View { var body: some View { MarkdownView( asyncMarkdown: { - try? await Task.sleep(for: .seconds(5)) - return Data("This is a *markdown* **example** taking 5 seconds to load.".utf8) + try? await Task.sleep(for: .seconds(2)) + return Data("This is a *markdown* **example** taking 2 seconds to load.".utf8) } ) MarkdownView( diff --git a/Tests/UITests/TestAppUITests/SpeziPersonalInfo/PersonalInfoViewsTests.swift b/Tests/UITests/TestAppUITests/SpeziPersonalInfo/PersonalInfoViewsTests.swift index e49134d..3fb8022 100644 --- a/Tests/UITests/TestAppUITests/SpeziPersonalInfo/PersonalInfoViewsTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziPersonalInfo/PersonalInfoViewsTests.swift @@ -11,21 +11,18 @@ import XCTestExtensions final class PersonalInfoViewsTests: XCTestCase { - @MainActor override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false - - let app = XCUIApplication() - app.launch() - - app.open(target: "SpeziPersonalInfo") } @MainActor func testNameFields() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziPersonalInfo") XCTAssert(app.buttons["Name Fields"].waitForExistence(timeout: 2)) app.buttons["Name Fields"].tap() @@ -43,6 +40,9 @@ final class PersonalInfoViewsTests: XCTestCase { @MainActor func testUserProfile() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziPersonalInfo") XCTAssert(app.buttons["User Profile"].waitForExistence(timeout: 2)) app.buttons["User Profile"].tap() diff --git a/Tests/UITests/TestAppUITests/SpeziValidation/ValidationTests.swift b/Tests/UITests/TestAppUITests/SpeziValidation/ValidationTests.swift index 096f6b2..3413542 100644 --- a/Tests/UITests/TestAppUITests/SpeziValidation/ValidationTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziValidation/ValidationTests.swift @@ -11,21 +11,18 @@ import XCTestExtensions final class ValidationTests: XCTestCase { - @MainActor override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false - - let app = XCUIApplication() - app.launch() - - app.open(target: "SpeziValidation") } @MainActor func testDefaultRules() { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziValidation") XCTAssert(app.buttons["ValidationRules"].waitForExistence(timeout: 2)) app.buttons["ValidationRules"].tap() @@ -34,6 +31,8 @@ final class ValidationTests: XCTestCase { @MainActor func testValidationWithFocus() throws { let app = XCUIApplication() + app.launch() + app.open(target: "SpeziValidation") let passwordMessage = "Your password must be at least 8 characters long." let emptyMessage = "This field cannot be empty." @@ -60,9 +59,7 @@ final class ValidationTests: XCTestCase { XCTAssertTrue(app.staticTexts[emptyMessage].exists) #if os(visionOS) - throw XCTSkip( - "This test is flakey on visionOS as the keyboard might be in front of the application and taps below will trigger keyboard buttons!" - ) + throw XCTSkip("Test is flakey on visionOS as the keyboard might be in front of the application and taps below will trigger keyboard buttons!") #endif #if os(macOS) @@ -100,4 +97,30 @@ final class ValidationTests: XCTestCase { XCTAssertTrue(app.textFields["Hello World!"].exists) XCTAssertTrue(app.textFields["Word"].exists) } + + @MainActor + func testValidationPredicate() throws { + let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziValidation") + + XCTAssert(app.buttons["Validation Picker"].waitForExistence(timeout: 2)) + app.buttons["Validation Picker"].tap() + + XCTAssertTrue(app.buttons["Cookies, Nothing selected"].waitForExistence(timeout: 2.0)) + app.buttons["Cookies, Nothing selected"].tap() + + XCTAssertTrue(app.buttons["Accept"].waitForExistence(timeout: 2.0)) + app.buttons["Accept"].tap() + + XCTAssertTrue(app.buttons["Cookies, Accept"].waitForExistence(timeout: 2.0)) + app.buttons["Cookies, Accept"].tap() + + XCTAssertTrue(app.buttons["Nothing selected"].waitForExistence(timeout: 2.0)) + app.buttons["Nothing selected"].tap() + + XCTAssertTrue(app.buttons["Cookies, Nothing selected"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.staticTexts["This field must be selected."].waitForExistence(timeout: 2.0)) + } } diff --git a/Tests/UITests/TestAppUITests/SpeziViews/EnvironmentTests.swift b/Tests/UITests/TestAppUITests/SpeziViews/EnvironmentTests.swift index d61e717..392f7eb 100644 --- a/Tests/UITests/TestAppUITests/SpeziViews/EnvironmentTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziViews/EnvironmentTests.swift @@ -11,22 +11,19 @@ import XCTestExtensions final class EnvironmentTests: XCTestCase { - @MainActor override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false + } + @MainActor + func testDefaultErrorDescription() throws { let app = XCUIApplication() app.launch() app.open(target: "SpeziViews") - } - @MainActor - func testDefaultErrorDescription() throws { - let app = XCUIApplication() - #if os(visionOS) app.buttons["View State"].swipeUp() #endif @@ -36,15 +33,14 @@ final class EnvironmentTests: XCTestCase { XCTAssert(app.staticTexts["View State: processing"].waitForExistence(timeout: 2)) - sleep(12) - #if os(macOS) let alerts = app.sheets #else let alerts = app.alerts #endif - XCTAssert(alerts.staticTexts["This is a default error description!"].exists) + XCTAssert(alerts.staticTexts["This is a default error description!"].waitForExistence(timeout: 6.0)) XCTAssert(alerts.staticTexts["Failure Reason\n\nHelp Anchor\n\nRecovery Suggestion"].exists) + XCTAssertTrue(alerts.buttons["OK"].exists) alerts.buttons["OK"].tap() XCTAssert(app.staticTexts["View State: idle"].waitForExistence(timeout: 2)) diff --git a/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift b/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift index ccbdac4..2bc022a 100644 --- a/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift @@ -11,21 +11,18 @@ import XCTestExtensions final class ModelTests: XCTestCase { - @MainActor override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false - - let app = XCUIApplication() - app.launch() - - app.open(target: "SpeziViews") } @MainActor func testViewState() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") XCTAssert(app.buttons["View State"].waitForExistence(timeout: 2)) app.buttons["View State"].tap() @@ -50,6 +47,9 @@ final class ModelTests: XCTestCase { @MainActor func testOperationState() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") XCTAssert(app.buttons["Operation State"].waitForExistence(timeout: 2)) app.buttons["Operation State"].tap() @@ -81,6 +81,9 @@ final class ModelTests: XCTestCase { @MainActor func testViewStateMapper() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") XCTAssert(app.buttons["View State Mapper"].waitForExistence(timeout: 2)) app.buttons["View State Mapper"].tap() @@ -116,6 +119,9 @@ final class ModelTests: XCTestCase { @MainActor func testConditionalModifier() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") XCTAssert(app.buttons["Conditional Modifier"].waitForExistence(timeout: 2)) app.buttons["Conditional Modifier"].tap() @@ -149,6 +155,9 @@ final class ModelTests: XCTestCase { @MainActor func testDefaultErrorDescription() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") XCTAssert(app.buttons["Default Error Only"].waitForExistence(timeout: 2)) app.buttons["Default Error Only"].tap() diff --git a/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift b/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift index c60f87d..a567ca8 100644 --- a/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift @@ -11,16 +11,10 @@ import XCTestExtensions final class ViewsTests: XCTestCase { - @MainActor override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false - - let app = XCUIApplication() - app.launch() - - app.open(target: "SpeziViews") } @MainActor @@ -34,47 +28,59 @@ final class ViewsTests: XCTestCase { #endif let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") + + #if os(visionOS) + // visionOS doesn't have the image anymore, this should be enough to check + let paletteView = app.scrollViews.otherElements["Pen, black"] + #else + let paletteView = app.images["palette_tool_pencil_base"] + #endif XCTAssert(app.collectionViews.buttons["Canvas"].waitForExistence(timeout: 2)) app.collectionViews.buttons["Canvas"].tap() XCTAssert(app.staticTexts["Did Draw Anything: false"].waitForExistence(timeout: 2)) - XCTAssertFalse(app.images["palette_tool_pencil_base"].waitForExistence(timeout: 2)) + XCTAssertFalse(paletteView.exists) let canvasView = app.scrollViews.firstMatch canvasView.swipeRight() canvasView.swipeDown() - + XCTAssert(app.staticTexts["Did Draw Anything: true"].waitForExistence(timeout: 2)) - + XCTAssert(app.buttons["Show Tool Picker"].waitForExistence(timeout: 2)) app.buttons["Show Tool Picker"].tap() - #if os(visionOS) - // visionOS doesn't have the image anymore, this should be enough to check - XCTAssert(app.scrollViews.otherElements["Pen, black"].waitForExistence(timeout: 2.0)) - #else - XCTAssert(app.images["palette_tool_pencil_base"].waitForExistence(timeout: 10)) - #endif + XCTAssertTrue(paletteView.waitForExistence(timeout: 5)) canvasView.swipeLeft() - sleep(1) + XCTAssertTrue(canvasView.waitForExistence(timeout: 2.0)) app.buttons["Show Tool Picker"].tap() #if os(visionOS) - return // currently the pencilKit toolbar cannot be hidden anymore + return // the pencilKit toolbar cannot be hidden anymore on visionOS #endif +#if compiler(>=6) + XCTAssertTrue(paletteView.waitForNonExistence(withTimeout: 15)) +#else sleep(15) // waitForExistence will otherwise return immediately - XCTAssertFalse(app.images["palette_tool_pencil_base"].exists) + XCTAssertFalse(paletteView.exists) +#endif canvasView.swipeUp() } @MainActor func testGeometryReader() throws { let app = XCUIApplication() - + app.launch() + + app.open(target: "SpeziViews") + XCTAssert(app.buttons["Geometry Reader"].waitForExistence(timeout: 2)) app.buttons["Geometry Reader"].tap() @@ -88,12 +94,15 @@ final class ViewsTests: XCTestCase { throw XCTSkip("Label is not supported on non-UIKit platforms") #endif let app = XCUIApplication() - + app.launch() + + app.open(target: "SpeziViews") + XCTAssert(app.collectionViews.buttons["Label"].waitForExistence(timeout: 2)) app.collectionViews.buttons["Label"].tap() sleep(2) - + // The string value needs to be searched for in the UI. // swiftlint:disable:next line_length let text = "This is a label ... An other text. This is longer and we can check if the justified text works as expected. This is a very long text." @@ -103,7 +112,10 @@ final class ViewsTests: XCTestCase { @MainActor func testLazyText() throws { let app = XCUIApplication() - + app.launch() + + app.open(target: "SpeziViews") + XCTAssert(app.buttons["Lazy Text"].waitForExistence(timeout: 2)) app.buttons["Lazy Text"].tap() @@ -116,17 +128,23 @@ final class ViewsTests: XCTestCase { @MainActor func testMarkdownView() throws { let app = XCUIApplication() - + app.launch() + + app.open(target: "SpeziViews") + XCTAssert(app.buttons["Markdown View"].waitForExistence(timeout: 2)) app.buttons["Markdown View"].tap() XCTAssert(app.staticTexts["This is a markdown example."].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["This is a markdown example taking 5 seconds to load."].waitForExistence(timeout: 10)) + XCTAssert(app.staticTexts["This is a markdown example taking 2 seconds to load."].waitForExistence(timeout: 5)) } @MainActor func testAsyncButtonView() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) #if os(visionOS) @@ -161,6 +179,9 @@ final class ViewsTests: XCTestCase { @MainActor func testListRowAccessibility() throws { let app = XCUIApplication() + app.launch() + + app.open(target: "SpeziViews") XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) #if os(visionOS) diff --git a/Tests/UITests/TestAppUITests/XCUIApplication+Targets.swift b/Tests/UITests/TestAppUITests/XCUIApplication+Targets.swift index 41a1894..7473cde 100644 --- a/Tests/UITests/TestAppUITests/XCUIApplication+Targets.swift +++ b/Tests/UITests/TestAppUITests/XCUIApplication+Targets.swift @@ -12,7 +12,7 @@ import XCTest extension XCUIApplication { func open(target: String) { XCTAssertTrue(staticTexts["Targets"].waitForExistence(timeout: 6.0)) - XCTAssertTrue(buttons[target].waitForExistence(timeout: 0.5)) + XCTAssertTrue(buttons[target].exists) buttons[target].tap() } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index e79aba6..b7f2d40 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 977CF55C2AD2B92C006D9B54 /* XCTestApp in Frameworks */ = {isa = PBXBuildFile; productRef = 977CF55B2AD2B92C006D9B54 /* XCTestApp */; }; 97A0A5102B8D7FD7006102EF /* ConditionalModifierTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97A0A50F2B8D7FD7006102EF /* ConditionalModifierTestView.swift */; }; 97EE16AC2B16D5AB004D25A3 /* OperationStateTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */; }; + A91E4DE62C6A1EAE00E0E5A7 /* ValidationPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91E4DE52C6A1EAE00E0E5A7 /* ValidationPredicateTests.swift */; }; A95B6E652AF4298500919504 /* SpeziPersonalInfo in Frameworks */ = {isa = PBXBuildFile; productRef = A95B6E642AF4298500919504 /* SpeziPersonalInfo */; }; A963ACAC2AF4683A00D745F2 /* SpeziValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A963ACAB2AF4683A00D745F2 /* SpeziValidationTests.swift */; }; A963ACB02AF4692500D745F2 /* SpeziValidation in Frameworks */ = {isa = PBXBuildFile; productRef = A963ACAF2AF4692500D745F2 /* SpeziValidation */; }; @@ -71,6 +72,7 @@ 9731B58E2B167053007676C0 /* ViewStateMapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStateMapperView.swift; sourceTree = ""; }; 97A0A50F2B8D7FD7006102EF /* ConditionalModifierTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalModifierTestView.swift; sourceTree = ""; }; 97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationStateTestView.swift; sourceTree = ""; }; + A91E4DE52C6A1EAE00E0E5A7 /* ValidationPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationPredicateTests.swift; sourceTree = ""; }; A963ACAB2AF4683A00D745F2 /* SpeziValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeziValidationTests.swift; sourceTree = ""; }; A963ACB12AF4709400D745F2 /* XCUIApplication+Targets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+Targets.swift"; sourceTree = ""; }; A97880962A4C4E6500150B2F /* ModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelTests.swift; sourceTree = ""; }; @@ -188,6 +190,7 @@ children = ( A99A65112AF57CA200E63582 /* FocusedValidationTests.swift */, A963ACAB2AF4683A00D745F2 /* SpeziValidationTests.swift */, + A91E4DE52C6A1EAE00E0E5A7 /* ValidationPredicateTests.swift */, A9A3535A2AF60A9E00661848 /* DefaultValidationRules.swift */, ); path = ValidationTests; @@ -365,6 +368,7 @@ A9F85B722B32A052005F16E6 /* NameFieldsExample.swift in Sources */, 2FA9486729DE90720081C086 /* CanvasTestView.swift in Sources */, 97A0A5102B8D7FD7006102EF /* ConditionalModifierTestView.swift in Sources */, + A91E4DE62C6A1EAE00E0E5A7 /* ValidationPredicateTests.swift in Sources */, A963ACAC2AF4683A00D745F2 /* SpeziValidationTests.swift in Sources */, A9FBAE982AF446F3001E4AF1 /* SpeziPersonalInfoTests.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */,