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

fix deletion of HealthKit-connected apps #28

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: XCTestExtensions-Package
destination: 'platform=watchOS Simulator,name=Apple Watch Series 9 (45mm)'
destination: 'platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)'
resultBundle: XCTestExtensions-watchOS.xcresult
artifactname: XCTestExtensions-watchOS.xcresult
buildandtest_visionos:
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thank you for that fix!

Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ extension XCUIApplication {
#endif

if springboard.icons[appName].waitForNonExistence(timeout: 2.0) {
// If the app had health data stored, deleting the app will show an alert, which we need to dismiss
let alertTitle = "There is data from “\(appName)” saved in Health"
if springboard.alerts[alertTitle].waitForExistence(timeout: 2) {
springboard.alerts[alertTitle].buttons["OK"].tap()
}
break
}
}
Expand Down
80 changes: 80 additions & 0 deletions Tests/UITests/TestApp/HealthKitDataEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// This source file is part of the Stanford XCTestExtensions open-source project
//
// SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import HealthKit
import SwiftUI


private let healthStore = HKHealthStore()

struct HealthKitDataEntryView: View {
@State private var isAddingSample = false
@State private var didAddSample = false

@State private var hasAccess = false

var body: some View {
Form {
Button("Add a HealthKit sample") {
Task {
do {
try await addSample()
didAddSample = true
} catch {
print("Erorr trying to add test sample: \(error)")
}
}
}.disabled(isAddingSample)

Section { // status section
if hasAccess {
Text("Has access")
}
if didAddSample {
Text("Did add sample!")
}
}
}
.task {
switch healthStore.authorizationStatus(for: HKQuantityType(.heartRate)) {
case .sharingAuthorized:
hasAccess = true
case .notDetermined, .sharingDenied: // sharingDenied will never happen in the test environment.
hasAccess = false
@unknown default:
hasAccess = false
}
}
}


private func addSample() async throws {
isAddingSample = true
defer {
isAddingSample = false
}
guard HKHealthStore.isHealthDataAvailable() else {
return
}

let now = Date()
let heartRateType = HKQuantityType(.heartRate)

try await healthStore.requestAuthorization(toShare: [heartRateType], read: [])
let sample = HKQuantitySample(
type: heartRateType,
quantity: .init(unit: .count().unitDivided(by: .minute()), doubleValue: 69),
start: now,
end: now,
// adding this incase anyone ever runs this on a real device by accicent,
// so that they can easily identify and delete any test samples.
metadata: ["edu.stanford.BDHG.XCTestExtensions.isTestSample": "1"]
)
try await healthStore.save(sample)
}
}
2 changes: 2 additions & 0 deletions Tests/UITests/TestApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSHealthUpdateUsageDescription</key>
<string>The app adds a sample to the HealthKit database, to fully test app deletion</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
Expand Down
5 changes: 4 additions & 1 deletion Tests/UITests/TestApp/TestApp.entitlements
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>com.apple.developer.healthkit</key>
<true/>
</dict>
</plist>
4 changes: 3 additions & 1 deletion Tests/UITests/TestApp/TestAppTestCaseEnum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ enum TestAppTestCaseEnum: String, TestAppTests {
case xcTestApp = "XCTestApp"
case xcTestExtensions = "XCTestExtensions"
case dismissKeyboard = "DismissKeyboard"

case healthKitDataEntry = "HealthKitDataEntry"

func view(withNavigationPath path: Binding<NavigationPath>) -> some View {
switch self {
Expand All @@ -24,6 +24,8 @@ enum TestAppTestCaseEnum: String, TestAppTests {
XCTestExtensionsTest()
case .dismissKeyboard:
DismissKeyboardTest()
case .healthKitDataEntry:
HealthKitDataEntryView()
}
}
}
31 changes: 31 additions & 0 deletions Tests/UITests/TestAppUITests/XCTestExtensionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,38 @@
XCTAssert(app.staticTexts["No text set ..."].waitForExistence(timeout: 5))
XCTAssert(app.staticTexts["No secure text set ..."].exists)
}

