Skip to content

Commit 6490c8a

Browse files
Added HKVisualizations UI Tests (#40)
# *Added HKVisualizations UI Tests* ## ♻️ Current situation & Problem *Link any open issues or pull requests (PRs) related to this PR. Please ensure that all non-trivial PRs are first tracked and discussed in an existing GitHub issue or discussion.* ## ⚙️ Release Notes *Add a bullet point list summary of the feature and possible migration guides if this is a breaking change so this section can be added to the release notes.* *Include code snippets that provide examples of the feature implemented or links to the documentation if it appends or changes the public interface.* This PR introduces a comprehensive set of UI tests to cover the HKVisualization view. The tests validate the correct rendering of UI elements, state handling, and user interactions. Test Coverage Overview: UI Rendering - Test Correct View Rendering – Ensures that the view renders correctly when data is available or unavailable. - Test Title and Labels – Verifies that chart titles, x-axis, and y-axis labels are correctly displayed. - Test Threshold Rendering – Confirms that the threshold line appears on the chart. Interaction and State Handling - Test Tap Gesture for Lollipop – Verifies that tapping a bar or point brings up the lollipop with correct data. - Test Data Accuracy in Lollipop – Validates that the correct data values appear in the lollipop. - Test Average Line Rendering – Ensures that the average line appears only when average data is available. - Test Accessibility Labels – Confirms that chart elements have accurate accessibility labels. The tests are located in HKVisualizationTests.swift. Tests run using XCTest and XCTestExtensions. Mock data is injected via launch arguments (--skipOnboarding, --mockLabData). ## 📚 Documentation *Please ensure that you properly document any additions in conformance to [Spezi Documentation Guide](https://github.com/StanfordSpezi/.github/blob/main/DOCUMENTATIONGUIDE.md).* *You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.* ## ✅ Testing *Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.* *This section describes important information about the tests and why some elements might not be testable.* ## 📝 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). --------- Co-authored-by: Sixian Du <[email protected]>
1 parent 76de429 commit 6490c8a

File tree

7 files changed

+142
-13
lines changed

7 files changed

+142
-13
lines changed

NeutroFeverGuard.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,14 @@
9999
C4C8A35F2D6F950600F313AE /* AddDataViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8A35E2D6F950600F313AE /* AddDataViewTests.swift */; };
100100
C4C8A3682D6F9E2100F313AE /* LabViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8A3672D6F9E1A00F313AE /* LabViewUITests.swift */; };
101101
C4C8A3B32D71204200F313AE /* HelperFuncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8A3B22D71204200F313AE /* HelperFuncTests.swift */; };
102+
EB0065572D813AC900F6DA00 /* HKVisualizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0065562D813AC900F6DA00 /* HKVisualizationTests.swift */; };
102103
EB02C6612D5D52E90035AA89 /* NeutroFeverGuardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256128338800005D4D48 /* NeutroFeverGuardTests.swift */; };
103104
EB5D7CF92D8108BC0046BF8E /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; };
104105
EB6FDBCE2D80133A008E9F90 /* HKVisualization.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6FDBCB2D80133A008E9F90 /* HKVisualization.swift */; };
105106
EB6FDBCF2D80133A008E9F90 /* HKVisualizationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6FDBCC2D80133A008E9F90 /* HKVisualizationItem.swift */; };
106107
EB6FDBD02D80133A008E9F90 /* HKVisualization.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6FDBCB2D80133A008E9F90 /* HKVisualization.swift */; };
107108
EB6FDBD12D80133A008E9F90 /* HKVisualizationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6FDBCC2D80133A008E9F90 /* HKVisualizationItem.swift */; };
109+
EBFA80652D81FBE600596C3C /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */; };
108110
F223937A2D5D4095006C8EB4 /* DataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22393792D5D4092006C8EB4 /* DataError.swift */; };
109111
F268C4562D7F928C0020026F /* SymptomManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F268C4552D7F928B0020026F /* SymptomManager.swift */; };
110112
F279F70C2D780462005C2927 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = F2A54D362D5DE24400A20113 /* FirebaseCore */; };
@@ -193,6 +195,7 @@
193195
C4C8A35E2D6F950600F313AE /* AddDataViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddDataViewTests.swift; sourceTree = "<group>"; };
194196
C4C8A3672D6F9E1A00F313AE /* LabViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabViewUITests.swift; sourceTree = "<group>"; };
195197
C4C8A3B22D71204200F313AE /* HelperFuncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperFuncTests.swift; sourceTree = "<group>"; };
198+
EB0065562D813AC900F6DA00 /* HKVisualizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKVisualizationTests.swift; sourceTree = "<group>"; };
196199
EB6FDBCB2D80133A008E9F90 /* HKVisualization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKVisualization.swift; sourceTree = "<group>"; };
197200
EB6FDBCC2D80133A008E9F90 /* HKVisualizationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKVisualizationItem.swift; sourceTree = "<group>"; };
198201
F22393792D5D4092006C8EB4 /* DataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataError.swift; sourceTree = "<group>"; };
@@ -403,6 +406,7 @@
403406
C4C8A35E2D6F950600F313AE /* AddDataViewTests.swift */,
404407
2F4E23862989DB360013F3D9 /* ContactsTests.swift */,
405408
5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */,
409+
EB0065562D813AC900F6DA00 /* HKVisualizationTests.swift */,
406410
);
407411
path = NeutroFeverGuardUITests;
408412
sourceTree = "<group>";
@@ -707,6 +711,7 @@
707711
EB6FDBD02D80133A008E9F90 /* HKVisualization.swift in Sources */,
708712
EB6FDBD12D80133A008E9F90 /* HKVisualizationItem.swift in Sources */,
709713
B03D0E482D7D24AA0024F2CA /* DataError.swift in Sources */,
714+
EBFA80652D81FBE600596C3C /* FeatureFlags.swift in Sources */,
710715
C4C8A3B32D71204200F313AE /* HelperFuncTests.swift in Sources */,
711716
B03D0E432D7D22700024F2CA /* HealthKitService.swift in Sources */,
712717
EB5D7CF92D8108BC0046BF8E /* AccountButton.swift in Sources */,
@@ -724,6 +729,7 @@
724729
C4C8A3682D6F9E2100F313AE /* LabViewUITests.swift in Sources */,
725730
C42C1C712D7AB84300EA417F /* MedicationViewUITests.swift in Sources */,
726731
C4C8A35F2D6F950600F313AE /* AddDataViewTests.swift in Sources */,
732+
EB0065572D813AC900F6DA00 /* HKVisualizationTests.swift in Sources */,
727733
2F4E23872989DB360013F3D9 /* ContactsTests.swift in Sources */,
728734
653A256C28338800005D4D48 /* SchedulerTests.swift in Sources */,
729735
);

