【问题标题】:Is there a way to reset the app between tests in Swift XCTest UI?有没有办法在 Swift XCTest UI 的测试之间重置应用程序?
【发布时间】:2016-01-11 12:03:56
【问题描述】:

XCTest 中是否有一个 API 调用可以放入 setUP() 或 tearDown() 以在测试之间重置应用程序?我查看了 XCUIApplication 的点语法,我看到的只是 .launch()

或者有没有办法在 Swift 中调用 shell 脚本?然后我可以在测试方法之间调用 xcrun 来重置模拟器。

【问题讨论】:

  • 有趣的是,当我写这篇文章时,我找不到这个问题。我将糟糕的查询结果归咎于 SO。任何人,请随意删除这个“骗子”,我不久前使用快速通道/gitlab-ci.yml 文件的优雅解决方案解决了这个问题。
  • 你是如何使用 gitlab-ci.yml 文件解决它的?可以分享一下吗?

标签: swift xcode7 xctest xcode-ui-testing


【解决方案1】:

此时,Xcode、Simulator 和 Swift Package Manager 提供的 public API 没有出现任何可从 setUp()tearDown() XCText 子类调用到“Reset模拟器的“内容和设置”。

还有其他可能使用公共 API 的方法:

  1. 应用程序代码。添加一些myResetApplication() 应用程序代码以将应用程序置于已知状态。但是,设备(模拟器)状态控制受到应用程序沙箱的限制……这在应用程序之外没有太大帮助。这种方法对于清除应用程序可控持久性是可以的。

  2. Shell 脚本。从 shell 脚本运行测试。在每次测试运行之间使用xcrun simctl erase allxcrun simctl uninstall <device> <app identifier> 或类似名称来重置模拟器(或卸载应用程序)see StackOverflow: "How can I reset the iOS Simulator from the command line?"

xcrun simctl --help
# Uninstall a single application
xcrun simctl uninstall --help  
xcrun simctl uninstall <device> <app identifier>

# Erase a device's contents and settings.
xcrun simctl erase <device>
xcrun simctl erase all      # all existing devices

# Grant, revoke, or reset privacy and permissions
simctl privacy <device> <action> <service> [<bundle identifier>]
  1. Xcode 架构脚本动作。将xcrun simctl erase all(或xcrun simctl erase &lt;DEVICE_UUID&gt;)或类似命令添加到Xcode Scheme 部分,例如Test 或Build 部分。选择产品 > 方案 > 编辑方案…菜单。展开方案测试部分。选择测试部分下的预操作。单击 (+) 添加“新运行脚本操作”。命令xcrun simctl erase all可以直接输入,不需要任何外部脚本。

调用的选项1。应用代码重置应用:

A. 应用程序界面[UI 测试] 提供重置按钮或其他用于重置应用程序的 UI 操作。 UI 元素可以通过XCUIApplicationXCTest 例程setUp()tearDown()testSomething() 中执行。

B. 启动参数[UI 测试] 正如 Victor Ronin 所指出的,可以从测试 setUp() 中传递一个参数 ...

class AppResetUITests: XCTestCase {

