Skip to content

Commit

Permalink
Improve App Deletion on VisionOS and Swift 6 (#26)
Browse files Browse the repository at this point in the history
# Improve App Deletion on VisionOS and Swift 6

## ♻️ Current situation & Problem
This PR improves app deletion on the visionOS platform. Further, we
introduce a new `delete(app:)` version that doesn't launch the
application. This is useful if you want to delete the app as a teardown
procedure of your application. Instead of deleting the application
before your test runs you can delete the application after test that
perform state changes on the application.

This feature enables the Swift 6 language mode.


## ⚙️ Release Notes 
* Added `delete(app:)` extension
* Improved app deletion on visionOS
* Swift 6


## 📚 Documentation
Updated new interfaces.


## ✅ Testing
--

### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Oct 17, 2024
1 parent aad6c16 commit 5379d70
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 54 deletions.
41 changes: 24 additions & 17 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0

//
// This source file is part of the Stanford XCTestExtensions open-source project
Expand All @@ -8,16 +8,10 @@
// SPDX-License-Identifier: MIT
//

import class Foundation.ProcessInfo
import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


let package = Package(
name: "XCTestExtensions",
platforms: [
Expand All @@ -30,27 +24,40 @@ let package = Package(
.library(name: "XCTestApp", targets: ["XCTestApp"]),
.library(name: "XCTestExtensions", targets: ["XCTestExtensions"])
],
dependencies: [] + swiftLintPackage(),
targets: [
.target(
name: "XCTestApp",
swiftSettings: [
swiftConcurrency
]
plugins: [] + swiftLintPlugin()
),
.target(
name: "XCTestExtensions",
swiftSettings: [
swiftConcurrency
]
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "XCTestExtensionsTests",
dependencies: [
.target(name: "XCTestExtensions")
],
swiftSettings: [
swiftConcurrency
]
plugins: [] + swiftLintPlugin()
)
]
)


func swiftLintPlugin() -> [Target.PluginUsage] {
// Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app`
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
[.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")]
} else {
[]
}
}

func swiftLintPackage() -> [PackageDescription.Package.Dependency] {
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
[.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")]
} else {
[]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ The `enter(value:)` and `delete(count:)` methods provide the `checkIfTextWasEnte
### App Interaction

- ``XCTest/XCUIApplication/deleteAndLaunch(withSpringboardAppName:)``
- ``XCTest/XCUIApplication/delete(app:)``
- ``XCTest/XCUIApplication/dismissKeyboard()``
- ``XCTest/XCUIApplication/homeScreenBundle``
77 changes: 42 additions & 35 deletions Sources/XCTestExtensions/XCUIApplication+DeleteAndLaunch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,86 +28,93 @@ extension XCUIApplication {
}

private static var visionOS2: Bool {
#if os(visionOS)
#if os(visionOS)
if #available(visionOS 2.0, *) {
true
} else {
false
}
#else
#else
false
#endif
#endif
}

/// Deletes the application from the iOS springboard (iOS home screen) and launches it after it has been deleted and reinstalled.
/// - Parameter appName: The name of the application as displayed on the springboard (iOS home screen).
@available(macOS, unavailable)
@available(watchOS, unavailable)
public func deleteAndLaunch(withSpringboardAppName appName: String) {
self.delete(app: appName)
self.launch()
}

/// Delete the application from the home screen.
///
/// Deletes the application from the iOS Springboard, visionOS RealityLauncher or tvOS Pineboard.
/// - Parameter appName: The springboard name of the application.
@available(macOS, unavailable)
@available(watchOS, unavailable)
public func delete(app appName: String) {
self.terminate()

let springboard = XCUIApplication(bundleIdentifier: Self.homeScreenBundle)
#if os(visionOS)
#if os(visionOS)
springboard.launch() // springboard is in `runningBackgroundSuspended` state on visionOS. So we need to launch it not just activate
#else
#else
springboard.activate()
#endif
#endif

if springboard.icons[appName].firstMatch.waitForExistence(timeout: 10.0) {
// There might be multiple apps installed with the same name (e.g., we use "TestApp" a lot), so delete all of them
while springboard.icons[appName].firstMatch.waitForExistence(timeout: 10.0) {
if !springboard.icons[appName].firstMatch.isHittable {
springboard.swipeLeft()
}

XCTAssertTrue(springboard.icons[appName].firstMatch.isHittable)
springboard.icons[appName].firstMatch.press(forDuration: 1.75)

#if os(visionOS)
springboard.icons[appName].firstMatch.press(forDuration: 1)
#else
springboard.icons[appName].firstMatch.press(forDuration: 1.5)
#endif

if XCUIApplication.visionOS2 {
// VisionOS 2.0 changed the behavior how apps are deleted, showing a delete button above the app icon.
sleep(5)
let deleteButtons = springboard.collectionViews.buttons.matching(identifier: "Delete")
// There is no isEmtpy property on the `XCUIElementQuery`.
// swiftlint:disable:next empty_count
if deleteButtons.count > 0 {
// We assume that the latest installed app is on the trailing part of the screen and therefore also the last button.
let lastDeleteButton = deleteButtons.element(boundBy: deleteButtons.count - 1)
lastDeleteButton.tap()
} else {
XCTFail("No 'Delete' buttons found")
}
let deleteButton = springboard.icons[appName].buttons["Delete"]

XCTAssertTrue(deleteButton.waitForExistence(timeout: 5.0))
deleteButton.tap()
} else {
if !springboard.collectionViews.buttons["Remove App"].waitForExistence(timeout: 10.0) && springboard.state != .runningForeground {
// The long press did not work, let's launch the springboard again and then try long pressing the app icon again.
springboard.activate()
sleep(2)

XCTAssert(springboard.wait(for: .runningForeground, timeout: 2.0))

XCTAssertTrue(springboard.icons[appName].firstMatch.isHittable)
springboard.icons[appName].firstMatch.press(forDuration: 1.75)

XCTAssertTrue(springboard.collectionViews.buttons["Remove App"].waitForExistence(timeout: 10.0))
}

springboard.buttons["Remove App"].tap()
}
#if os(visionOS)

#if os(visionOS)
// alerts are running in their own process on visionOS (lol). Took me literally 3 hours.
let notifications = visionOSNotifications

XCTAssert(notifications.staticTexts["Delete “\(appName)”?"].waitForExistence(timeout: 5.0))
XCTAssert(notifications.buttons["Delete"].waitForExistence(timeout: 2.0))
notifications.buttons["Delete"].tap() // currently no better way of hitting some "random" delete button.
#else
#else
XCTAssertTrue(springboard.alerts["Remove “\(appName)”?"].buttons["Delete App"].waitForExistence(timeout: 10.0))
springboard.alerts["Remove “\(appName)”?"].buttons["Delete App"].tap()
XCTAssertTrue(springboard.alerts["Delete “\(appName)”?"].buttons["Delete"].waitForExistence(timeout: 10.0))
springboard.alerts["Delete “\(appName)”?"].buttons["Delete"].tap()
#endif
#endif

if springboard.icons[appName].waitForNonExistence(timeout: 2.0) {
break
}
}

// Wait for 5 Seconds for the application to be deleted and removed.
sleep(5)

self.launch()
}
}
6 changes: 4 additions & 2 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 77;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -180,14 +180,14 @@
};
};
buildConfigurationList = 2F6D138D28F5F384007C25D6 /* Build configuration list for PBXProject "UITests" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 2F6D138928F5F384007C25D6;
preferredProjectObjectVersion = 77;
productRefGroup = 2F6D139328F5F384007C25D6 /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -307,6 +307,7 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 6.0;
XROS_DEPLOYMENT_TARGET = 1.0;
};
name = Debug;
Expand Down Expand Up @@ -362,6 +363,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 6.0;
VALIDATE_PRODUCT = YES;
XROS_DEPLOYMENT_TARGET = 1.0;
};
Expand Down

0 comments on commit 5379d70

Please sign in to comment.