Skip to content

Commit

Permalink
Assessments tests and minor fixes (#56)
Browse files Browse the repository at this point in the history
# Assessments tests and minor fixes

## ♻️ 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.

## ⚙️ 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.

<img width="200" alt="image"
src="https://github.com/CS342/2024-PICS/assets/32094663/bd943430-d570-4dfa-952a-9cc0f5b2430c">


## 📚 Documentation
Related commends are added to the codes.

## ✅ Testing
The PR adds tests to our codes.

## 📝 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).
  • Loading branch information
YurenSUN authored Mar 11, 2024
1 parent 3be7d9a commit e58a3a8
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 77 deletions.
8 changes: 6 additions & 2 deletions PICS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -205,6 +206,7 @@
86F62AF52B916B670075F23C /* AppointmentInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppointmentInformation.swift; sourceTree = "<group>"; };
A403A52D2B705A8C003CFA5C /* HealthVisualizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthVisualizationTests.swift; sourceTree = "<group>"; };
A40559A32B98204C00221783 /* HKVizUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKVizUnitTests.swift; sourceTree = "<group>"; };
A45546C12B9E56A1006DB4B4 /* AssessmentsUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssessmentsUnitTests.swift; sourceTree = "<group>"; };
A45993282B90544300A98C95 /* Assessments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assessments.swift; sourceTree = "<group>"; };
A459932B2B906C3B00A98C95 /* ResultsViz.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsViz.swift; sourceTree = "<group>"; };
A480C7BF2B6D5A3700B29A07 /* HKVisualization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKVisualization.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -438,6 +440,7 @@
children = (
653A256128338800005D4D48 /* PICSTests.swift */,
A40559A32B98204C00221783 /* HKVizUnitTests.swift */,
A45546C12B9E56A1006DB4B4 /* AssessmentsUnitTests.swift */,
);
path = PICSTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -786,6 +789,7 @@
buildActionMask = 2147483647;
files = (
A40559A42B98204C00221783 /* HKVizUnitTests.swift in Sources */,
A45546C22B9E56A1006DB4B4 /* AssessmentsUnitTests.swift in Sources */,
653A256228338800005D4D48 /* PICSTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
59 changes: 28 additions & 31 deletions PICS/Assessment/Assessments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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)
}
}
}
Expand All @@ -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)
}
}
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 2 additions & 3 deletions PICS/Assessment/ReactionTime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions PICS/Assessment/ResultsViz.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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? {
Expand Down
4 changes: 1 addition & 3 deletions PICS/Assessment/StroopTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
}
Expand Down
53 changes: 28 additions & 25 deletions PICS/Assessment/TrailMakingTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -44,42 +43,46 @@ 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()
}

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 {
Expand Down
7 changes: 5 additions & 2 deletions PICS/HealthVisulization/HKVisualizationItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]))
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
40 changes: 40 additions & 0 deletions PICSTests/AssessmentsUnitTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading

0 comments on commit e58a3a8

Please sign in to comment.