NeutroFeverGuard.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

NeutroFeverGuard/HKVisualization.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ struct HKVisualization: View {
107107
}
108108

109109
func readAllHKData(ensureUpdate: Bool = false) {
110+
if FeatureFlags.mockVizData {
111+
loadMockData()
112+
return
113+
}
110114
print("Reading all HealthKit data with ensureUpdate: \(ensureUpdate)")
111115
let dateRange = generateDateRange()
112116
guard let startDate = dateRange[0] as? Date else {
@@ -287,6 +291,51 @@ struct HKVisualization: View {
287291
print("Unexpected quantity received:", quantityTypeIDF)
288292
}
289293
}
294+
295+
func loadMockData() {
296+
let today = Date()
297+
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today) ?? today
298+
let twoDaysAgo = Calendar.current.date(byAdding: .day, value: -2, to: today) ?? today
299+
300+
// Heart Rate Mock Data (60-100 bpm normal range)
301+
self.heartRateData = [
302+
HKData(date: today, sumValue: 75, avgValue: 75, minValue: 65, maxValue: 85),
303+
HKData(date: yesterday, sumValue: 82, avgValue: 82, minValue: 70, maxValue: 95),
304+
HKData(date: twoDaysAgo, sumValue: 90, avgValue: 90, minValue: 80, maxValue: 105)
305+
]
306+
307+
self.heartRateScatterData = [
308+
HKData(date: today, sumValue: 75, avgValue: 75, minValue: 75, maxValue: 75),
309+
HKData(date: yesterday, sumValue: 82, avgValue: 82, minValue: 82, maxValue: 82),
310+
HKData(date: twoDaysAgo, sumValue: 90, avgValue: 90, minValue: 90, maxValue: 90)
311+
]
312+
313+
// Body Temperature Mock Data (97-99°F normal range)
314+
self.bodyTemperatureData = [
315+
HKData(date: today, sumValue: 98.6, avgValue: 98.6, minValue: 98.2, maxValue: 99.0),
316+
HKData(date: yesterday, sumValue: 98.9, avgValue: 98.9, minValue: 98.5, maxValue: 99.2),
317+
HKData(date: twoDaysAgo, sumValue: 99.1, avgValue: 99.1, minValue: 98.7, maxValue: 99.5)
318+
]
319+
320+
self.bodyTemperatureScatterData = [
321+
HKData(date: today, sumValue: 98.6, avgValue: 98.6, minValue: 98.6, maxValue: 98.6),
322+
HKData(date: yesterday, sumValue: 98.9, avgValue: 98.9, minValue: 98.9, maxValue: 98.9),
323+
HKData(date: twoDaysAgo, sumValue: 99.1, avgValue: 99.1, minValue: 99.1, maxValue: 99.1)
324+
]
325+
326+
// Oxygen Saturation Mock Data (94-100% normal range)
327+
self.oxygenSaturationData = [
328+
HKData(date: today, sumValue: 98, avgValue: 98, minValue: 96, maxValue: 99),
329+
HKData(date: yesterday, sumValue: 97, avgValue: 97, minValue: 95, maxValue: 98),
330+
HKData(date: twoDaysAgo, sumValue: 96, avgValue: 96, minValue: 94, maxValue: 97)
331+
]
332+
333+
self.oxygenSaturationScatterData = [
334+
HKData(date: today, sumValue: 98, avgValue: 98, minValue: 98, maxValue: 98),
335+
HKData(date: yesterday, sumValue: 97, avgValue: 97, minValue: 97, maxValue: 97),
336+
HKData(date: twoDaysAgo, sumValue: 96, avgValue: 96, minValue: 96, maxValue: 96)
337+
]
338+
}
290339
// swiftlint:enable closure_body_length
291340
}
292341

