diff --git a/Package.swift b/Package.swift index a341786..7e62a34 100644 --- a/Package.swift +++ b/Package.swift @@ -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 @@ -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: [ @@ -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 { + [] + } +} diff --git a/Sources/XCTestExtensions/XCTestExtensions.docc/XCTestExtensions.md b/Sources/XCTestExtensions/XCTestExtensions.docc/XCTestExtensions.md index 015d5c1..9084b8b 100644 --- a/Sources/XCTestExtensions/XCTestExtensions.docc/XCTestExtensions.md +++ b/Sources/XCTestExtensions/XCTestExtensions.docc/XCTestExtensions.md @@ -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`` diff --git a/Sources/XCTestExtensions/XCUIApplication+DeleteAndLaunch.swift b/Sources/XCTestExtensions/XCUIApplication+DeleteAndLaunch.swift index a7a94fb..8d91e3b 100644 --- a/Sources/XCTestExtensions/XCUIApplication+DeleteAndLaunch.swift +++ b/Sources/XCTestExtensions/XCUIApplication+DeleteAndLaunch.swift @@ -28,15 +28,15 @@ 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. @@ -44,70 +44,77 @@ extension XCUIApplication { @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() } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 3d73aad..81cd900 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -180,7 +180,6 @@ }; }; buildConfigurationList = 2F6D138D28F5F384007C25D6 /* Build configuration list for PBXProject "UITests" */; - compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -188,6 +187,7 @@ Base, ); mainGroup = 2F6D138928F5F384007C25D6; + preferredProjectObjectVersion = 77; productRefGroup = 2F6D139328F5F384007C25D6 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -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; @@ -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; };