diff --git a/Sources/SpeziFHIR/FHIRResource/FHIRResource+Category.swift b/Sources/SpeziFHIR/FHIRResource/FHIRResource+Category.swift index 2a7b0b8..62ea806 100644 --- a/Sources/SpeziFHIR/FHIRResource/FHIRResource+Category.swift +++ b/Sources/SpeziFHIR/FHIRResource/FHIRResource+Category.swift @@ -15,7 +15,7 @@ import enum ModelsDSTU2.ResourceProxy extension FHIRResource { /// Enum representing different categories of FHIR resources. /// This categorization helps in classifying FHIR resources into common healthcare scenarios and types. - enum FHIRResourceCategory { + enum FHIRResourceCategory: CaseIterable { /// Represents an observation-type resource (e.g., patient measurements, lab results). case observation /// Represents an encounter-type resource (e.g., patient visits, admissions). @@ -34,32 +34,27 @@ extension FHIRResource { case medication /// Represents other types of resources not covered by the above categories. case other - } - - var storeKeyPath: KeyPath { - switch self.category { - case .observation: - \.observations - case .encounter: - \.encounters - case .condition: - \.conditions - case .diagnostic: - \.diagnostics - case .procedure: - \.procedures - case .immunization: - \.immunizations - case .allergyIntolerance: - \.allergyIntolerances - case .medication: - \.medications - case .other: - \.otherResources + + + /// The ``FHIRStore`` property key path of the resource. + /// + /// - Note: Needs to be isolated on `MainActor` as the respective ``FHIRStore`` properties referred to by the `KeyPath` are isolated on the `MainActor`. + @MainActor var storeKeyPath: KeyPath { + switch self { + case .observation: \.observations + case .encounter: \.encounters + case .condition: \.conditions + case .diagnostic: \.diagnostics + case .procedure: \.procedures + case .immunization: \.immunizations + case .allergyIntolerance: \.allergyIntolerances + case .medication: \.medications + case .other: \.otherResources + } } } - + /// Category of the FHIR resource. /// /// Analyzes the type of the underlying resource and assigns it to an appropriate category. diff --git a/Sources/SpeziFHIR/FHIRResource/FHIRResource.swift b/Sources/SpeziFHIR/FHIRResource/FHIRResource.swift index e191fa9..ba5aa08 100644 --- a/Sources/SpeziFHIR/FHIRResource/FHIRResource.swift +++ b/Sources/SpeziFHIR/FHIRResource/FHIRResource.swift @@ -7,19 +7,19 @@ // import Foundation -@preconcurrency import ModelsDSTU2 -@preconcurrency import ModelsR4 +import ModelsDSTU2 +import ModelsR4 /// Represents a FHIR (Fast Healthcare Interoperability Resources) entity. /// /// Handles both DSTU2 and R4 versions, providing a unified interface to interact with different FHIR versions. -public struct FHIRResource: Sendable, Identifiable, Hashable { +public struct FHIRResource: Identifiable, Hashable { /// Version-specific FHIR resources. - public enum VersionedFHIRResource: Sendable, Hashable { + public enum VersionedFHIRResource: Hashable { /// R4 version of FHIR resources. case r4(ModelsR4.Resource) // swiftlint:disable:this identifier_name - // DSTU2 version of FHIR resources. + /// DSTU2 version of FHIR resources. case dstu2(ModelsDSTU2.Resource) } diff --git a/Sources/SpeziFHIR/FHIRStore.swift b/Sources/SpeziFHIR/FHIRStore.swift index 08abdf9..f09c205 100644 --- a/Sources/SpeziFHIR/FHIRStore.swift +++ b/Sources/SpeziFHIR/FHIRStore.swift @@ -6,8 +6,6 @@ // SPDX-License-Identifier: MIT // -import Combine -import Foundation import Observation import class ModelsR4.Bundle import enum ModelsDSTU2.ResourceProxy @@ -19,149 +17,155 @@ import Spezi /// The ``FHIRStore`` is automatically injected in the environment if you use the ``FHIR`` standard or can be used as a standalone module. @Observable public final class FHIRStore: Module, - EnvironmentAccessible, - DefaultInitializable, - @unchecked Sendable /* `unchecked` `Sendable` conformance fine as access to `_resources` protected by `NSLock` */ { - private let lock = NSLock() - @ObservationIgnored private var _resources: [FHIRResource] - - - /// Allergy intolerances. - public var allergyIntolerances: [FHIRResource] { + EnvironmentAccessible, + DefaultInitializable, + Sendable { + @MainActor private var _resources: [FHIRResource] = [] + + + /// `FHIRResource`s with category `allergyIntolerance`. + @MainActor public var allergyIntolerances: [FHIRResource] { access(keyPath: \.allergyIntolerances) - return lock.withLock { - _resources.filter { $0.category == .allergyIntolerance } - } + return _resources.filter { $0.category == .allergyIntolerance } } - - /// Conditions. - public var conditions: [FHIRResource] { + + /// `FHIRResource`s with category `condition`. + @MainActor public var conditions: [FHIRResource] { access(keyPath: \.conditions) - return lock.withLock { - _resources.filter { $0.category == .condition } - } + return _resources.filter { $0.category == .condition } } - - /// Diagnostics. - public var diagnostics: [FHIRResource] { + + /// `FHIRResource`s with category `diagnostic`. + @MainActor public var diagnostics: [FHIRResource] { access(keyPath: \.diagnostics) - return lock.withLock { - _resources.filter { $0.category == .diagnostic } - } + return _resources.filter { $0.category == .diagnostic } } - - /// Encounters. - public var encounters: [FHIRResource] { + + /// `FHIRResource`s with category `encounter`. + @MainActor public var encounters: [FHIRResource] { access(keyPath: \.encounters) return _resources.filter { $0.category == .encounter } } - - /// Immunizations. - public var immunizations: [FHIRResource] { + + /// `FHIRResource`s with category `immunization` + @MainActor public var immunizations: [FHIRResource] { access(keyPath: \.immunizations) - return lock.withLock { - _resources.filter { $0.category == .immunization } - } + return _resources.filter { $0.category == .immunization } } - - /// Medications. - public var medications: [FHIRResource] { + + /// `FHIRResource`s with category `medication`. + @MainActor public var medications: [FHIRResource] { access(keyPath: \.medications) - return lock.withLock { - _resources.filter { $0.category == .medication } - } + return _resources.filter { $0.category == .medication } } - - /// Observations. - public var observations: [FHIRResource] { + + /// `FHIRResource`s with category `observation`. + @MainActor public var observations: [FHIRResource] { access(keyPath: \.observations) - return lock.withLock { - _resources.filter { $0.category == .observation } - } - } - - /// Other resources that could not be classified on the other categories. - public var otherResources: [FHIRResource] { - access(keyPath: \.otherResources) - return lock.withLock { - _resources.filter { $0.category == .other } - } + return _resources.filter { $0.category == .observation } } - - /// Procedures. - public var procedures: [FHIRResource] { + + /// `FHIRResource`s with category `procedure`. + @MainActor public var procedures: [FHIRResource] { access(keyPath: \.procedures) - return lock.withLock { - _resources.filter { $0.category == .procedure } - } + return _resources.filter { $0.category == .procedure } } - - - public required init() { - self._resources = [] + + /// `FHIRResource`s with category `other`. + @MainActor public var otherResources: [FHIRResource] { + access(keyPath: \.otherResources) + return _resources.filter { $0.category == .other } } - - - /// Inserts a FHIR resource into the store. + + + /// Create an empty ``FHIRStore``. + public required init() {} + + + /// Inserts a FHIR resource into the ``FHIRStore``. /// /// - Parameter resource: The `FHIRResource` to be inserted. + @MainActor public func insert(resource: FHIRResource) { - withMutation(keyPath: resource.storeKeyPath) { - lock.withLock { - _resources.append(resource) - } - } + _$observationRegistrar.willSet(self, keyPath: resource.category.storeKeyPath) + + _resources.append(resource) + + _$observationRegistrar.didSet(self, keyPath: resource.category.storeKeyPath) } - - /// Removes a FHIR resource from the store. + + /// Inserts a ``Collection`` of FHIR resources into the ``FHIRStore``. /// - /// - Parameter resource: The `FHIRResource` identifier to be inserted. - public func remove(resource resourceId: FHIRResource.ID) { - lock.withLock { - guard let resource = _resources.first(where: { $0.id == resourceId }) else { - return - } - - withMutation(keyPath: resource.storeKeyPath) { - _resources.removeAll(where: { $0.id == resourceId }) - } + /// - Parameter resources: The `FHIRResource`s to be inserted. + @MainActor + public func insert(resources: T) where T.Element == FHIRResource { + let resourceCategories = Set(resources.map(\.category)) + + for category in resourceCategories { + _$observationRegistrar.willSet(self, keyPath: category.storeKeyPath) + } + + self._resources.append(contentsOf: resources) + + for category in resourceCategories { + _$observationRegistrar.didSet(self, keyPath: category.storeKeyPath) } } - - /// Loads resources from a given FHIR `Bundle`. + + /// Loads resources from a given FHIR `Bundle` into the ``FHIRStore``. /// /// - Parameter bundle: The FHIR `Bundle` containing resources to be loaded. - public func load(bundle: Bundle) { + public func load(bundle: sending Bundle) async { let resourceProxies = bundle.entry?.compactMap { $0.resource } ?? [] - + var resources: [FHIRResource] = [] + for resourceProxy in resourceProxies { - insert(resource: FHIRResource(resource: resourceProxy.get(), displayName: resourceProxy.displayName)) + if Task.isCancelled { + return + } + + resources.append( + FHIRResource( + resource: resourceProxy.get(), + displayName: resourceProxy.displayName + ) + ) + } + + if Task.isCancelled { + return } + + await insert(resources: resources) } - - /// Removes all resources from the store. + + /// Removes a FHIR resource from the ``FHIRStore``. + /// + /// - Parameter resource: The `FHIRResource` identifier to be inserted. + @MainActor + public func remove(resource resourceId: FHIRResource.ID) { + guard let resource = _resources.first(where: { $0.id == resourceId }) else { + return + } + + _$observationRegistrar.willSet(self, keyPath: resource.category.storeKeyPath) + + _resources.removeAll { $0.id == resourceId } + + _$observationRegistrar.didSet(self, keyPath: resource.category.storeKeyPath) + } + + /// Removes all resources from the ``FHIRStore``. + @MainActor public func removeAllResources() { - lock.withLock { - // Not really ideal but seems to be a path to ensure that all observables are called. - _$observationRegistrar.willSet(self, keyPath: \.allergyIntolerances) - _$observationRegistrar.willSet(self, keyPath: \.conditions) - _$observationRegistrar.willSet(self, keyPath: \.diagnostics) - _$observationRegistrar.willSet(self, keyPath: \.encounters) - _$observationRegistrar.willSet(self, keyPath: \.immunizations) - _$observationRegistrar.willSet(self, keyPath: \.medications) - _$observationRegistrar.willSet(self, keyPath: \.observations) - _$observationRegistrar.willSet(self, keyPath: \.otherResources) - _$observationRegistrar.willSet(self, keyPath: \.procedures) - _resources = [] - _$observationRegistrar.didSet(self, keyPath: \.allergyIntolerances) - _$observationRegistrar.didSet(self, keyPath: \.conditions) - _$observationRegistrar.didSet(self, keyPath: \.diagnostics) - _$observationRegistrar.didSet(self, keyPath: \.encounters) - _$observationRegistrar.didSet(self, keyPath: \.immunizations) - _$observationRegistrar.didSet(self, keyPath: \.medications) - _$observationRegistrar.didSet(self, keyPath: \.observations) - _$observationRegistrar.didSet(self, keyPath: \.otherResources) - _$observationRegistrar.didSet(self, keyPath: \.procedures) + for category in FHIRResource.FHIRResourceCategory.allCases { + _$observationRegistrar.willSet(self, keyPath: category.storeKeyPath) + } + + _resources = [] + + for category in FHIRResource.FHIRResourceCategory.allCases { + _$observationRegistrar.didSet(self, keyPath: category.storeKeyPath) } } } diff --git a/Sources/SpeziFHIRHealthKit/FHIRStore+HealthKit.swift b/Sources/SpeziFHIRHealthKit/FHIRStore+HealthKit.swift index d599058..b44ca28 100644 --- a/Sources/SpeziFHIRHealthKit/FHIRStore+HealthKit.swift +++ b/Sources/SpeziFHIRHealthKit/FHIRStore+HealthKit.swift @@ -29,7 +29,7 @@ extension FHIRStore { public func add(sample: HKSample) async { do { let resource = try await transform(sample: sample) - insert(resource: resource) + await insert(resource: resource) } catch { print("Could not transform HKSample: \(error)") } @@ -38,7 +38,7 @@ extension FHIRStore { /// Remove a HealthKit sample delete object from the FHIR store. /// - Parameter sample: The sample delete object that should be removed. public func remove(sample: HKDeletedObject) async { - remove(resource: sample.uuid.uuidString) + await remove(resource: sample.uuid.uuidString) } diff --git a/Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift b/Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift index 8181238..35ceb4e 100644 --- a/Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift +++ b/Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift @@ -20,53 +20,63 @@ public struct FHIRBundleSelector: View { let id: String let bundle: ModelsR4.Bundle } - - - private let bundles: [PatientIdentifiedBundle] - + + @Environment(FHIRStore.self) private var store + @State private var selectedBundleId: PatientIdentifiedBundle.ID? + @State private var fhirResourceLoadingTask: Task? + + private let bundles: [PatientIdentifiedBundle] + - private var selectedBundle: Binding { - Binding( - get: { - guard let patient = store.otherResources.compactMap({ resource -> ModelsR4.Patient? in - guard case let .r4(resource) = resource.versionedResource, let loadedPatient = resource as? Patient else { - return nil - } - return loadedPatient - }).first else { - return nil - } - - - guard let bundle = bundles.first(where: { patient.identifier == $0.bundle.patient?.identifier }) else { - return nil - } - - return bundle.id - }, - set: { newValue in - guard let newValue, let bundle = bundles.first(where: { $0.id == newValue })?.bundle else { - return - } - - store.removeAllResources() - store.load(bundle: bundle) - } - ) - } - - public var body: some View { Picker( String(localized: "Select Mock Patient", bundle: .module), - selection: selectedBundle + selection: $selectedBundleId ) { ForEach(bundles) { bundle in Text(bundle.bundle.patientName) .tag(bundle.id as String?) } } + // Load the currently stored patient data to update `selectedBundleID` + .task { + let existingResources = store.otherResources + + guard + let patient = existingResources.compactMap({ resource -> Patient? in + guard case let .r4(r4Resource) = resource.versionedResource, + let loadedPatient = r4Resource as? Patient else { + return nil + } + + return loadedPatient + }).first, + let matchedBundle = bundles.first(where: { + patient.identifier == $0.bundle.patient?.identifier + }) + else { + return + } + + selectedBundleId = matchedBundle.id + } + // Remove existing resources and load the newly selected bundle + .onChange(of: selectedBundleId) { _, newValue in + fhirResourceLoadingTask?.cancel() + fhirResourceLoadingTask = nil + + guard let newValue, + let selected = bundles.first(where: { $0.id == newValue }) else { + return + } + + store.removeAllResources() + + fhirResourceLoadingTask = Task { + await store.load(bundle: selected.bundle) + } + } } diff --git a/Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift b/Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift index 5c2f7f4..bb2f57d 100644 --- a/Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift +++ b/Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -@preconcurrency import ModelsR4 +import ModelsR4 import SwiftUI @@ -31,7 +31,12 @@ public struct FHIRMockPatientSelection: View { } } .task { - self.bundles = await ModelsR4.Bundle.mockPatients + _Concurrency.Task.detached { // Workaround but enables us to not mark `import ModelsR4` as `@preconcurrency` + let bundles = await ModelsR4.Bundle.mockPatients + await MainActor.run { + self.bundles = bundles + } + } } } diff --git a/Sources/SpeziFHIRMockPatients/FHIRStore+TestingSupport.swift b/Sources/SpeziFHIRMockPatients/FHIRStore+TestingSupport.swift index 1458133..b7916de 100644 --- a/Sources/SpeziFHIRMockPatients/FHIRStore+TestingSupport.swift +++ b/Sources/SpeziFHIRMockPatients/FHIRStore+TestingSupport.swift @@ -12,7 +12,7 @@ import SpeziFHIR extension FHIRStore { /// Loads a mock resource into the `FHIRStore` for testing purposes. - public func loadTestingResources() { + public func loadTestingResources() async { let mockObservation = Observation( code: CodeableConcept(coding: [Coding(code: "1234".asFHIRStringPrimitive())]), id: FHIRPrimitive("1234"), @@ -25,7 +25,7 @@ extension FHIRStore { displayName: "Mock Resource" ) - removeAllResources() - insert(resource: mockFHIRResource) + await removeAllResources() + await insert(resource: mockFHIRResource) } } diff --git a/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift b/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift index 0df93ef..0516c81 100644 --- a/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift +++ b/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift @@ -7,7 +7,7 @@ // import Foundation -@preconcurrency import class ModelsR4.Bundle +import class ModelsR4.Bundle extension Foundation.Bundle { @@ -18,14 +18,12 @@ extension Foundation.Bundle { guard let resourceURL = Self.module.url(forResource: name, withExtension: "json") else { fatalError("Could not find the resource \"\(name)\".json in the SpeziFHIRMockPatients Resources folder.") } - - let loadingTask = Task { - let resourceData = try Data(contentsOf: resourceURL) - return try JSONDecoder().decode(Bundle.self, from: resourceData) - } - + do { - return try await loadingTask.value + return try JSONDecoder().decode( + Bundle.self, + from: Data(contentsOf: resourceURL) + ) } catch { fatalError("Could not decode the FHIR bundle named \"\(name).json\": \(error)") } diff --git a/Tests/UITests/TestApp/ContentView.swift b/Tests/UITests/TestApp/ContentView.swift index 09895cb..24e2ba8 100644 --- a/Tests/UITests/TestApp/ContentView.swift +++ b/Tests/UITests/TestApp/ContentView.swift @@ -6,17 +6,20 @@ // SPDX-License-Identifier: MIT // +import ModelsR4 import SpeziFHIR import SwiftUI struct ContentView: View { - @Environment(FHIRStore.self) var fhirStore - @State var presentPatientSelection = false - + @Environment(FHIRStore.self) private var fhirStore + @State private var presentPatientSelection = false + + private let additionalFHIRResourceId = "SuperUniqueFHIRResourceIdentifier" + var body: some View { - NavigationStack { + NavigationStack { // swiftlint:disable:this closure_body_length List { Section { Text("Allergy Intolerances: \(fhirStore.allergyIntolerances.count)") @@ -26,8 +29,8 @@ struct ContentView: View { Text("Immunizations: \(fhirStore.immunizations.count)") Text("Medications: \(fhirStore.medications.count)") Text("Observations: \(fhirStore.observations.count)") - Text("Other Resources: \(fhirStore.otherResources.count)") Text("Procedures: \(fhirStore.procedures.count)") + Text("Other Resources: \(fhirStore.otherResources.count)") } Section { presentPatientSelectionButton @@ -36,6 +39,30 @@ struct ContentView: View { .sheet(isPresented: $presentPatientSelection) { MockPatientSelection(presentPatientSelection: $presentPatientSelection) } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + fhirStore.insert( + resource: .init( + resource: ModelsR4.Account(id: .init(stringLiteral: additionalFHIRResourceId), status: .init()), + displayName: "Random Account FHIR Resource" + ) + ) + } label: { + Label("Add", systemImage: "doc.badge.plus") + .accessibilityLabel("Add FHIR Resource") + } + } + + ToolbarItem { + Button { + fhirStore.remove(resource: additionalFHIRResourceId) + } label: { + Label("Remove", systemImage: "folder.badge.minus") + .accessibilityLabel("Remove FHIR Resource") + } + } + } } } diff --git a/Tests/UITests/TestAppUITests/FHIRMockDataStorageProviderTests.swift b/Tests/UITests/TestAppUITests/FHIRMockDataStorageProviderTests.swift index 6a0eb23..2dea837 100644 --- a/Tests/UITests/TestAppUITests/FHIRMockDataStorageProviderTests.swift +++ b/Tests/UITests/TestAppUITests/FHIRMockDataStorageProviderTests.swift @@ -10,7 +10,7 @@ import XCTest final class SpeziFHIRTests: XCTestCase { - func testSpeziFHIRMockPatients() throws { + func testMockPatientResources() throws { let app = XCUIApplication() app.launch() @@ -21,11 +21,12 @@ final class SpeziFHIRTests: XCTestCase { XCTAssert(app.staticTexts["Immunizations: 0"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Medications: 0"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Observations: 0"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["Other Resources: 0"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Procedures: 0"].waitForExistence(timeout: 2)) - + XCTAssert(app.staticTexts["Other Resources: 0"].waitForExistence(timeout: 2)) + + XCTAssert(app.buttons["Select Mock Patient"].waitForExistence(timeout: 2)) app.buttons["Select Mock Patient"].tap() - + XCTAssert(app.buttons["Jamison785 Denesik803"].waitForExistence(timeout: 20)) app.buttons["Jamison785 Denesik803"].tap() @@ -38,11 +39,12 @@ final class SpeziFHIRTests: XCTestCase { XCTAssert(app.staticTexts["Immunizations: 12"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Medications: 31"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Observations: 769"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["Other Resources: 302"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Procedures: 106"].waitForExistence(timeout: 2)) - + XCTAssert(app.staticTexts["Other Resources: 302"].waitForExistence(timeout: 2)) + + XCTAssert(app.buttons["Select Mock Patient"].waitForExistence(timeout: 2)) app.buttons["Select Mock Patient"].tap() - + XCTAssert(app.buttons["Maye976 Dickinson688"].waitForExistence(timeout: 20)) app.buttons["Maye976 Dickinson688"].tap() @@ -55,7 +57,61 @@ final class SpeziFHIRTests: XCTestCase { XCTAssert(app.staticTexts["Immunizations: 11"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Medications: 55"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Observations: 169"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["Other Resources: 322"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["Procedures: 225"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["Other Resources: 322"].waitForExistence(timeout: 2)) + } + + func testAddingAndRemovingResources() throws { + let app = XCUIApplication() + app.launch() + + // Add 5 resources + for resourceCount in 0..<5 { + XCTAssert(app.staticTexts["Other Resources: \(resourceCount)"].waitForExistence(timeout: 2)) + + XCTAssert(app.buttons["Add FHIR Resource"].waitForExistence(timeout: 2)) + app.buttons["Add FHIR Resource"].tap() + } + + // Remove added resources + XCTAssert(app.buttons["Remove FHIR Resource"].waitForExistence(timeout: 2)) + app.buttons["Remove FHIR Resource"].tap() + + XCTAssert(app.staticTexts["Other Resources: 0"].waitForExistence(timeout: 2)) + + // Try removing a second time + XCTAssert(app.buttons["Remove FHIR Resource"].waitForExistence(timeout: 2)) + app.buttons["Remove FHIR Resource"].tap() + + XCTAssert(app.staticTexts["Other Resources: 0"].waitForExistence(timeout: 2)) + + // Select mock patient + XCTAssert(app.buttons["Select Mock Patient"].waitForExistence(timeout: 2)) + app.buttons["Select Mock Patient"].tap() + + XCTAssert(app.buttons["Jamison785 Denesik803"].waitForExistence(timeout: 20)) + app.buttons["Jamison785 Denesik803"].tap() + + app.navigationBars["Select Mock Patient"].buttons["Dismiss"].tap() + + XCTAssert(app.staticTexts["Other Resources: 302"].waitForExistence(timeout: 2)) + + // Add resource to mock patient + XCTAssert(app.buttons["Add FHIR Resource"].waitForExistence(timeout: 2)) + app.buttons["Add FHIR Resource"].tap() + + XCTAssert(app.staticTexts["Other Resources: 303"].waitForExistence(timeout: 2)) + + // Remove resource from mock patient + XCTAssert(app.buttons["Remove FHIR Resource"].waitForExistence(timeout: 2)) + app.buttons["Remove FHIR Resource"].tap() + + XCTAssert(app.staticTexts["Other Resources: 302"].waitForExistence(timeout: 2)) + + // Try removing a second time + XCTAssert(app.buttons["Remove FHIR Resource"].waitForExistence(timeout: 2)) + app.buttons["Remove FHIR Resource"].tap() + + XCTAssert(app.staticTexts["Other Resources: 302"].waitForExistence(timeout: 2)) } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 0a5207b..031a5d6 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -164,7 +164,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1620; TargetAttributes = { 2F6D139128F5F384007C25D6 = { CreatedOnToolsVersion = 14.1; @@ -412,7 +412,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -445,7 +445,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -453,7 +453,6 @@ 2F6D13BD28F5F386007C25D6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; @@ -473,7 +472,6 @@ 2F6D13BE28F5F386007C25D6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index ba86621..322d325 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -1,6 +1,6 @@