NeutroFeverGuard/HKVisualizationItem.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ struct HKVisualizationItem: View {
5151
String(localized: "Average: ", locale: locale) +
5252
String(round(elm.avgValue * 10) / 10) +
5353
", " +
54-
String(localized: "Max Value: ", locale: locale) +
54+
String(localized: "Max value: ", locale: locale) +
5555
String(Int(round(elm.maxValue))) +
5656
", " +
57-
String(localized: "Min Value: ", locale: locale) +
57+
String(localized: "Min value: ", locale: locale) +
5858
String(Int(round(elm.minValue)))
5959
)
6060
Text(details)
@@ -72,6 +72,7 @@ struct HKVisualizationItem: View {
7272
y: .value(.init(self.yName), dataPoint.sumValue)
7373
)
7474
.foregroundStyle(getBarColor(value: dataPoint.sumValue, date: dataPoint.date).opacity(0.2))
75+
.accessibilityIdentifier("ScatterPoint_\(dataPoint.date)")
7576
}
7677
ForEach(data) { dataPoint in
7778
BarMark(
@@ -80,6 +81,7 @@ struct HKVisualizationItem: View {
8081
width: .fixed(10)
8182
)
8283
.foregroundStyle(getBarColor(value: dataPoint.sumValue, date: dataPoint.date))
84+
.accessibilityIdentifier("Bar_\(dataPoint.date)")
8385
if self.plotAvg {
8486
LineMark(
8587
x: .value(.init(self.xName), dataPoint.date, unit: .day),
@@ -95,6 +97,7 @@ struct HKVisualizationItem: View {
9597
)
9698
.foregroundStyle(.primary)
9799
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
100+
.accessibilityIdentifier("Threshold")
98101
}
99102
}
100103
.padding(.top, 10)

NeutroFeverGuard/LabResultsManager.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class LabResultsManager: Module, EnvironmentAccessible {
2121
@ObservationIgnored @Dependency(LocalStorage.self) private var localStorage
2222
@ObservationIgnored @Dependency(FirebaseConfiguration.self) private var firebaseConfig
2323

24-
2524
func configure() {
2625
loadLabResults() // Load data on startup
2726
if FeatureFlags.mockLabData {

NeutroFeverGuard/SharedContext/FeatureFlags.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ enum FeatureFlags {
1414
static let showOnboarding = CommandLine.arguments.contains("--showOnboarding")
1515
/// Disables the Firebase interactions, including the login/sign-up step and the Firebase Firestore upload.
1616
static let disableFirebase = CommandLine.arguments.contains("--disableFirebase")
17-
#if targetEnvironment(simulator)
17+
#if targetEnvironment(simulator)
1818
/// Defines if the application should connect to the local firebase emulator. Always set to true when using the iOS simulator.
1919
static let useFirebaseEmulator = true
20-
#else
20+
#else
2121
/// Defines if the application should connect to the local firebase emulator. Always set to true when using the iOS simulator.
2222
static let useFirebaseEmulator = CommandLine.arguments.contains("--useFirebaseEmulator")
23-
#endif
23+
#endif
2424
/// Automatically sign in into a test account upon app launch.
25-
25+
2626
/// Requires ``disableFirebase`` to be `false`.
2727
static let setupTestAccount = CommandLine.arguments.contains("--setupTestAccount")
2828

2929
static let mockLabData = CommandLine.arguments.contains("--mockLabData")
3030

3131
static let mockMedData = CommandLine.arguments.contains("--mockMedData")
3232

33-
// static let vizMockTestData = CommandLine.arguments.contains("--vizMockTestData")
33+
static let mockVizData = CommandLine.arguments.contains("--mockVizData")
3434
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// This source file is part of the NeutroFeverGuard based on the Stanford Spezi Template Application project
3+
//
4+
// SPDX-FileCopyrightText: 2025 Stanford University
5+
//
6+
// SPDX-License-Identifier: MIT
7+
//
8+
import XCTest
9+
import XCTestExtensions
10+
11+
12+
class HKVisualizationTests: XCTestCase {
13+
@MainActor
14+
override func setUp() async throws {
15+
continueAfterFailure = false
16+
17+
let app = XCUIApplication()
18+
app.launchArguments = ["--skipOnboarding", "--mockVizData"]
19+
app.deleteAndLaunch(withSpringboardAppName: "NeutroFeverGuard")
20+
}
21+
22+
@MainActor
23+
func testHealthKitView() throws {
24+
let app = XCUIApplication()
25+
26+
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0))
27+
28+
XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Dashboard"].waitForExistence(timeout: 2))
29+
app.tabBars["Tab Bar"].buttons["Dashboard"].tap()
30+
try app.handleHealthKitAuthorization()
31+
}
32+
33+
@MainActor
34+
func testHealthDashboard() throws {
35+
let app = XCUIApplication()
36+
37+
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0))
38+
39+
XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Dashboard"].waitForExistence(timeout: 2))
40+
app.tabBars["Tab Bar"].buttons["Dashboard"].tap()
41+
try app.handleHealthKitAuthorization()
42+
43+
// Verify Plot Titles Exists
44+
XCTAssertTrue(app.staticTexts["Oxygen Saturation Over Time"].exists)
45+
XCTAssertTrue(app.staticTexts["Body Temperature Over Time"].exists)
46+
XCTAssertTrue(app.staticTexts["Heart Rate Over Time"].exists)
47+
48+
// Test Lollipop functions
49+
XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Dashboard"].waitForExistence(timeout: 2))
50+
app.tabBars["Tab Bar"].buttons["Dashboard"].tap()
51+
try app.handleHealthKitAuthorization()
52+
53+
// Wait for chart to appear
54+
let chartTitle = app.staticTexts["Oxygen Saturation Over Time"]
55+
XCTAssertTrue(chartTitle.waitForExistence(timeout: 5))
56+
57+
// Get the frame of the chart title
58+
let frame = chartTitle.frame
59+
// Tap below the title where the chart should be
60+
let tapPoint = CGPoint(x: frame.maxX - 50, y: frame.maxY + 100) // Tap near the right side where today's data point should be
61+
app.coordinate(withNormalizedOffset: .zero).withOffset(CGVector(dx: tapPoint.x, dy: tapPoint.y)).tap()
62+
63+
// Verify that some interaction happened by checking for a date
64+
let today = Date()
65+
let formatter = DateFormatter()
66+
formatter.dateStyle = .medium
67+
let dateStr = formatter.string(from: today)
68+
69+
let dateExists = app.staticTexts[dateStr].waitForExistence(timeout: 2)
70+
XCTAssertTrue(dateExists, "Today's date (\(dateStr)) should appear after tapping")
71+
}
72+
}

0 commit comments

Comments
 (0)