Xcode 7 UI 测试:如何在代码中消除一系列系统警报
Xcode 7 UI Testing: how to dismiss a series of system alerts in code
我正在使用新的 Xcode 7 UI 测试功能编写 UI 测试用例。在我的应用程序的某个时刻,我请求用户允许访问相机和推送通知。因此将显示两个 iOS 弹出窗口:"MyApp Would Like to Access the Camera"
弹出窗口和 "MyApp Would Like to Send You Notifications"
弹出窗口。我希望我的测试能够关闭两个弹出窗口。
UI 录音为我生成了以下代码:
[app.alerts[@"cameraAccessTitle"].collectionViews.buttons[@"OK"] tap];
然而,[app.alerts[@"cameraAccessTitle"] exists]
解析为 false,上面的代码生成错误:Assertion Failure: UI Testing Failure - Failure getting refresh snapshot Error Domain=XCTestManagerErrorDomain Code=13 "Error copying attributes -25202"
。
那么在测试中消除一堆系统警报的最佳方法是什么?系统弹出窗口中断了我的应用程序流程并立即使我的正常 UI 测试用例失败。事实上,任何关于我如何绕过系统警报以便我可以恢复测试通常流程的建议都值得赞赏。
这个问题可能与这个 SO post 有关,它也没有答案:
提前致谢。
听起来像您所说的实现摄像头访问和通知的方法是线程化的,但不是物理管理的,而是随机显示它们的时间和方式。
我怀疑一个是由另一个触发的,当以编程方式单击它时,它也会清除另一个(Apple 可能永远不会允许)
想想你是在征求用户许可然后代表他们做出决定?为什么?因为你可能无法让你的代码工作。
如何修复 - 跟踪这两个组件触发弹出对话框的位置 - 它们在哪里被调用?重写以仅触发一个,在完成一个对话时发送 NSNotification 以触发并显示剩余的对话.
我会严重反对以编程方式单击为用户设计的对话按钮的方法。
Xcode 7.1
Xcode 7.1 终于解决了系统提示的问题。但是,有两个小陷阱。
首先,您需要在显示警报之前设置 "UI Interuption Handler"。这是我们告诉框架如何处理警报出现的方式。
其次,在显示警报后,您必须与界面进行交互。只需点击该应用程序即可正常工作,但这是必需的。
addUIInterruptionMonitorWithDescription("Location Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Request Location"].tap()
app.tap() // need to interact with the app for the handler to fire
"Location Dialog" 只是一个字符串,用于帮助开发人员识别访问了哪个处理程序,它并不特定于警报类型。
我相信从处理程序返回 true
会将其标记为 "complete",这意味着它不会被再次调用。对于您的情况,我会尝试返回 false
,这样第二个警报将再次触发处理程序。
Xcode 7.0
以下将取消 Xcode 7 Beta 6 中的单个 "system alert":
let app = XCUIApplication()
app.launch()
// trigger location permission dialog
app.alerts.element.collectionViews.buttons["Allow"].tap()
Beta 6 引入了一系列针对 UI 测试的修复,我相信这就是其中之一。
另请注意,我是直接在 -alerts
上调用 -element
。在 XCUIElementQuery
上调用 -element
会强制框架选择屏幕上的 "one and only" 匹配元素。这对于您一次只能看到一个的警报非常有用。但是,如果您对一个标签尝试此操作并且有两个标签,则框架将引发异常。
天哪!我讨厌 XCTest 处理 UIView 警报的最糟糕时间。我有一个应用程序,我收到 2 个警报,第一个要我 select“允许”以启用应用程序权限的位置服务,然后在启动页面上,用户必须按下一个名为“打开位置”的 UIButton 和最后在 UIViewAlert 中有一个通知短信警报,用户必须 select“确定”。我们遇到的问题是无法与系统警报进行交互,而且还有一种竞争条件,即行为及其在屏幕上的出现不合时宜。似乎如果你使用 alert.element.buttons["whateverText"].tap
XCTest 的逻辑是一直按下直到测试时间用完。所以基本上一直按屏幕上的任何东西,直到所有系统警报都消失了。
这是一个 hack,但这对我有用。
func testGetPastTheStupidAlerts() {
let app = XCUIApplication()
app.launch()
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
app.buttons["TURN ON MY LOCATION"].tap()
}
字符串“Allow”被完全忽略,app.tap()
的逻辑在每次出现警报时都被调用,最后我想要到达的按钮 ["Turn On Location"] 可以访问并且测试通过
~一头雾水,谢谢 Apple。
我发现唯一可靠地解决这个问题的方法是设置两个单独的测试来处理警报。在第一个测试中,我调用 app.tap()
并且什么都不做。在第二次测试中,我再次调用 app.tap()
然后进行真正的工作。
天哪。
它总是点击 "Don't Allow" 即使我故意说点击 "Allow"
至少
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
允许我继续进行其他测试。
Objective - C
-(void) registerHandlerforDescription: (NSString*) description {
[self addUIInterruptionMonitorWithDescription:description handler:^BOOL(XCUIElement * _Nonnull interruptingElement) {
XCUIElement *element = interruptingElement;
XCUIElement *allow = element.buttons[@"Allow"];
XCUIElement *ok = element.buttons[@"OK"];
if ([ok exists]) {
[ok tap];
return YES;
}
if ([allow exists]) {
[allow tap];
return YES;
}
return NO;
}];
}
-(void)setUp {
[super setUp];
self.continueAfterFailure = NO;
self.app = [[XCUIApplication alloc] init];
[self.app launch];
[self registerHandlerforDescription:@"“MyApp” would like to make data available to nearby Bluetooth devices even when you're not using app."];
[self registerHandlerforDescription:@"“MyApp” Would Like to Access Your Photos"];
[self registerHandlerforDescription:@"“MyApp” Would Like to Access the Camera"];
}
Swift
addUIInterruptionMonitorWithDescription("Description") { (alert) -> Bool in
alert.buttons["Allow"].tap()
alert.buttons["OK"].tap()
return true
}
在 xcode 9.1 上,仅当测试设备具有 iOS 11 时才会处理警报。不适用于旧的 iOS 版本,例如 10.3 等。参考:https://forums.developer.apple.com/thread/86989
要处理警报,请使用:
//Use this before the alerts appear. I am doing it before app.launch()
let allowButtonPredicate = NSPredicate(format: "label == 'Always Allow' || label == 'Allow'")
//1st alert
_ = addUIInterruptionMonitor(withDescription: "Allow to access your location?") { (alert) -> Bool in
let alwaysAllowButton = alert.buttons.matching(allowButtonPredicate).element.firstMatch
if alwaysAllowButton.exists {
alwaysAllowButton.tap()
return true
}
return false
}
//Copy paste if there are more than one alerts to handle in the app
对于那些正在寻找特定系统对话框的特定描述的人(就像我一样)有 none :) 该字符串仅用于测试人员跟踪目的。相关苹果文档 link : https://developer.apple.com/documentation/xctest/xctestcase/1496273-adduiinterruptionmonitor
更新:xcode9.2
该方法有时触发有时不触发。对我来说最好的解决方法是当我知道会有系统警报时,我添加:
sleep(2)
app.tap()
系统警报消失
@Joe Masilotti 的回答是正确的,谢谢,它对我帮助很大:)
我只想指出一件事,那就是 UIInterruptionMonitor 捕获 all 系列系统警报 TOGETHER,以便您在完成处理程序中应用的操作应用于每个警报("Don't allow" 或 "OK")。如果您想以不同的方式处理警报操作,则必须在完成处理程序内部检查当前显示的是哪个警报,例如通过检查其静态文本,然后操作将仅应用于该警报。
这是对第二个警报应用 "Don't allow" 操作的小代码片段,在三个警报系列中,"OK" 对剩余两个的操作:
addUIInterruptionMonitor(withDescription: "Access to sound recording") { (alert) -> Bool in
if alert.staticTexts["MyApp would like to use your microphone for recording your sound."].exists {
alert.buttons["Don’t Allow"].tap()
} else {
alert.buttons["OK"].tap()
}
return true
}
app.tap()
这是一个老问题,但现在有另一种方法来处理这些警报。
无法从您启动的应用程序的 应用程序上下文 访问系统警报,但是您仍然可以访问应用程序上下文。看这个简单的例子:
func testLoginHappyPath() {
let app = XCUIApplication()
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}
在模拟器已经启动并且权限已经被授予或拒绝的情况下,这将起作用。但是如果我们把它放在一个 CI 管道中,在那里它会得到一个全新的模拟器,它会突然无法找到该用户名字段,因为会弹出一个通知警报。
所以现在有 3 个选择来处理这个问题:
隐式
已经有一个默认的系统警报中断处理程序。因此,理论上,只需尝试在第一个字段上输入文本,就应该检查中断事件并以肯定的方式处理它。
如果一切按设计运行,您将不必编写任何代码,但您会在日志中看到记录和处理的中断,并且您的测试将多花几秒钟。
通过中断监视器显式
我不会重写之前关于此的工作,但这是您明确设置中断监视器以处理弹出的特定警报的地方 - 或者您期望发生的任何警报。
如果 built-in 处理程序不执行您想要的操作 - 或者根本不工作,这将很有用。
通过 XCUITest 框架显式
在 xCode 9.0 及更高版本中,您可以通过简单地定义多个 XCUIApplication()
实例在应用上下文之间流畅地切换。然后您可以通过熟悉的方法定位到您需要的字段。因此,明确地执行此操作将如下所示:
func testLoginHappyPath() {
let app = XCUIApplication()
let springboardApp = XCUIApplication(bundleidentifier: "com.apple.springboard")
if springboardApp.alerts[""FunHappyApp" would like permission to own your soul."].exists {
springboardApp.alerts.buttons["Allow"].tap()
}
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}
我正在使用新的 Xcode 7 UI 测试功能编写 UI 测试用例。在我的应用程序的某个时刻,我请求用户允许访问相机和推送通知。因此将显示两个 iOS 弹出窗口:"MyApp Would Like to Access the Camera"
弹出窗口和 "MyApp Would Like to Send You Notifications"
弹出窗口。我希望我的测试能够关闭两个弹出窗口。
UI 录音为我生成了以下代码:
[app.alerts[@"cameraAccessTitle"].collectionViews.buttons[@"OK"] tap];
然而,[app.alerts[@"cameraAccessTitle"] exists]
解析为 false,上面的代码生成错误:Assertion Failure: UI Testing Failure - Failure getting refresh snapshot Error Domain=XCTestManagerErrorDomain Code=13 "Error copying attributes -25202"
。
那么在测试中消除一堆系统警报的最佳方法是什么?系统弹出窗口中断了我的应用程序流程并立即使我的正常 UI 测试用例失败。事实上,任何关于我如何绕过系统警报以便我可以恢复测试通常流程的建议都值得赞赏。
这个问题可能与这个 SO post 有关,它也没有答案:
提前致谢。
听起来像您所说的实现摄像头访问和通知的方法是线程化的,但不是物理管理的,而是随机显示它们的时间和方式。
我怀疑一个是由另一个触发的,当以编程方式单击它时,它也会清除另一个(Apple 可能永远不会允许)
想想你是在征求用户许可然后代表他们做出决定?为什么?因为你可能无法让你的代码工作。
如何修复 - 跟踪这两个组件触发弹出对话框的位置 - 它们在哪里被调用?重写以仅触发一个,在完成一个对话时发送 NSNotification 以触发并显示剩余的对话.
我会严重反对以编程方式单击为用户设计的对话按钮的方法。
Xcode 7.1
Xcode 7.1 终于解决了系统提示的问题。但是,有两个小陷阱。
首先,您需要在显示警报之前设置 "UI Interuption Handler"。这是我们告诉框架如何处理警报出现的方式。
其次,在显示警报后,您必须与界面进行交互。只需点击该应用程序即可正常工作,但这是必需的。
addUIInterruptionMonitorWithDescription("Location Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Request Location"].tap()
app.tap() // need to interact with the app for the handler to fire
"Location Dialog" 只是一个字符串,用于帮助开发人员识别访问了哪个处理程序,它并不特定于警报类型。
我相信从处理程序返回 true
会将其标记为 "complete",这意味着它不会被再次调用。对于您的情况,我会尝试返回 false
,这样第二个警报将再次触发处理程序。
Xcode 7.0
以下将取消 Xcode 7 Beta 6 中的单个 "system alert":
let app = XCUIApplication()
app.launch()
// trigger location permission dialog
app.alerts.element.collectionViews.buttons["Allow"].tap()
Beta 6 引入了一系列针对 UI 测试的修复,我相信这就是其中之一。
另请注意,我是直接在 -alerts
上调用 -element
。在 XCUIElementQuery
上调用 -element
会强制框架选择屏幕上的 "one and only" 匹配元素。这对于您一次只能看到一个的警报非常有用。但是,如果您对一个标签尝试此操作并且有两个标签,则框架将引发异常。
天哪!我讨厌 XCTest 处理 UIView 警报的最糟糕时间。我有一个应用程序,我收到 2 个警报,第一个要我 select“允许”以启用应用程序权限的位置服务,然后在启动页面上,用户必须按下一个名为“打开位置”的 UIButton 和最后在 UIViewAlert 中有一个通知短信警报,用户必须 select“确定”。我们遇到的问题是无法与系统警报进行交互,而且还有一种竞争条件,即行为及其在屏幕上的出现不合时宜。似乎如果你使用 alert.element.buttons["whateverText"].tap
XCTest 的逻辑是一直按下直到测试时间用完。所以基本上一直按屏幕上的任何东西,直到所有系统警报都消失了。
这是一个 hack,但这对我有用。
func testGetPastTheStupidAlerts() {
let app = XCUIApplication()
app.launch()
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
app.buttons["TURN ON MY LOCATION"].tap()
}
字符串“Allow”被完全忽略,app.tap()
的逻辑在每次出现警报时都被调用,最后我想要到达的按钮 ["Turn On Location"] 可以访问并且测试通过
~一头雾水,谢谢 Apple。
我发现唯一可靠地解决这个问题的方法是设置两个单独的测试来处理警报。在第一个测试中,我调用 app.tap()
并且什么都不做。在第二次测试中,我再次调用 app.tap()
然后进行真正的工作。
天哪。 它总是点击 "Don't Allow" 即使我故意说点击 "Allow"
至少
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
允许我继续进行其他测试。
Objective - C
-(void) registerHandlerforDescription: (NSString*) description {
[self addUIInterruptionMonitorWithDescription:description handler:^BOOL(XCUIElement * _Nonnull interruptingElement) {
XCUIElement *element = interruptingElement;
XCUIElement *allow = element.buttons[@"Allow"];
XCUIElement *ok = element.buttons[@"OK"];
if ([ok exists]) {
[ok tap];
return YES;
}
if ([allow exists]) {
[allow tap];
return YES;
}
return NO;
}];
}
-(void)setUp {
[super setUp];
self.continueAfterFailure = NO;
self.app = [[XCUIApplication alloc] init];
[self.app launch];
[self registerHandlerforDescription:@"“MyApp” would like to make data available to nearby Bluetooth devices even when you're not using app."];
[self registerHandlerforDescription:@"“MyApp” Would Like to Access Your Photos"];
[self registerHandlerforDescription:@"“MyApp” Would Like to Access the Camera"];
}
Swift
addUIInterruptionMonitorWithDescription("Description") { (alert) -> Bool in
alert.buttons["Allow"].tap()
alert.buttons["OK"].tap()
return true
}
在 xcode 9.1 上,仅当测试设备具有 iOS 11 时才会处理警报。不适用于旧的 iOS 版本,例如 10.3 等。参考:https://forums.developer.apple.com/thread/86989
要处理警报,请使用:
//Use this before the alerts appear. I am doing it before app.launch()
let allowButtonPredicate = NSPredicate(format: "label == 'Always Allow' || label == 'Allow'")
//1st alert
_ = addUIInterruptionMonitor(withDescription: "Allow to access your location?") { (alert) -> Bool in
let alwaysAllowButton = alert.buttons.matching(allowButtonPredicate).element.firstMatch
if alwaysAllowButton.exists {
alwaysAllowButton.tap()
return true
}
return false
}
//Copy paste if there are more than one alerts to handle in the app
对于那些正在寻找特定系统对话框的特定描述的人(就像我一样)有 none :) 该字符串仅用于测试人员跟踪目的。相关苹果文档 link : https://developer.apple.com/documentation/xctest/xctestcase/1496273-adduiinterruptionmonitor
更新:xcode9.2
该方法有时触发有时不触发。对我来说最好的解决方法是当我知道会有系统警报时,我添加:
sleep(2)
app.tap()
系统警报消失
@Joe Masilotti 的回答是正确的,谢谢,它对我帮助很大:)
我只想指出一件事,那就是 UIInterruptionMonitor 捕获 all 系列系统警报 TOGETHER,以便您在完成处理程序中应用的操作应用于每个警报("Don't allow" 或 "OK")。如果您想以不同的方式处理警报操作,则必须在完成处理程序内部检查当前显示的是哪个警报,例如通过检查其静态文本,然后操作将仅应用于该警报。
这是对第二个警报应用 "Don't allow" 操作的小代码片段,在三个警报系列中,"OK" 对剩余两个的操作:
addUIInterruptionMonitor(withDescription: "Access to sound recording") { (alert) -> Bool in
if alert.staticTexts["MyApp would like to use your microphone for recording your sound."].exists {
alert.buttons["Don’t Allow"].tap()
} else {
alert.buttons["OK"].tap()
}
return true
}
app.tap()
这是一个老问题,但现在有另一种方法来处理这些警报。
无法从您启动的应用程序的 应用程序上下文 访问系统警报,但是您仍然可以访问应用程序上下文。看这个简单的例子:
func testLoginHappyPath() {
let app = XCUIApplication()
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}
在模拟器已经启动并且权限已经被授予或拒绝的情况下,这将起作用。但是如果我们把它放在一个 CI 管道中,在那里它会得到一个全新的模拟器,它会突然无法找到该用户名字段,因为会弹出一个通知警报。
所以现在有 3 个选择来处理这个问题:
隐式
已经有一个默认的系统警报中断处理程序。因此,理论上,只需尝试在第一个字段上输入文本,就应该检查中断事件并以肯定的方式处理它。
如果一切按设计运行,您将不必编写任何代码,但您会在日志中看到记录和处理的中断,并且您的测试将多花几秒钟。
通过中断监视器显式
我不会重写之前关于此的工作,但这是您明确设置中断监视器以处理弹出的特定警报的地方 - 或者您期望发生的任何警报。
如果 built-in 处理程序不执行您想要的操作 - 或者根本不工作,这将很有用。
通过 XCUITest 框架显式
在 xCode 9.0 及更高版本中,您可以通过简单地定义多个 XCUIApplication()
实例在应用上下文之间流畅地切换。然后您可以通过熟悉的方法定位到您需要的字段。因此,明确地执行此操作将如下所示:
func testLoginHappyPath() {
let app = XCUIApplication()
let springboardApp = XCUIApplication(bundleidentifier: "com.apple.springboard")
if springboardApp.alerts[""FunHappyApp" would like permission to own your soul."].exists {
springboardApp.alerts.buttons["Allow"].tap()
}
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}