  override func setUp() {
    // ...
    let app = XCUIApplication()
    app.launchArguments = ["MY_UI_TEST_MODE"]
    app.launch()

...由AppDelegate接收...

class AppDelegate: UIResponder, UIApplicationDelegate {

  func application( …didFinishLaunchingWithOptions… ) -> Bool {
    // ...
    let args = ProcessInfo.processInfo.arguments
    if args.contains("MY_UI_TEST_MODE") {
      myResetApplication()
    }

C. Xcode 方案参数[UI 测试,单元测试] 选择 Product > Scheme > Edit Scheme... 菜单。展开方案运行部分。 (+) 添加一些参数,如MY_UI_TEST_MODE。该参数将在ProcessInfo.processInfo 中提供。

// ... in application
let args = ProcessInfo.processInfo.arguments
if args.contains("MY_UI_TEST_MODE") {
    myResetApplication()
}

D. 直接调用[单元测试] 单元测试包被注入到正在运行的应用程序中,可以直接调用应用程序中的一些myResetApplication() 例程。警告:默认单元测试在主屏幕加载后运行。 see Test Load Sequence 但是,UI 测试包作为被测应用程序外部的进程运行。因此,在单元测试中有效的方法在 UI 测试中会出现链接错误。

class AppResetUnitTests: XCTestCase {

  override func setUp() {
    // ... Unit Test: runs.  UI Test: link error.
    myResetApplication() // visible code implemented in application

【讨论】:

  • xcrun simctl erase all 是一个很棒的建议 - 谢谢!
  • 您可以在测试目标构建阶段软卸载您的应用,而不是第三种解决方案。看我的回答。
  • 看起来在 Xcode 13 中用于重置应用程序和模拟器,模拟器需要运行。在我的模式脚本中,我在xcrun simctl erase all 之前添加了killall "Simulator"。另外,为了让它在我第一次手动杀死它时工作。
  • 另外,由于之前没有提到,“提供构建设置来自”可以留给None,它正在工作,虽然我不确定这是什么意思。
【解决方案2】:

您可以要求您的应用自行“清理”

  • 您使用XCUIApplication.launchArguments 设置一些标志

  • 在 AppDelegate 中检查

    如果 NSProcessInfo.processInfo().arguments.contains("YOUR_FLAG_NAME_HERE") { // 在这里进行清理 }

【讨论】:

  • 这是我理解 launchArgruments 方法的一大步。谢谢你的洞察力。它把我带到nshipster.com/launch-arguments-and-environment-variables 请原谅我在这里的菜鸟。如果我编辑方案并创建一个启动参数,我在哪里以及如何设置新创建的参数的细节?我看到如何将它作为令牌传递给测试,但就像我的情况一样,我想运行一个重置模拟器状态的脚本。您能否对实际论证的创建给出更详细的解释?
  • @jermobileqa 首先,不需要道歉。我和你有点相似。我今天开始使用新的 UI 测试。我正在寻找如何解决这个问题。我当前在我的测试的 setUp 方法中设置 XCUIApplication.launchArguments 并在 func 应用程序的 AppDelegate 中检查它。我没有修改架构。结果,我可以使用 Command+U 从 XCode 运行测试,它将使用此参数,我的应用程序将清除它所保留的所有内容。
  • 如何重置应用权限?
【解决方案3】:

您可以在测试目标中添加“运行脚本”阶段来构建阶段,以便在对其运行单元测试之前卸载应用程序,不幸的是,这不是在测试用例之间

/usr/bin/xcrun simctl uninstall booted com.mycompany.bundleId

更新


在测试之间,您可以在 tearDown 阶段通过 Springboard 删除应用。虽然,这确实需要使用 XCTest 的私有标头。 (标头转储可从Facebook's WebDriverAgent here 获得。)

以下是 Springboard 类中的一些示例代码,用于通过长按从 Springboard 中删除应用:

#Swift 4:

import XCTest

class Springboard {

    static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
    
    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()
        
         // Force delete the app from the springboard
        let icon = springboard.icons["Citizen"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.press(forDuration: 1.3)
        
            // Tap the little "X" button at approximately where it is. The X is not exposed directly
            springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap()
        
            springboard.alerts.buttons["Delete"].tap()
        }
    }
 }

#Swift 3-:

import XCTest

class Springboard {

    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
    
    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()
        
        // Resolve the query for the springboard rather than launching it
        springboard.resolve()
        
        // Force delete the app from the springboard
        let icon = springboard.icons["MyAppName"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.pressForDuration(1.3)
            
            if #available(iOS 13.0, *) {
                springboard.buttons["Remove App"].tap()
                springboard.alerts.buttons["Delete App"].tap()
                springboard.alerts.buttons["Delete"].tap()
            } else {
            
                // Tap the little "X" button at approximately where it is. The X is not exposed directly
                let xPosition = CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX,
                                     dy: (iconFrame.minY + 3) / springboardFrame.maxY)
                springboard.coordinate(withNormalizedOffset: xPosition).tap()
                springboard.alerts.buttons["Delete"].tap()
            }
        }
    }
 }

然后:

override func tearDown() {
    Springboard.deleteMyApp()
    super.tearDown()
}

私有标头被导入到 Swift 桥接头中。您需要导入:

// Private headers from XCTest
#import "XCUIApplication.h"
#import "XCUIElement.h"

注意:从 Xcode 10 开始,XCUIApplication(bundleIdentifier:) 现已被 Apple 公开,并且 不再需要私有标头

【讨论】:

  • 很好的答案!有没有更聪明的方法来获取“MyAppName”?我尝试使用NSBundle-bundleWithIdentifier/Path,但是测试应用程序没有对应用程序包的引用。我的项目有许多目标,每个目标都有不同的名称,我希望能够在所有目标中使用 Springboard 类。
  • 小“x”按钮的可访问性标识符为“DeleteButton”,可以通过在长按后运行icon.buttons["DeleteButton"].tap()而不是使用CGVector来点击。
  • 从 iOS 13.4 开始,当我在 Springboard.deleteMyApp() 之后调用 app.launch() 时出现错误:The request was denied by service delegate (SBMainWorkspace) for reason: NotFound ("Application "com.serpentisei.studyjapanese" is unknown to FrontBoard").
  • 上述错误似乎是在Xcode 11.4中引入的,与模拟器iOS版本无关。每当您启动应用程序,使用上述技术删除应用程序,然后尝试再次启动它(即使这是跨单独的测试)时,都会发生这种情况。我已经提交了 FB7666257。
  • 不管怎样,Apple 回复了我的反馈请求,并确认此问题现已在最新版本的 Xcode 中得到修复。
【解决方案4】:

我使用了@Chase Holland answer 并按照相同的方法更新了 Springboard 类,以使用设置应用程序重置内容和设置。这在您需要重置权限对话框时很有用。

import XCTest

class Springboard {
    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
    static let settings = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.Preferences")

    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()

        // Resolve the query for the springboard rather than launching it
        springboard.resolve()

        // Force delete the app from the springboard
        let icon = springboard.icons["MyAppName"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.pressForDuration(1.3)

            // Tap the little "X" button at approximately where it is. The X is not exposed directly
            springboard.coordinateWithNormalizedOffset(CGVectorMake((iconFrame.minX + 3) / springboardFrame.maxX, (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

            springboard.alerts.buttons["Delete"].tap()

            // Press home once make the icons stop wiggling
            XCUIDevice.sharedDevice().pressButton(.Home)
            // Press home again to go to the first page of the springboard
            XCUIDevice.sharedDevice().pressButton(.Home)
            // Wait some time for the animation end
            NSThread.sleepForTimeInterval(0.5)

            let settingsIcon = springboard.icons["Settings"]
            if settingsIcon.exists {
                settingsIcon.tap()
                settings.tables.staticTexts["General"].tap()
                settings.tables.staticTexts["Reset"].tap()
                settings.tables.staticTexts["Reset Location & Privacy"].tap()
                settings.buttons["Reset Warnings"].tap()
                settings.terminate()
            }
        }
    }
}

【讨论】:

  • XCUIApplication(privateWithPath: …) 没有在 Swift 3 中公开,看起来像?
  • @buildsucceeded 你需要创建一个桥接头并导入私有头。检查我的答案是否正确。
【解决方案5】:

为 swift 3.1 / xcode 8.3 更新

在测试目标中创建桥接头:

#import <XCTest/XCUIApplication.h>
#import <XCTest/XCUIElement.h>

@interface XCUIApplication (Private)
- (id)initPrivateWithPath:(NSString *)path bundleID:(NSString *)bundleID;
- (void)resolve;
@end

更新的跳板类

class Springboard {
   static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")!
   static let settings = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.Preferences")!

/**
Terminate and delete the app via springboard
*/

class func deleteMyApp() {
   XCUIApplication().terminate()

// Resolve the query for the springboard rather than launching it

   springboard.resolve()

// Force delete the app from the springboard
   let icon = springboard.icons["{MyAppName}"] /// change to correct app name
   if icon.exists {
     let iconFrame = icon.frame
     let springboardFrame = springboard.frame
     icon.press(forDuration: 1.3)

  // Tap the little "X" button at approximately where it is. The X is not exposed directly

    springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

     springboard.alerts.buttons["Delete"].tap()

     // Press home once make the icons stop wiggling

     XCUIDevice.shared().press(.home)
     // Press home again to go to the first page of the springboard
     XCUIDevice.shared().press(.home)
     // Wait some time for the animation end
     Thread.sleep(forTimeInterval: 0.5)

      let settingsIcon = springboard.icons["Settings"]
      if settingsIcon.exists {
       settingsIcon.tap()
       settings.tables.staticTexts["General"].tap()
       settings.tables.staticTexts["Reset"].tap()
       settings.tables.staticTexts["Reset Location & Privacy"].tap()
       settings.buttons["Reset Warnings"].tap()
       settings.terminate()
      }
     }
    }
   }

【讨论】:

  • 完美运行!
  • 真的很好!完美运行
  • 在设备上运行此程序时,有时我会收到“信任这台计算机?”警报,这会阻止我的应用启动。
  • 这在最新的 Xcode/XCtest 中仍然有效吗?如果是这样,您如何/在哪里启动 deleteMyApp()?
  • ... 有效!太棒了!
【解决方案6】:

我使用了@ODManswer,但对其进行了修改以适用于 Swift 4。注意:一些 S/O 答案没有区分 Swift 版本,它们有时具有相当基本的差异。我已经在 iPhone 7 模拟器和 iPad Air 模拟器上进行了纵向测试,它适用于我的应用。

斯威夫特 4

import XCTest
import Foundation

class Springboard {

let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let settings = XCUIApplication(bundleIdentifier: "com.apple.Preferences")


/**
 Terminate and delete the app via springboard
 */
func deleteMyApp() {
    XCUIApplication().terminate()

    // Resolve the query for the springboard rather than launching it
    springboard.activate()

    // Rotate back to Portrait, just to ensure repeatability here
    XCUIDevice.shared.orientation = UIDeviceOrientation.portrait
    // Sleep to let the device finish its rotation animation, if it needed rotating
    sleep(2)

    // Force delete the app from the springboard
    // Handle iOS 11 iPad 'duplication' of icons (one nested under "Home screen icons" and the other nested under "Multitasking Dock"
    let icon = springboard.otherElements["Home screen icons"].scrollViews.otherElements.icons["YourAppName"]
    if icon.exists {
        let iconFrame = icon.frame
        let springboardFrame = springboard.frame
        icon.press(forDuration: 2.5)

        // Tap the little "X" button at approximately where it is. The X is not exposed directly
        springboard.coordinate(withNormalizedOffset: CGVector(dx: ((iconFrame.minX + 3) / springboardFrame.maxX), dy:((iconFrame.minY + 3) / springboardFrame.maxY))).tap()
        // Wait some time for the animation end
        Thread.sleep(forTimeInterval: 0.5)

        //springboard.alerts.buttons["Delete"].firstMatch.tap()
        springboard.buttons["Delete"].firstMatch.tap()

        // Press home once make the icons stop wiggling
        XCUIDevice.shared.press(.home)
        // Press home again to go to the first page of the springboard
        XCUIDevice.shared.press(.home)
        // Wait some time for the animation end
        Thread.sleep(forTimeInterval: 0.5)

        // Handle iOS 11 iPad 'duplication' of icons (one nested under "Home screen icons" and the other nested under "Multitasking Dock"
        let settingsIcon = springboard.otherElements["Home screen icons"].scrollViews.otherElements.icons["Settings"]
        if settingsIcon.exists {
            settingsIcon.tap()
            settings.tables.staticTexts["General"].tap()
            settings.tables.staticTexts["Reset"].tap()
            settings.tables.staticTexts["Reset Location & Privacy"].tap()
            // Handle iOS 11 iPad difference in error button text
            if UIDevice.current.userInterfaceIdiom == .pad {
                settings.buttons["Reset"].tap()
            }
            else {
                settings.buttons["Reset Warnings"].tap()
            }
            settings.terminate()
        }
    }
  }
}

【讨论】:

  • 我不得不进一步改变它,因为由于缩放变化,它在“Plus”型号手机上不起作用。如果将常量“3”替换为“3 * UIScreen.main.scale”,则它可以正常工作。
  • 我似乎无法让我的 iPad 按下 x 按钮。有人在 iPad 上遇到过运气吗?
  • 我能够解决这个问题,如下面的回答所示。
【解决方案7】:

对于 iOS 11 sims an up,我做了一个非常轻微的修改,点击“x”图标,我们点击了 @Code Monkey 建议的修复。修复适用于 10.3 和 11.2 电话 sims。作为记录,我使用的是 swift 3。我想我会通过一些代码来复制和粘贴以更容易地找到修复程序。 :)

import XCTest

class Springboard {

    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")

    class func deleteMyApp() {
        XCUIApplication().terminate()

        // Resolve the query for the springboard rather than launching it
        springboard!.resolve()

        // Force delete the app from the springboard
        let icon = springboard!.icons["My Test App"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard!.frame
            icon.press(forDuration: 1.3)

            springboard!.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3 * UIScreen.main.scale) / springboardFrame.maxX, dy: (iconFrame.minY + 3 * UIScreen.main.scale) / springboardFrame.maxY)).tap()

            springboard!.alerts.buttons["Delete"].tap()
        }
    }
}

【讨论】:

    【解决方案8】:

    更新 Craig Fishers 对 Swift 4 的回答。为 iPad 横向更新,可能仅适用于横向左侧。

    导入 XCTest

    类跳板{

    static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
    
    class func deleteMyApp(name: String) {        
        // Force delete the app from the springboard
        let icon = springboard.icons[name]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.press(forDuration: 2.0)
    
            var portaitOffset = 0.0 as CGFloat
            if XCUIDevice.shared.orientation != .portrait {
                portaitOffset = iconFrame.size.width - 2 * 3 * UIScreen.main.scale
            }
    
            let coord = springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + portaitOffset + 3 * UIScreen.main.scale) / springboardFrame.maxX, dy: (iconFrame.minY + 3 * UIScreen.main.scale) / springboardFrame.maxY))
            coord.tap()
    