@available(macOS, unavailable)
@available(watchOS, unavailable)
@available(visionOS, unavailable)
@available(tvOS, unavailable)
func testDeleteAndLaunchWithHealthKitSample() throws {
#if os(macOS) || os(watchOS) || os(visionOS) || os(tvOS)
throw XCTSkip("Not supported on this platform")
#endif

let app = XCUIApplication()

Check warning on line 46 in Tests/UITests/TestAppUITests/XCTestExtensionsTests.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS / Test using xcodebuild or run fastlane

code after 'throw' will never be executed
app.deleteAndLaunch(withSpringboardAppName: "TestApp")

XCTAssert(app.buttons["HealthKitDataEntry"].waitForExistence(timeout: 5.0))
app.buttons["HealthKitDataEntry"].tap()

XCTAssert(app.buttons["Add a HealthKit sample"].waitForExistence(timeout: 3))
app.buttons["Add a HealthKit sample"].tap()

if app.staticTexts["Has access"].waitForNonExistence(timeout: 2) {
// if the view isn't telling us that it has access, it's currently requesting access
app.tables.staticTexts["Turn On All"].tap()
app.navigationBars["Health Access"].buttons["Allow"].tap()
}

XCTAssert(app.staticTexts["Did add sample!"].waitForExistence(timeout: 5))
XCTAssert(app.staticTexts["Did add sample!"].exists)

app.deleteAndLaunch(withSpringboardAppName: "TestApp")
}


@available(macOS, unavailable)
@available(watchOS, unavailable)
func testDeleteAndLaunchFromFirstPage() throws {
Expand All @@ -44,7 +75,7 @@
// currently don't know how to swipe on the Reality Launcher. So not a super big use case for us now.
throw XCTSkip("VisionOS will typically have apps installed on the first screen anyways.")
#endif
let springboard = XCUIApplication(bundleIdentifier: XCUIApplication.homeScreenBundle)

Check warning on line 78 in Tests/UITests/TestAppUITests/XCTestExtensionsTests.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS / Test using xcodebuild or run fastlane

code after 'throw' will never be executed
springboard.activate()
springboard.swipeRight()

Expand Down
8 changes: 8 additions & 0 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
2F87F9EE2952F10600810247 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 2F87F9ED2952F10600810247 /* XCTestExtensions */; };
2F8A431329130A8C005D2B8F /* XCTestExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* XCTestExtensionsTests.swift */; };
2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; };
80A1F4312D393F6800183BFF /* HealthKitDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A1F4302D393F6800183BFF /* HealthKitDataEntry.swift */; };
A9E8E2552AA375DC0051274C /* DismissKeyboardTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8E2542AA375DC0051274C /* DismissKeyboardTest.swift */; };
/* End PBXBuildFile section */

Expand All @@ -42,6 +43,8 @@
2F8A431229130A8C005D2B8F /* XCTestExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestExtensionsTests.swift; sourceTree = "<group>"; };
2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = "<group>"; };
2FE5AAA829985FD9004A0442 /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = "<group>"; };
80A1F42F2D393F3600183BFF /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = "<group>"; };
80A1F4302D393F6800183BFF /* HealthKitDataEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitDataEntry.swift; sourceTree = "<group>"; };
A9E8E2542AA375DC0051274C /* DismissKeyboardTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissKeyboardTest.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -89,13 +92,15 @@
2F6D139428F5F384007C25D6 /* TestApp */ = {
isa = PBXGroup;
children = (
80A1F42F2D393F3600183BFF /* TestApp.entitlements */,
2F273FCA298DC10300FCA397 /* Info.plist */,
2FA7382B290ADFAA007ACEB9 /* TestApp.swift */,
2F2D337729DE323E00081B1D /* XCTestAppTestCaseTest.swift */,
2F2D338A29DE576800081B1D /* XCTestExtensionsTest.swift */,
2F2D338829DE570C00081B1D /* TestAppTestCaseEnum.swift */,
2F6D139928F5F386007C25D6 /* Assets.xcassets */,
A9E8E2542AA375DC0051274C /* DismissKeyboardTest.swift */,
80A1F4302D393F6800183BFF /* HealthKitDataEntry.swift */,
);
path = TestApp;
sourceTree = "<group>";
Expand Down Expand Up @@ -226,6 +231,7 @@
A9E8E2552AA375DC0051274C /* DismissKeyboardTest.swift in Sources */,
2F2D338929DE570C00081B1D /* TestAppTestCaseEnum.swift in Sources */,
2F2D337829DE323E00081B1D /* XCTestAppTestCaseTest.swift in Sources */,
80A1F4312D393F6800183BFF /* HealthKitDataEntry.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -374,6 +380,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
Expand Down Expand Up @@ -411,6 +418,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
Expand Down
Loading