Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Testing #75

Merged
merged 10 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Intake.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
2FF53D8B2A8725DE00042B76 /* SpeziMockWebService in Frameworks */ = {isa = PBXBuildFile; productRef = 2FF53D8A2A8725DE00042B76 /* SpeziMockWebService */; };
2FF53D8D2A8729D600042B76 /* IntakeStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF53D8C2A8729D600042B76 /* IntakeStandard.swift */; };
3C89F66D2B9D948B00A4F52D /* PatientInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C89F66C2B9D948B00A4F52D /* PatientInfo.swift */; };
510CAAF12BA0DFFB00872B1A /* MedicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510CAAF02BA0DFFB00872B1A /* MedicationTests.swift */; };
511827962B740192002033A0 /* SurgeryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511827952B740191002033A0 /* SurgeryView.swift */; };
51805C122B81853800D17109 /* IntakeMedication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C112B81853700D17109 /* IntakeMedication.swift */; };
51805C152B81857100D17109 /* IntakeMedicationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C142B81857100D17109 /* IntakeMedicationViewModel.swift */; };
Expand Down Expand Up @@ -180,6 +181,7 @@
2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = "<group>"; };
2FF53D8C2A8729D600042B76 /* IntakeStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntakeStandard.swift; sourceTree = "<group>"; };
3C89F66C2B9D948B00A4F52D /* PatientInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientInfo.swift; sourceTree = "<group>"; };
510CAAF02BA0DFFB00872B1A /* MedicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedicationTests.swift; sourceTree = "<group>"; };
511827952B740191002033A0 /* SurgeryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurgeryView.swift; sourceTree = "<group>"; };
51805C112B81853700D17109 /* IntakeMedication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeMedication.swift; sourceTree = "<group>"; };
51805C142B81857100D17109 /* IntakeMedicationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeMedicationViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -560,6 +562,7 @@
isa = PBXGroup;
children = (
2F4E237D2989A2FE0013F3D9 /* LaunchTests.swift */,
510CAAF02BA0DFFB00872B1A /* MedicationTests.swift */,
);
path = IntakeUITests;
sourceTree = "<group>";
Expand Down Expand Up @@ -928,6 +931,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
510CAAF12BA0DFFB00872B1A /* MedicationTests.swift in Sources */,
2F4E237E2989A2FE0013F3D9 /* LaunchTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
12 changes: 12 additions & 0 deletions Intake.xcodeproj/xcshareddata/xcschemes/Intake.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@
argument = "--disableFirebase"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--skipToScrollable"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--testPatient"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--showOnboarding"
isEnabled = "NO">
Expand All @@ -93,6 +101,10 @@
argument = "--testSchedule"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--testMedication"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--useFirebaseEmulator"
isEnabled = "NO">
Expand Down
1 change: 1 addition & 0 deletions Intake/Allergy Records/AllergyLLMAssistant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ struct UpdateAllergyFunction: LLMFunction {
}
}

// The AllergyLLMAssistant allows the user to ask the chat questions about their current allergies and add new allergies to their data.
struct AllergyLLMAssistant: View {
@Environment(DataStore.self) private var data
@Environment(NavigationPathWrapper.self) private var navigationPath
Expand Down
6 changes: 5 additions & 1 deletion Intake/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@

var body: some View {
Button(action: {
navigationPath.append(NavigationViews.general)
if FeatureFlags.testMedication {
navigationPath.append(NavigationViews.medication)
} else {
navigationPath.append(NavigationViews.general)

Check warning on line 38 in Intake/Home.swift

View check run for this annotation

Codecov / codecov/patch

Intake/Home.swift#L38

Added line #L38 was not covered by tests
}
}) {
Text("Create New Form")
.font(.headline)
Expand Down
10 changes: 9 additions & 1 deletion Intake/IntakeTestingSetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
//
// SPDX-License-Identifier: MIT
//

import ModelsR4
import SpeziFHIR
import SpeziFHIRMockPatients
import SwiftUI

private struct IntakeAppTestingSetup: ViewModifier {
@AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false
@Environment(FHIRStore.self) private var store

func body(content: Content) -> some View {
content
Expand All @@ -20,6 +23,11 @@ private struct IntakeAppTestingSetup: ViewModifier {
if FeatureFlags.showOnboarding {
completedOnboardingFlow = false
}
if FeatureFlags.testPatient {
let bundle = await ModelsR4.Bundle.gonzalo160Duenas839
store.removeAllResources()
store.load(bundle: bundle)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Intake/Medication View/IntakeDosage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import Foundation
import SpeziMedication

// The IntakeDosage struct has a localizedDescription that describes the dosage information.
struct IntakeDosage: Dosage, Codable {
var localizedDescription: String
}
1 change: 1 addition & 0 deletions Intake/Medication View/IntakeMedication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import Foundation
import SpeziMedication

// This describes the IntakeMedication struct which contains a localizedDescription (medication name) and a list of dosages.
struct IntakeMedication: Medication, Comparable, Codable {
var localizedDescription: String
var dosages: [IntakeDosage]
Expand Down
1 change: 1 addition & 0 deletions Intake/Medication View/IntakeMedicationInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import Foundation
import SpeziMedication

// This defines an IntakeMedicationInstance which is composed of an id, an IntakeMedication type, a dosage, and a schedule.
struct IntakeMedicationInstance: MedicationInstance, MedicationInstanceInitializable, Codable {
let id: UUID
let type: IntakeMedication
Expand Down
12 changes: 10 additions & 2 deletions Intake/Medication View/IntakeMedicationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SpeziFHIR
import SpeziMedication
import SwiftUI

// The IntakeMedicationSettingsViewModel takes the patient's FHIRStore medications and adds any that match to the medicationOptions to the medicationInstances list which is then used for the MedicationContentView.
@Observable
class IntakeMedicationSettingsViewModel: Module, MedicationSettingsViewModel, CustomStringConvertible {
var medicationInstances: Set<IntakeMedicationInstance> = []
Expand Down Expand Up @@ -46,7 +47,9 @@ class IntakeMedicationSettingsViewModel: Module, MedicationSettingsViewModel, Cu
.joined(separator: ", ")
}

// The init is modified from the SpeziMedication examples to load in the existing patient medications from their FHIRStore data.
init(existingMedications: [FHIRResource]) { // swiftlint:disable:this function_body_length
// medicationOptions provides the list of medications options chosen as the most common medications from the sample patients
self.medicationOptions = [
IntakeMedication(
localizedDescription: "Hydrochlorothiazide 25 MG Oral Tablet",
Expand Down Expand Up @@ -99,17 +102,20 @@ class IntakeMedicationSettingsViewModel: Module, MedicationSettingsViewModel, Cu
]

var foundMedications: [IntakeMedicationInstance] = []
// This function matches any patient medication from FHIRStore to a medication in medicationOptions.
if !existingMedications.isEmpty {
for medication in existingMedications {
for option in medicationOptions where option.localizedDescription == medication.displayName {
var medSchedule: SpeziMedication.Schedule
let medRequest = medicationRequest(resource: medication)
if case .boolean(let asNeeded) = medRequest?.dosageInstruction?.first?.asNeeded {
// Checks if medication is asNeeded, otherwise finds the frequency in days.
if let asNeededbool = asNeeded.value?.bool {
if asNeededbool {
medSchedule = SpeziMedication.Schedule(frequency: .asNeeded)
} else {
let intValue: Int
// Need to convert from FHIRDecimal to int.
let interval = medRequest?.dosageInstruction?.first?.timing?.repeat?.period?.value?.decimal
if let interval = interval {
intValue = interval.int
Expand All @@ -122,7 +128,7 @@ class IntakeMedicationSettingsViewModel: Module, MedicationSettingsViewModel, Cu
guard let firstDosage = option.dosages.first else {
continue
}

// Create an IntakeMedicationInstance to the data.
let intakeMedicationInstance = IntakeMedicationInstance(
type: option,
dosage: firstDosage,
Expand All @@ -139,7 +145,8 @@ class IntakeMedicationSettingsViewModel: Module, MedicationSettingsViewModel, Cu
func persist(medicationInstances: Set<IntakeMedicationInstance>) async throws {
self.medicationInstances = medicationInstances
}


// Converts a FHIRResource into a MedicationRequest.
func medicationRequest(resource: FHIRResource) -> MedicationRequest? {
guard case let .r4(resource) = resource.versionedResource,
let medicationRequest = resource as? ModelsR4.MedicationRequest else {
Expand All @@ -149,6 +156,7 @@ class IntakeMedicationSettingsViewModel: Module, MedicationSettingsViewModel, Cu
}
}

// Needed to convert the FHIRDecimal into an Int.
extension Decimal {
var int: Int {
let intVal = NSDecimalNumber(decimal: self).intValue // swiftlint:disable:this legacy_objc_type
Expand Down
43 changes: 27 additions & 16 deletions Intake/Medication View/MedicationContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,49 @@
import SpeziMedication
import SwiftUI

// This view displays the medications in the patient's FHIR data, and allows them to add, update and delete their medications.
struct MedicationContentView: View {
@Environment(FHIRStore.self) private var fhirStore
@Environment(NavigationPathWrapper.self) private var navigationPath
@Environment(DataStore.self) private var data
@State private var presentSettings = false

@State private var medicationSettingsViewModel: IntakeMedicationSettingsViewModel?

var body: some View {
VStack {
if let medicationSettingsViewModel {
MedicationSettings(allowEmptySave: true, medicationSettingsViewModel: medicationSettingsViewModel) {
data.medicationData = medicationSettingsViewModel.medicationInstances
navigationPath.path.append(NavigationViews.allergies)
if FeatureFlags.skipToScrollable {
data.medicationData = medicationSettingsViewModel.medicationInstances
navigationPath.path.append(NavigationViews.pdfs)
} else {
data.medicationData = medicationSettingsViewModel.medicationInstances
navigationPath.path.append(NavigationViews.allergies)

Check warning on line 38 in Intake/Medication View/MedicationContentView.swift

View check run for this annotation

Codecov / codecov/patch

Intake/Medication View/MedicationContentView.swift#L37-L38

Added lines #L37 - L38 were not covered by tests
}
}
.navigationTitle("Medications")
.navigationBarItems(trailing: NavigationLink(destination: MedicationLLMAssistant(presentingAccount: .constant(false))) {
Text("Chat")
})
.navigationTitle("Medications")
.navigationBarItems(trailing: NavigationLink(destination: MedicationLLMAssistant(presentingAccount: .constant(false))) {
Text("Chat")
})
} else {
ProgressView()
}
}
.task {
let patientMedications = fhirStore.llmMedications
self.medicationSettingsViewModel = IntakeMedicationSettingsViewModel(existingMedications: patientMedications)
var initialData: Set<IntakeMedicationInstance> = []
if let newMed = self.medicationSettingsViewModel?.medicationInstances {
initialData = newMed
}
data.medicationData = initialData
// Updates the medicationSettingsViewModel init if there's a change to the patient's fhirStore medications.
.onChange(of: fhirStore.llmMedications) {
medicationSettingsViewModel = .init(existingMedications: fhirStore.llmMedications)
}
// Task to initialize the MedicationSettingsViewModel with the patient's existing fhirStore medications.
.task {
let patientMedications = fhirStore.llmMedications
self.medicationSettingsViewModel = IntakeMedicationSettingsViewModel(existingMedications: patientMedications)
var initialData: Set<IntakeMedicationInstance> = []
if let newMed = self.medicationSettingsViewModel?.medicationInstances {
initialData = newMed
}
data.medicationData = initialData
}
}

init() {}
Expand Down
2 changes: 2 additions & 0 deletions Intake/Medication View/MedicationLLMAssistant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import SpeziLLMLocal
import SpeziLLMOpenAI
import SwiftUI

// Adds the current patient medications to the system prompt.
func getCurrentPatientMedications(medicationList: Set<IntakeMedicationInstance>) -> String? {
var medicationDetails = "The patient is currently taking several medications:"
print(medicationList)
Expand All @@ -31,6 +32,7 @@ func getCurrentPatientMedications(medicationList: Set<IntakeMedicationInstance>)
return medicationDetails.isEmpty ? nil : medicationDetails
}

// Provides medication LLM assistant functionality to allow the patient to ask about their current medications.
struct MedicationLLMAssistant: View {
@Environment(DataStore.self) private var data
@Environment(NavigationPathWrapper.self) private var navigationPath
Expand Down
3 changes: 3 additions & 0 deletions Intake/SharedContext/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ enum FeatureFlags {
#endif
/// Adds a test task to the schedule at the current time
static let testSchedule = CommandLine.arguments.contains("--testSchedule")
static let testPatient = CommandLine.arguments.contains("--testPatient")
static let testMedication = CommandLine.arguments.contains("--testMedication")
static let skipToScrollable = CommandLine.arguments.contains("--skipToScrollable")
}
54 changes: 54 additions & 0 deletions IntakeUITests/MedicationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// MedicationTests.swift
// IntakeUITests
//
// Created by Kate Callon on 3/12/24.
//
//
// This source file is part of the Intake based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import Foundation
import XCTest

class MedicationTests: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()

continueAfterFailure = false

let app = XCUIApplication()
app.launchArguments = ["--skipOnboarding", "--disableFirebase", "--testPatient", "--testMedication", "--skipToScrollable"]
app.launch()
}

func testMedications() throws {
let app = XCUIApplication()

// Small workaround to wait until the madications loaded into main memory
sleep(8)

XCTAssertEqual(app.state, .runningForeground)
app.buttons["Create New Form"].tap()

XCTAssertTrue(app.staticTexts["Hydrochlorothiazide 25 MG Oral Tablet"].waitForExistence(timeout: 10))
XCTAssertTrue(app.staticTexts["amLODIPine 2.5 MG Oral Tablet"].waitForExistence(timeout: 10))
XCTAssertTrue(app.navigationBars["Medication Settings"].buttons["Add New Medication"].waitForExistence(timeout: 2))
XCTAssertTrue(app.navigationBars["Medication Settings"].buttons["Chat"].waitForExistence(timeout: 2))
app.navigationBars["Medication Settings"].buttons["Add New Medication"].tap()
app.buttons["Verapamil Hydrochloride 40 MG"].tap()
app.buttons["Save Dosage"].tap()
app.buttons["Add Medication"].tap()
XCTAssertTrue(app.staticTexts["Verapamil Hydrochloride 40 MG"].waitForExistence(timeout: 5))
app.buttons["Save Medications"].tap()
XCTAssertTrue(app.navigationBars["Patient Form"].waitForExistence(timeout: 2))
// XCTAssertTrue(app.staticTexts["Hydrochlorothiazide 25 MG Oral Tablet"].waitForExistence(timeout: 5))
// XCTAssertTrue(app.staticTexts["amLODIPine 2.5 MG Oral Tablet"].waitForExistence(timeout: 5))
// XCTAssertTrue(app.staticTexts["Verapamil Hydrochloride 40 MG"].waitForExistence(timeout: 5))
// XCTAssertTrue(app.staticTexts["2.5 MG - Every Day"].waitForExistence(timeout: 5))
}
}
Loading