            let _ = springboard.alerts.buttons["Delete"].waitForExistence(timeout: 5)
            springboard.alerts.buttons["Delete"].tap()
    
            XCUIDevice.shared.press(.home)
        }
    }
    

    }

    【讨论】:

      【解决方案9】:

      这似乎适用于 iOS 12.1 和模拟器

      class func deleteApp(appName: String) {
          XCUIApplication().terminate()
      
          // Force delete the app from the springboard
          let icon = springboard.icons[appName]
          if icon.exists {
              icon.press(forDuration: 2.0)
      
              icon.buttons["DeleteButton"].tap()
              sleep(2)
              springboard.alerts["Delete “\(appName)”?"].buttons["Delete"].tap()
              sleep(2)
      
              XCUIDevice.shared.press(.home)
          }
      }
      

      【讨论】:

        【解决方案10】:

        这是上述删除应用程序和重置警告的答案的 Objective C 版本(在 iOS 11 和 12 上测试):

        - (void)uninstallAppNamed:(NSString *)appName {
        
            [[[XCUIApplication alloc] init] terminate];
        
            XCUIApplication *springboard = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"];
            [springboard activate];
            XCUIElement *icon = springboard.otherElements[@"Home screen icons"].scrollViews.otherElements.icons[appName];
        
            if (icon.exists) {
                [icon pressForDuration:2.3];
                [icon.buttons[@"DeleteButton"] tap];
                sleep(2);
                [[springboard.alerts firstMatch].buttons[@"Delete"] tap];
                sleep(2);
                [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome];
                sleep(2);
            }
        }
        

        ..

        - (void)resetWarnings {
        
            XCUIApplication *settings = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.Preferences"];
            [settings activate];
            sleep(2);
            [settings.tables.staticTexts[@"General"] tap];
            [settings.tables.staticTexts[@"Reset"] tap];
            [settings.tables.staticTexts[@"Reset Location & Privacy"] tap];
        
            if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
                [settings.buttons[@"Reset"] tap];
            } else {
                [settings.buttons[@"Reset Warnings"] tap];
            }
            sleep(2);
            [settings terminate];
        }
        

        【讨论】:

          【解决方案11】:

          iOS 13.1/Swift 5.1 基于 UI 的删除

          static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")!
          
          class func deleteApp() {
              XCUIApplication().terminate()
              XCUIDevice.shared.press(.home)
              XCUIDevice.shared.press(.home)
          
              let icon = springboard.icons["YourApplication"]
              if !icon.exists { return }
          
              springboard.swipeLeft()
              springboard.activate()
              Thread.sleep(forTimeInterval: 1.0)
          
              icon.press(forDuration: 1.3)
              springboard.buttons["Rearrange Apps"].eventuallyExists().tap()
          
              icon.buttons["DeleteButton"].eventuallyExists().tap()
              springboard.alerts.buttons["Delete"].eventuallyExists().tap()
          
              XCUIDevice.shared.press(.home)
              XCUIDevice.shared.press(.home)
          }
          

          【讨论】:

          • 这对您来说可靠吗?它间歇性地为我找到“删除按钮”时遇到问题。
          • @bencallis 相同
          • 你是对的,对我来说也是片状的。我正在考虑擦除整个模拟器,直到我们找不到正确的方法。
          • 我还看到了错误的图标坐标。我是 x:-2,y:4。当它发生时 deleteButton 不存在。我尝试刷新元素树,但没有帮助。
          • 我已经更新了方法,请试试这个。现在它正在为我工​​作。但是 13.2 就在门口,我认为这将过时
          【解决方案12】:

          这适用于所有操作系统版本(iOS11、12 和 13)

          static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
          
              func deleteApp() {
                  XCUIApplication().terminate()
                  springboard.activate()
          
                  let icon = springboard.icons[appName]
          
                  if icon.exists {
                      icon.firstMatch.press(forDuration: 5)
                      icon.buttons["DeleteButton"].tap()
          
                      let deleteConfirmation = springboard.alerts["Delete “\(appName)”?"].buttons["Delete"]
                      XCTAssertTrue(deleteConfirmation.waitForExistence(timeout: 5), "Delete confirmation not shown")
                      deleteConfirmation.tap()
                  }
              }
          

          【讨论】:

            【解决方案13】:

            iOS 13.2 解决方案

            final class Springboard {
            
                private static var springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")
            
                class func deleteApp(name: String) {
                    XCUIApplication().terminate()
            
                    springboardApp.activate()
            
                    sleep(1)
            
                    let appIcon = springboardApp.icons.matching(identifier: name).firstMatch
                    appIcon.press(forDuration: 1.3)
            
                    sleep(1)
            
                    springboardApp.buttons["Delete App"].tap()
            
                    let deleteButton = springboardApp.alerts.buttons["Delete"].firstMatch
                    if deleteButton.waitForExistence(timeout: 5) {
                        deleteButton.tap()
                    }
                }
            }
            

            【讨论】:

              【解决方案14】:

              我在您的测试的setUptearDown 中看到很多卸载您的应用程序的答案。

              但您可以通过在测试目标中添加运行脚本阶段,在启动测试之前轻松卸载您的应用。

              这样做:

              1. 选择应用程序的 Xcode 项目
              2. 选择您的测试目标
              3. 选择“构建阶段”
              4. 点击“+”和“新建运行脚本阶段”

              然后,用命令替换占位符# Type a script or drag a script file from your workspace to insert its path.

              xcrun simctl boot ${TARGET_DEVICE_IDENTIFIER}
              xcrun simctl uninstall ${TARGET_DEVICE_IDENTIFIER} YOUR_APP_BUNDLE
              

              【讨论】:

              • 知道如何获取测试将在其中运行的克隆的 ID 吗?只想在其他克隆仍在运行测试时删除该克隆
              • 嗨@AlexandreG,你在说什么克隆?你说的是模拟器吗?
              • 是的,当使用 Xcode 10+ 并行测试时,测试在具有自己 ID 的模拟器克隆上运行。在其他人的帮助下,我找到了如何擦除它们stackoverflow.com/questions/52660037/…,但不知道如何在测试前确定要擦除哪个
              • 我不知道如何获得这些克隆 ID,但是 Xcode 应该创建目标模拟器的克隆,因此如果您在目标模拟器上删除应用程序,它也应该在克隆上删除。
              • 此外,如果您真的想管理克隆,您可以使用 CLI 使用 xcrun simctl create 创建自己的模拟器,然后在这些模拟器上启动您的测试,将多个目标设置为 xcodebuild test 命令。如果它不起作用,请尝试 xcodebuild test-without-building 的选项 -only-testing: 自行分离 UITests。
              【解决方案15】:

              从 Xcode 11.4 开始,如果您只想重置权限,您可以在 XCUIApplication 的实例上使用 resetAuthorizationStatus(for:),请参阅 https://developer.apple.com/documentation/xctest/xcuiapplication/3526066-resetauthorizationstatusforresou

              如果需要也可以使用simctl,引用自Xcode 11.4 Release Notes

              simctl 现在支持修改隐私权限。您可以修改隐私权限以创建已知状态以进行测试。例如,允许示例应用在没有任何提示的情况下访问照片库:
              xcrun simctl privacy &lt;device&gt; grant photos com.example.app

              要将所有权限重置为默认值,就像以前从未安装过该应用一样:
              xcrun simctl privacy &lt;device&gt; reset all com.example.app

              【讨论】:

              • 令人讨厌的是,这似乎不适用于通知权限。
              • @bencallis 嘿,你有没有发现如何在不删除应用的情况下重置通知权限?
              • 很烦人!
              【解决方案16】:

              适用于 iOS14 的工作解决方案

              final class Springboard {
              
                  private static var springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")
              
                  class func deleteApp(name: String) {
                      XCUIApplication().terminate()
              
                      springboardApp.activate()
              
                      sleep(1)
              
                      let appIcon = springboardApp.icons.matching(identifier: name).firstMatch
                      appIcon.press(forDuration: 1.3)
              
                      sleep(1)
              
                      springboardApp.buttons["Remove App"].tap()
              
                      let deleteButton = springboardApp.alerts.buttons["Delete App"].firstMatch
                      if deleteButton.waitForExistence(timeout: 5) {
                          deleteButton.tap()
                          springboardApp.alerts.buttons["Delete"].tap()
                      }
                  }
              }
              

              【讨论】:

                【解决方案17】:

                经过一些实验,我已经完成了一个涵盖不同 iOS 版本的更清晰的解决方案:

                import XCTest
                
                private enum Constants {
                  static let springboardBundleIdentifier = "com.apple.springboard"
                  static let appIconName = "Your App Name"
                  static let appIconPressShortDuration: TimeInterval = 2.0
                  static let appIconPressLongDuration: TimeInterval = 3.0
                  static let deleteAppButton = "Delete App"
                  static let removeAppButton = "Remove App"
                  static let deleteButton = "Delete"
                  static let deleteButtonVectorOffset: CGFloat = 3.0
                }
                
                final class SpringboardManager {
                
                  private static let springboard = XCUIApplication(bundleIdentifier: Constants.springboardBundleIdentifier)
                
                  static func deleteApp(_ app: XCUIApplication) {
                    if app.exists && app.isHittable {
                      XCUIDevice.shared.press(.home)
                    }
                
                    app.terminate()
                
                    self.deleteAppIfNeeded(with: Constants.appIconName)
                    sleep(1)
                  }
                
                  private static func deleteAppIfNeeded(with iconName: String) {
                    let appIcon = self.springboard.icons[iconName]
                
                    guard appIcon.exists else {
                      return
                    }
                
                    appIcon.press(forDuration: Constants.appIconPressShortDuration)
                
                    if let deleteListButton = self.deleteListButton() {
                      deleteListButton.tap()
                      self.pressDeleteAlertButtons()
                    } else {
                      appIcon.press(forDuration: Constants.appIconPressLongDuration)
                      self.pressDeleteTopLeftButton(for: appIcon)
                      self.pressDeleteAlertButtons()
                    }
                  }
                
                }
                
                private extension SpringboardManager {
                
                  static func pressDeleteAlertButtons() {
                    self.pressDeleteAlertButton(self.deleteAppAlertButton())
                    self.pressDeleteAlertButton(self.deleteAlertButton())
                  }
                
                  static func pressDeleteAlertButton(_ button: XCUIElement?) {
                    guard let button = button else {
                      return
                    }
                
                    button.tap()
                  }
                
                  static func pressDeleteTopLeftButton(for appIcon: XCUIElement) {
                    let iconFrame = appIcon.frame
                    let springboardFrame = self.springboard.frame
                
                    let deleteButtonVector = CGVector(
                      dx: (iconFrame.minX + Constants.deleteButtonVectorOffset) / springboardFrame.maxX,
                      dy: (iconFrame.minY + Constants.deleteButtonVectorOffset) / springboardFrame.maxY)
                
                    let deleteButtonCoordinate = self.springboard.coordinate(withNormalizedOffset: deleteButtonVector)
                    deleteButtonCoordinate.tap()
                  }
                
                }
                
                private extension SpringboardManager {
                
                  static func deleteListButton() -> XCUIElement? {
                    sleep(1)
                    
                    let removeListButton = self.springboard.buttons[Constants.removeAppButton]
                    let deleteListButton = self.springboard.buttons[Constants.deleteAppButton]
                
                    if removeListButton.exists {
                      return removeListButton
                    } else if deleteListButton.exists {
                      return deleteListButton
                    }
                
                    return nil
                  }
                
                  static func deleteAppAlertButton() -> XCUIElement? {
                    sleep(1)
                
                    let deleteAppButton = self.springboard.alerts.buttons[Constants.deleteAppButton]
                
                    if deleteAppButton.exists {
                      return deleteAppButton
                    }
                
                    return nil
                  }
                
                  static func deleteAlertButton() -> XCUIElement? {
                    sleep(1)
                
                    let deleteButton = self.springboard.alerts.buttons[Constants.deleteButton]
                
                    if deleteButton.exists {
                      return deleteButton
                    }
                
                    return nil
                  }
                
                }
                

                【讨论】:

                  【解决方案18】:

                  答案有很多变体,甚至不确定我是否应该补充,但万一有人需要通用解决方案:

                  iOS 14.6 和 15 测试版

                      class func deleteApp() {
                      XCUIApplication().terminate()
                      
                      // Force delete the app from the springboard
                      let icon = springboard.icons["APP_NAME"]
                      if icon.exists {
                          icon.press(forDuration: 1.3)
                          
                          springboard.buttons["Remove App"].tap()
                          springboard.alerts.buttons["Delete App"].tap()
                          springboard.alerts.buttons["Delete"].tap()
                          
                          // Press home once to make the icons stop wiggling
                          XCUIDevice.shared.press(.home)
                      }
                  }
                  

                  【讨论】:

                    猜你喜欢
                    • 2021-04-09
                    • 2022-08-23
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-07-13
                    • 2012-02-20
                    • 2011-07-03
                    相关资源
                    最近更新 更多