From e58a3a848d18eeb28f49324570427c117dc60014 Mon Sep 17 00:00:00 2001 From: YurenSUN Date: Sun, 10 Mar 2024 17:46:36 -0700 Subject: [PATCH] Assessments tests and minor fixes (#56) # Assessments tests and minor fixes ## :recycle: Current situation & Problem Currently, we only implement the assessments tab for #44 but we did not write any tests to test it. Therefore, this PR aims to add the UI and uni tests for the assessments tab, including each of the tasks and the visualization of results. Currently, we only test creating the task for trail making and reaction time task as described below, but we reach the desired test coverage and 539 out of 589 lines (~91.5%) in the assessment folder are covered. This PR also includes some small fixes described below. ## :gear: Release Notes - tests the assessment tasks. Currently, we run through the Stroop task during the UI task and only test creating tasks for (1) trail-making task as we are unable to click the buttons to make trails as they are not shown in the app but grouped into an Other element `Tappable Buttons` and (2) reaction time as currently there is no supports for the shake motion in testing. This should be acceptable as the process of the two tasks is also solely handled by the ResaerchKit, and we only wrote functions to create and parse the results. - Add the additional unit task to the function parsing the trail-making task to ensure that no error is returned - We are missing tests for parsing the reaction time result as the simulator does not support result files, which is required when parsing the reaction time result. Other fixes - Fix some colors for the dark mode for HealthKit visualizations. - Fix the issue that the running task occurs on both the main screen and the sheet. Change the toggle showingTestSheet to directly setting to true to avoid the sheet not re-rendering (always showing trail-making task) issue. - Do not show chart legend for score/error count if those data are not collected. image ## :books: Documentation Related commends are added to the codes. ## :white_check_mark: Testing The PR adds tests to our codes. ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md). --- PICS.xcodeproj/project.pbxproj | 8 +- PICS/Assessment/Assessments.swift | 59 ++++----- PICS/Assessment/ReactionTime.swift | 5 +- PICS/Assessment/ResultsViz.swift | 14 +- PICS/Assessment/StroopTest.swift | 4 +- PICS/Assessment/TrailMakingTest.swift | 53 ++++---- .../HKVisualizationItem.swift | 7 +- PICSTests/AssessmentsUnitTests.swift | 40 ++++++ PICSUITests/AssessmentsTests.swift | 122 +++++++++++++++++- 9 files changed, 235 insertions(+), 77 deletions(-) create mode 100644 PICSTests/AssessmentsUnitTests.swift diff --git a/PICS.xcodeproj/project.pbxproj b/PICS.xcodeproj/project.pbxproj index 134d2df..59c4d3f 100644 --- a/PICS.xcodeproj/project.pbxproj +++ b/PICS.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ 97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */; }; A403A52E2B705A8C003CFA5C /* HealthVisualizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A403A52D2B705A8C003CFA5C /* HealthVisualizationTests.swift */; }; A40559A42B98204C00221783 /* HKVizUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40559A32B98204C00221783 /* HKVizUnitTests.swift */; }; + A45546C22B9E56A1006DB4B4 /* AssessmentsUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45546C12B9E56A1006DB4B4 /* AssessmentsUnitTests.swift */; }; A45993292B90544300A98C95 /* Assessments.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45993282B90544300A98C95 /* Assessments.swift */; }; A459932C2B906C3B00A98C95 /* ResultsViz.swift in Sources */ = {isa = PBXBuildFile; fileRef = A459932B2B906C3B00A98C95 /* ResultsViz.swift */; }; A480C7C02B6D5A3700B29A07 /* HKVisualization.swift in Sources */ = {isa = PBXBuildFile; fileRef = A480C7BF2B6D5A3700B29A07 /* HKVisualization.swift */; }; @@ -205,6 +206,7 @@ 86F62AF52B916B670075F23C /* AppointmentInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppointmentInformation.swift; sourceTree = ""; }; A403A52D2B705A8C003CFA5C /* HealthVisualizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthVisualizationTests.swift; sourceTree = ""; }; A40559A32B98204C00221783 /* HKVizUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKVizUnitTests.swift; sourceTree = ""; }; + A45546C12B9E56A1006DB4B4 /* AssessmentsUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssessmentsUnitTests.swift; sourceTree = ""; }; A45993282B90544300A98C95 /* Assessments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assessments.swift; sourceTree = ""; }; A459932B2B906C3B00A98C95 /* ResultsViz.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsViz.swift; sourceTree = ""; }; A480C7BF2B6D5A3700B29A07 /* HKVisualization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKVisualization.swift; sourceTree = ""; }; @@ -438,6 +440,7 @@ children = ( 653A256128338800005D4D48 /* PICSTests.swift */, A40559A32B98204C00221783 /* HKVizUnitTests.swift */, + A45546C12B9E56A1006DB4B4 /* AssessmentsUnitTests.swift */, ); path = PICSTests; sourceTree = ""; @@ -786,6 +789,7 @@ buildActionMask = 2147483647; files = ( A40559A42B98204C00221783 /* HKVizUnitTests.swift in Sources */, + A45546C22B9E56A1006DB4B4 /* AssessmentsUnitTests.swift in Sources */, 653A256228338800005D4D48 /* PICSTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -894,7 +898,7 @@ CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 2CJ9STHUFD; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "PICS/Supporting Files/Info.plist"; @@ -1094,7 +1098,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = "PICS/Supporting Files/PICSDebug.entitlements"; + CODE_SIGN_ENTITLEMENTS = "PICS/Supporting Files/PICS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; diff --git a/PICS/Assessment/Assessments.swift b/PICS/Assessment/Assessments.swift index a971928..0116448 100644 --- a/PICS/Assessment/Assessments.swift +++ b/PICS/Assessment/Assessments.swift @@ -34,8 +34,7 @@ struct Assessments: View { @AppStorage("trailMakingResults") private var tmStorageResults: [AssessmentResult] = [] @AppStorage("stroopTestResults") private var stroopTestResults: [AssessmentResult] = [] @AppStorage("reactionTimeResults") private var reactionTimeResults: [AssessmentResult] = [] - // Decide whether to show test or not. - @AppStorage("AssessmentsInProgress") private var assessmentsIP = false + // Tracks which test is currently selected. @State var currentTest = Assessments.trailMaking // New property to control the sheet presentation @@ -55,25 +54,13 @@ struct Assessments: View { var body: some View { NavigationStack { - if assessmentsIP { - // Displays the active assessment based on the currentTest state. - switch currentTest { - case .trailMaking: - TrailMakingTaskView() - case .stroopTest: - StroopTestView() - case .reactionTime: - ReactionTimeView() - } - } else { - assessmentList - .navigationTitle(String(localized: "ASSESSMENTS_NAVIGATION_TITLE")) - .toolbar { - if AccountButton.shouldDisplay { - AccountButton(isPresented: $presentingAccount) - } + assessmentList + .navigationTitle(String(localized: "ASSESSMENTS_NAVIGATION_TITLE")) + .toolbar { + if AccountButton.shouldDisplay { + AccountButton(isPresented: $presentingAccount) } - } + } } .sheet(isPresented: $showingTestSheet) { // Determine which assessment view to present based on the currentTest state @@ -86,6 +73,16 @@ struct Assessments: View { ReactionTimeView() } } + .onAppear { + if FeatureFlags.mockTestData { + // Set mock test data for --mockTestData feature data. + let resultsWithTimeError = AssessmentResult(testDateTime: Date(), timeSpent: 10, errorCnt: 5) + tmStorageResults = [resultsWithTimeError] + stroopTestResults = [resultsWithTimeError] + // We only record time spent for reactionTimeResults. + reactionTimeResults = [AssessmentResult(testDateTime: Date(), timeSpent: 10)] + } + } } private var trailMakingTestSection: some View { @@ -104,8 +101,9 @@ struct Assessments: View { Text(btnText) .foregroundStyle(.accent) } - // Use style to restrict clickable area. - .buttonStyle(.plain) + .accessibility(identifier: "startTrailMakingTestButton") + // Use style to restrict clickable area. + .buttonStyle(.plain) } } } @@ -126,7 +124,8 @@ struct Assessments: View { Text(btnText) .foregroundStyle(.accent) } - .accessibility(identifier: "startTrailMakingTestButton") + .accessibility(identifier: "startStroopTestButton") + // Use style to restrict clickable area. .buttonStyle(.plain) } } @@ -148,8 +147,9 @@ struct Assessments: View { Text(btnText) .foregroundStyle(.accent) } - // Use style to restrict clickable area. - .buttonStyle(.plain) + .accessibility(identifier: "startReactimeTestButton") + // Use style to restrict clickable area. + .buttonStyle(.plain) } } } @@ -209,21 +209,18 @@ struct Assessments: View { // Function to set up and start the Trail Making assessment. func startTrailMaking() { currentTest = Assessments.trailMaking - assessmentsIP = true - showingTestSheet.toggle() + showingTestSheet = true } // Function to set up and start the Stroop Test. func startStroopTest() { currentTest = Assessments.stroopTest - assessmentsIP = true - showingTestSheet.toggle() + showingTestSheet = true } // Function to set up and start the ReactionTime Test. func startReactionTimeTest() { currentTest = Assessments.reactionTime - assessmentsIP = true - showingTestSheet.toggle() + showingTestSheet = true } // A view for displaying a message indicating that a specific assessment has not been completed. diff --git a/PICS/Assessment/ReactionTime.swift b/PICS/Assessment/ReactionTime.swift index a30db63..923488c 100644 --- a/PICS/Assessment/ReactionTime.swift +++ b/PICS/Assessment/ReactionTime.swift @@ -12,7 +12,6 @@ import SwiftUI struct ReactionTimeView: View { @AppStorage("reactionTimeResults") private var reactionTimeResults: [AssessmentResult] = [] - @AppStorage("AssessmentsInProgress") private var assessmentsIP = false @Environment(\.presentationMode) var presentationMode var body: some View { @@ -49,11 +48,11 @@ struct ReactionTimeView: View { } // Handles the result of the ReactionTime task. private func handleTaskResult(result: TaskResult) async { - assessmentsIP = false // End the assessment - // Adding this logic to dismiss the view + // Close the test by dismissing the view DispatchQueue.main.async { self.presentationMode.wrappedValue.dismiss() } + guard case let .completed(taskResult) = result else { // Failed or canceled test. Do nothing for current. return diff --git a/PICS/Assessment/ResultsViz.swift b/PICS/Assessment/ResultsViz.swift index 8b722e6..6514a21 100644 --- a/PICS/Assessment/ResultsViz.swift +++ b/PICS/Assessment/ResultsViz.swift @@ -17,6 +17,7 @@ struct ResultsViz: View { // Vars for plotting. var metricType: String + var metricEmpty: Bool // Whether we have result recorded for metric. var timeSpentLable = String(localized: "ASSESSMENT_VIZ_TIME") let metricColor = Color.purple let timeColor = Color.teal @@ -64,10 +65,9 @@ struct ResultsViz: View { } .lineStyle(StrokeStyle(lineWidth: 2.0)) } - .chartForegroundStyleScale([ - self.metricType: self.metricColor, - self.timeSpentLable: self.timeColor - ]) + .chartForegroundStyleScale( + self.metricEmpty ? [self.timeSpentLable: self.timeColor] : [self.timeSpentLable: self.timeColor, self.metricType: self.metricColor] + ) .padding(10) .chartXAxis { AxisMarks(values: .automatic(desiredCount: 3)) @@ -107,8 +107,10 @@ struct ResultsViz: View { self.yName = yName self.title = title // We assume that we only need to plot one of error count or score. - let metricMax = data.map(\.errorCnt).max() ?? -1 - self.metricType = metricMax == -1 ? String(localized: "ASSESSMENT_VIZ_SCORE") : String(localized: "ASSESSMENT_VIZ_ERRORCNT") + let errorCntMax = data.map(\.errorCnt).max() ?? -1 + let scoreMax = data.map(\.score).max() ?? -1 + self.metricType = errorCntMax == -1 ? String(localized: "ASSESSMENT_VIZ_SCORE") : String(localized: "ASSESSMENT_VIZ_ERRORCNT") + self.metricEmpty = (max(scoreMax, errorCntMax) == -1) } private func findElement(location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) -> AssessmentResult? { diff --git a/PICS/Assessment/StroopTest.swift b/PICS/Assessment/StroopTest.swift index 9e44f66..63b0e6e 100644 --- a/PICS/Assessment/StroopTest.swift +++ b/PICS/Assessment/StroopTest.swift @@ -12,7 +12,6 @@ import SwiftUI struct StroopTestView: View { @AppStorage("stroopTestResults") private var stroopTestResults: [AssessmentResult] = [] - @AppStorage("AssessmentsInProgress") private var assessmentsIP = false @Environment(\.presentationMode) var presentationMode @@ -45,8 +44,7 @@ struct StroopTestView: View { // Handles the result of the Stroop task. private func handleTaskResult(result: TaskResult) async { - assessmentsIP = false // End the assessment - // Adding this logic to dismiss the view + // Close the test by dismissing the view DispatchQueue.main.async { self.presentationMode.wrappedValue.dismiss() } diff --git a/PICS/Assessment/TrailMakingTest.swift b/PICS/Assessment/TrailMakingTest.swift index f78b9cf..185de30 100644 --- a/PICS/Assessment/TrailMakingTest.swift +++ b/PICS/Assessment/TrailMakingTest.swift @@ -13,7 +13,6 @@ import SwiftUI struct TrailMakingTaskView: View { // Use @AppStorage to store the selected dates @AppStorage("trailMakingResults") private var tmStorageResults: [AssessmentResult] = [] - @AppStorage("AssessmentsInProgress") private var tmInProgress = false @Environment(\.presentationMode) var presentationMode @@ -44,9 +43,7 @@ struct TrailMakingTaskView: View { // Handle task result private func handleTaskResult(result: TaskResult) async { - // Close the test. - tmInProgress = false - // Adding this logic to dismiss the view + // Close the test by dismissing the view DispatchQueue.main.async { self.presentationMode.wrappedValue.dismiss() } @@ -54,32 +51,38 @@ struct TrailMakingTaskView: View { guard case let .completed(taskResult) = result else { return } - - // Go to the trail making results and parse the result. - for result in taskResult.results ?? [] { - if let stepResult = result as? ORKStepResult { - if stepResult.identifier != "trailmaking" { - continue - } - for trailMakingResult in stepResult.results ?? [] { - if let curResult = trailMakingResult as? ORKTrailmakingResult { - let timeTask = if let lastItem = curResult.taps.last { - lastItem.timestamp - } else { - -1.0 - } - let parsedResult = AssessmentResult( - testDateTime: Date(), - timeSpent: timeTask, - errorCnt: Int(curResult.numberOfErrors) - ) - // Add result to local storage. - tmStorageResults += [parsedResult] + let parsedResult = parseTMResult(taskResult: taskResult) + if let nonEmptyResult = parsedResult { + tmStorageResults += [nonEmptyResult] + } + } +} + +func parseTMResult(taskResult: ORKTaskResult) -> AssessmentResult? { + // Go to the trail making results and parse the result. + for result in taskResult.results ?? [] { + if let stepResult = result as? ORKStepResult { + if stepResult.identifier != "trailmaking" { + continue + } + for trailMakingResult in stepResult.results ?? [] { + if let curResult = trailMakingResult as? ORKTrailmakingResult { + let timeTask = if let lastItem = curResult.taps.last { + lastItem.timestamp + } else { + -1.0 } + let parsedResult = AssessmentResult( + testDateTime: Date(), + timeSpent: timeTask, + errorCnt: Int(curResult.numberOfErrors) + ) + return parsedResult } } } } + return nil } #Preview { diff --git a/PICS/HealthVisulization/HKVisualizationItem.swift b/PICS/HealthVisulization/HKVisualizationItem.swift index 11c6155..cffc3aa 100644 --- a/PICS/HealthVisulization/HKVisualizationItem.swift +++ b/PICS/HealthVisulization/HKVisualizationItem.swift @@ -13,6 +13,9 @@ import SwiftUI struct HKVisualizationItem: View { + // Environment recording light or dark mode to decide color. + @Environment(\.colorScheme) var colorScheme + let id = UUID() var data: [HKData] var xName: String @@ -92,7 +95,7 @@ struct HKVisualizationItem: View { RuleMark( y: .value("Threshold", self.threshold) ) - .foregroundStyle(.black) + .foregroundStyle(colorScheme == .dark ? .white : .black) .lineStyle(StrokeStyle(lineWidth: 1, dash: [5])) } } @@ -206,7 +209,7 @@ struct HKVisualizationItem: View { } .accessibilityElement(children: .combine) .frame(width: lollipopBoxWidth, alignment: .center) - .background(Color.gray.brightness(0.4)) + .background(Color(UIColor.systemBackground)) .cornerRadius(5) .offset(x: boxOffset) } diff --git a/PICSTests/AssessmentsUnitTests.swift b/PICSTests/AssessmentsUnitTests.swift new file mode 100644 index 0000000..6c0e5ff --- /dev/null +++ b/PICSTests/AssessmentsUnitTests.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the PICS based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +@testable import PICS +import ResearchKit +import XCTest + + +class AssessmentsUnitTests: XCTestCase { + // Test tha the trail making test result could be parsed correctly without error. + func testParseTMResult() throws { + // Create the task result object with trail making results. + let taskResult = ORKTaskResult(taskIdentifier: "TestTMTaskResult", taskRun: UUID(), outputDirectory: nil) + + let tmResult = ORKTrailmakingResult(identifier: "tmResult") + tmResult.numberOfErrors = 10 + let tmTap1 = ORKTrailmakingTap() + tmTap1.timestamp = 20 + let tmTap2 = ORKTrailmakingTap() + tmTap2.timestamp = 30 + tmResult.taps = [tmTap1, tmTap2] + + let tmStepResult = ORKStepResult(stepIdentifier: "trailmaking", results: [tmResult]) + taskResult.results = [tmStepResult] + + if let parsedResult = parseTMResult(taskResult: taskResult) { + // The correct timestamp is the one from the last tap. + XCTAssertEqual(parsedResult.timeSpent, 30) + XCTAssertEqual(parsedResult.errorCnt, 10) + } else { + // Failed to parse the result. + XCTAssert(true) + } + } +} diff --git a/PICSUITests/AssessmentsTests.swift b/PICSUITests/AssessmentsTests.swift index bc713db..78e8d26 100644 --- a/PICSUITests/AssessmentsTests.swift +++ b/PICSUITests/AssessmentsTests.swift @@ -1,7 +1,7 @@ // -// This source file is part of the PICS to test the health data dashboard. +// This source file is part of the PICS to test the assessments task tab. // -// SPDX-FileCopyrightText: 2023 Stanford University +// SPDX-FileCopyrightText: 2024 Stanford University // // SPDX-License-Identifier: MIT // @@ -10,11 +10,123 @@ import XCTest class AssessmentsTests: XCTestCase { override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false - XCUIApplication().launch() + + let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding", "--mockTestData", "--disableFirebase"] + app.launch() } - - func testAssessmentViewPresence() { + + // This function launch the app and go to the assessment tab. Error will + // be thrown if no "Assessments" button found. + func getAssessmentTab() -> XCUIElement { let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding", "--mockTestData", "--disableFirebase"] + + // Go to the assessment dashboard. + XCTAssertTrue(app.buttons["Assessments"].waitForExistence(timeout: 2)) + app.buttons["Assessments"].tap() + return app + } + + func testAssessmentTextsPresence() { + let app = getAssessmentTab() + XCTAssertTrue(app.staticTexts["Trail Making Test Results"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.staticTexts["Stroop Test Results"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.staticTexts["Reaction Time Results"].waitForExistence(timeout: 2)) + } + + // Click in each assessments to check taht we are able to successfully create and start each assessment. + // Currently, we simply cancel the task for trail making and reaction time tasks as the testing of the + // task process should also be handled through ResearchKit and it's hard to test both tests as described + // below in skipTest function. + func testRunAssessments() { + let app = getAssessmentTab() + let btnsIdf = ["startTrailMakingTestButton", "startStroopTestButton", "startReactimeTestButton"] + let asmFuncs: [( (XCUIElement) -> Void )] = [skipTest, runStroopTest, skipTest] + for (idf, asmFunc) in zip(btnsIdf, asmFuncs) { + // Start assessment tasks by clicking respective buttons. + XCTAssertTrue(app.buttons[idf].waitForExistence(timeout: 2)) + app.buttons[idf].tap() + + // Check texts in this view. + while app.buttons["Next"].waitForExistence(timeout: 2) { + app.buttons["Next"].tap() + } + XCTAssertTrue(app.buttons["Get Started"].waitForExistence(timeout: 2)) + app.buttons["Get Started"].tap() + + // Run or cancel the assessments. + asmFunc(app) + } + } + + // Cancel the task to go back to original page. This is used for (1) trail making task + // for which we are unable to click the buttons to make trails as they are not showned + // in the app but grouped into an Other element 'Tappable Buttons', and (2) reaction + // time task as currently there is no supports for the shake motion in testing. + func skipTest(app: XCUIElement) { + XCTAssertTrue(app.buttons["Cancel"].waitForExistence(timeout: 2)) + app.buttons["Cancel"].tap() + XCTAssertTrue(app.buttons["End Task"].waitForExistence(timeout: 2)) + app.buttons["End Task"].tap() + } + + // Check content and run through the Stroop Assessment. + func runStroopTest(app: XCUIElement) { + // The test will start in 5 secondes, adding a buffer, we wait for 7 seconds. + sleep(7) + + // Click on "R" for all attempts. + for _ in 0...4 { + XCTAssertTrue(app.staticTexts["R"].waitForExistence(timeout: 2)) + app.staticTexts["R"].tap() + } + + XCTAssertTrue(app.buttons["Done"].waitForExistence(timeout: 2)) + app.buttons["Done"].tap() + } + + // Test whether the charts pop up the correct details for the assessment results when + // clicking/not clicking on the charts. + func testResultViz() { + let app = getAssessmentTab() + + // We use the offset from the chart title to find the position to click on the chart. + let xOffset: CGFloat = 50 + let yOffset: CGFloat = 150 + let normalized = app.coordinate(withNormalizedOffset: .zero) + let frame = app.staticTexts["Trail Making Test Results"].frame + normalized.withOffset(CGVector(dx: frame.minX + xOffset, dy: frame.minY + yOffset)).tap() + checkOccurrenceResultTexts(app: app) + + // Click again to reset the details, ensure that no error happened. + normalized.withOffset(CGVector(dx: frame.minX + xOffset, dy: frame.minY + yOffset)).tap() + checkOccurrenceResultTexts(app: app, hideDetails: true) + } + + // Check the text occurrences for results to ensure that we show correct details. + func checkOccurrenceResultTexts(app: XCUIElement, hideDetails: Bool = false) { + // Check to verify that the details pop up the correct texts. + // The unclicked results details should show the last result. + let lastResultText = "Your last assessment result at" + // The clicked result should show with different prefix texts. + let clickedResultText = "Your assessment result at" + // We manually set the time spent to 10 for all three tasks. + let timeText = "Time Spent (seconds): 10.0" + var lastResultCnt = 0 + var timeTextCnt = 0 + var clickedResultCnt = 0 + for staticText in app.staticTexts.allElementsBoundByIndex { + let curLabel = staticText.label + lastResultCnt += curLabel.contains(lastResultText) ? 1 : 0 + clickedResultCnt += curLabel.contains(clickedResultText) ? 1 : 0 + timeTextCnt += curLabel.contains(timeText) ? 1 : 0 + } + XCTAssertEqual(lastResultCnt, hideDetails ? 3 : 2) + XCTAssertEqual(timeTextCnt, 3) + XCTAssertEqual(clickedResultCnt, hideDetails ? 0 : 1) } }