55

我正在使用新的 Xcode 7 UI 测试功能编写 UI 测试用例。在我的应用程序的某个时刻,我要求用户授予相机访问权限和推送通知。所以会出现两个 iOS 弹出窗口:"MyApp Would Like to Access the Camera"popup 和"MyApp Would Like to Send You Notifications"popup。我希望我的测试消除两个弹出窗口。

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 帖子有关:Xcode7 | Xcode UI 测试 | 如何处理位置服务警报?

提前致谢。

4

10 回答 10

50

Xcode 7.1

Xcode 7.1 终于解决了系统警报的问题。然而,有两个小问题。

首先,您需要在显示警报之前设置“UI 中断处理程序”。这是我们告诉框架如何处理出现的警报的方式。

其次,在呈现警报后,您必须与界面进行交互。只需点击应用程序就可以了,但这是必需的。

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

“位置对话框”只是帮助开发人员识别访问了哪个处理程序的字符串,它不特定于警报类型。

我相信true从处理程序返回将其标记为“完成”,这意味着它不会再次被调用。对于您的情况,我会尝试返回false,以便第二个警报将再次触发处理程序。

Xcode 7.0

以下将关闭 Xcode 7 Beta 6 中的单个“系统警报”:

let app = XCUIApplication()
app.launch()
// trigger location permission dialog

app.alerts.element.collectionViews.buttons["Allow"].tap()

Beta 6 为 UI 测试引入了一系列修复,我相信这就是其中之一。

另请注意,我是-element直接调用-alerts. 调用-elementanXCUIElementQuery会强制框架选择屏幕上的“唯一”匹配元素。这对于您一次只能看到一个的警报非常有用。但是,如果您为一个标签尝试此操作并且有两个标签,则框架将引发异常。

于 2015-08-26T13:29:17.220 回答
3

目标 - 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"];
}

迅速

addUIInterruptionMonitorWithDescription("Description") { (alert) -> Bool in
    alert.buttons["Allow"].tap()
    alert.buttons["OK"].tap()
    return true
}
于 2017-03-15T13:08:24.293 回答
3

天哪。即使我故意说点击“允许”,它总是点击“不允许”

至少

if app.alerts.element.collectionViews.buttons["Allow"].exists {
    app.tap()
}

允许我继续前进并进行其他测试。

于 2016-10-20T07:53:24.290 回答
2

对于那些正在寻找特定系统对话框的特定描述的人(就像我所做的那样),没有:) 该字符串仅用于测试人员跟踪目的。相关苹果文档链接:https ://developer.apple.com/documentation/xctest/xctestcase/1496273-adduiinterruptionmonitor


更新:xcode 9.2

该方法有时触发有时不触发。对我来说最好的解决方法是当我知道会有系统警报时,我补充说:

sleep(2)
app.tap()

并且系统警报消失了

于 2018-02-27T14:42:38.897 回答
1

我发现唯一能可靠解决此问题的是设置两个单独的测试来处理警报。在第一次测试中,我打电话app.tap(),什么也不做。在第二个测试中,我app.tap()再次调用,然后做真正的工作。

于 2016-10-17T23:30:53.847 回答
1

上帝!我讨厌 XCTest 如何在处理 UIView 警报时遇到最糟糕的情况。我有一个应用程序,我收到 2 个警报,第一个警报要我选择“允许”以启用应用程序权限的位置服务,然后在启动页面上,用户必须按下一个名为“打开位置”的 UIButton,最后有一个UIViewAlert 中的通知短信警报,用户必须选择“确定”。我们遇到的问题是无法与系统警报交互,而且是一种竞争条件,其中行为及其在屏幕上的出现不合时宜。看来,如果你使用alert.element.buttons["whateverText"].tapXCTest 的逻辑是一直按下直到测试时间用完。所以基本上一直按屏幕上的任何东西,直到所有系统警报都清晰可见。

这是一个黑客,但这对我有用。

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。

于 2016-08-04T06:55:38.987 回答
0

@Joe Masilotti 的回答是正确的,谢谢,它帮了我很多:)

我只想指出一件事,那就是UIInterruptionMonitor捕获了TOGETHER系列中呈现的所有系统警报,因此您在完成处理程序中应用的操作将应用于每个警报(“不允许”“确定” " )。如果您想以不同的方式处理警报操作,您必须在完成处理程序中检查当前呈现的是哪个警报,例如通过检查其静态文本,然后该操作将仅应用于该警报。

下面是一个小代码片段,用于在第二个警报上应用“不允许”操作,在三个警报系列中,对其余两个警报应用“确定”操作:

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()
于 2018-04-11T14:08:59.177 回答
0

在 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
于 2017-12-20T13:42:22.860 回答
0

这是一个老问题,但现在有另一种方法来处理这些警报。

系统警报无法从您启动的应用程序的应用程序上下文中访问,但您仍然可以访问应用程序上下文。看这个简单的例子:

func testLoginHappyPath() {
    let app = XCUIApplication()
    app.textFields["Username"].typeText["Billy"]
    app.secureTextFields["Password"].typeText["hunter2"]
    app.buttons["Log In"].tap()
}

在已经启动模拟器并且已经授予或拒绝权限的真空中,这将起作用。但是如果我们把它放在一个 CI 管道中,它会得到一个全新的模拟器,突然之间它就无法找到那个 Username 字段,因为会弹出一个通知警报。

所以现在有 3 种选择来处理这个问题:

隐式

已经有一个默认的系统警报中断处理程序。所以理论上,简单地尝试在第一个字段上键入文本应该检查中断事件并肯定地处理它。

如果一切都按设计进行,您将不必编写任何代码,但您会看到日志中记录和处理了中断,并且您的测试将多花几秒钟时间。

显式通过中断监视器

我不会重写之前的工作,但这是您明确设置中断监视器来处理弹出的特定警报的地方 - 或您希望发生的任何警报。

如果内置处理程序没有执行您想要的操作 - 或者根本不工作,这将很有用。

显式通过 XCUITest 框架

XCUIApplication()在 xCode 9.0 及更高版本中,您可以通过简单地定义多个实例来流畅地在应用程序上下文之间切换。然后您可以通过熟悉的方法找到您需要的字段。因此,明确地执行此操作将如下所示:

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()
}
于 2020-08-20T23:14:15.013 回答
-1

听起来像您所说的实现相机访问和通知的方法是线程化的,但没有进行物理管理,并且在何时以及如何显示它们时留有机会。

我怀疑一个是由另一个触发的,当以编程方式单击它时,它也会清除另一个(Apple 可能永远不会允许)

想想看,您是在请求用户许可,然后代表他们做出决定?为什么?因为你可能无法让你的代码工作。

如何修复 - 跟踪这两个组件在哪里触发弹出对话框 - 它们在哪里被调用?,重写以仅触发一个,当一个对话完成时发送一个 NSNotification 以触发并显示剩余的一个。

我会严重劝阻以编程方式单击用户专用的对话按钮的方法。

于 2015-08-24T13:53:52.203 回答