XCUITesting для всплывающего окна разрешения: появляется предупреждение, но UIInterruptionMonitor не срабатывает

Я хотел бы написать тест следующим образом:

Когда мое приложение переходит на определенную панель, оно должно запрашивать разрешение на использование камеры.

Я хочу проверить, появляется ли панель. Для этого я использую встроенную структуру XC UITest. Согласно тому, что я нашел в Google и здесь, похоже, я должен сделать следующее:

let dialogAppearedExpectation = expectationWithDescription("Camera Permission Dialog Appears")

addUIInterruptionMonitorWithDescription("Camera Permission Alert") { (alert) -> Bool in
    dialogAppearedExpectation.fulfill()
    return true
}

goToCameraPage()

waitForExpectationsWithTimeout(10) { (error: NSError?) -> Void in
    print("Error: \(error?.localizedDescription)")
}

Тест начался с провала, отлично. Я реализовал goToCameraPage, который правильно вызывает появление всплывающего окна «дать разрешение». Однако я ожидаю, что это вызовет монитор прерывания. Однако такое прерывание не перехватывается, и выполнение не происходит.

Я где-то читал, что вы должны сделать app.tap() после появления диалога. Однако, когда я это делаю, он нажимает кнопку «разрешить». Диалоговое окно исчезает, а прерывание по-прежнему не обрабатывается.

Есть ли способ, которым диалоги разрешений не считаются «предупреждениями» или не могут быть обработаны? Я даже вошел и заменил бит прерывания на вещь, которая просто смотрит на app.alerts, но оказывается пустой, даже когда я смотрю прямо на всплывающее окно в симуляторе.

Спасибо! Я использую Xcode7.2, симулятор iOS 9.2 для iPhone 6s.


person qqq    schedule 04.03.2016    source источник


Ответы (3)


Я тоже заметил эту проблему. Похоже, что обработчики прерываний запускаются асинхронно, и нет возможности установить, были ли они вызваны. Кроме того, ожидание ожидания, по-видимому, вообще предотвращает запуск монитора прерываний. Похоже, что система ожидает выполнения ожидания, а ожидание ждет срабатывания монитора прерываний. Классический случай тупика.

Однако я нашел довольно необычное решение, использующее ожидания на основе NSPredicate:

var didShowDialog = false
expectation(for: NSPredicate() {(_,_) in
    XCUIApplication().tap() // this is the magic tap that makes it work
    return didShowDialog
}, evaluatedWith: NSNull(), handler: nil)

addUIInterruptionMonitor(withDescription: "Camera Permission Alert") { (alert) -> Bool in
    alert.buttons.element(boundBy: 0).tap() // not sure if allow = 0 or 1
    didShowDialog = true
    return true
}

goToCameraPage()

waitForExpectations(timeout: 10) { (error: Error?) -> Void in
    print("Error: \(error?.localizedDescription)")
}

По-видимому, выполнение XCUIApplication().tap() внутри блока предиката каким-то образом позволяет запустить монитор прерываний, даже если тестовый пример ожидает ожидания.

Я надеюсь, что это работает так же хорошо для вас, как и для меня!

person pancake    schedule 02.12.2016
comment
мне не нравится этот подход, но, снова вернувшись к аналогичной проблеме, я заставил это работать. - person qqq; 25.02.2018
comment
мне тоже не нравится :D - person pancake; 27.02.2018
comment
Я заметил, что нет необходимости отслеживать диалог, закрытие монитора прерывания вызывается, как только приложение нажимается в закрытии предиката ожидания, поэтому оно может просто вернуть true сразу - person Ilya Puchka; 23.07.2018
comment
Это решение работает 50/50 для моих тестов на Xcode 11.3. - person Nikolay Krasnov; 24.02.2020

ответ блина работает, но только если приложение тестируется впервые. Если приложение ранее тестировалось в том же симуляторе, разрешение уже будет предоставлено приложению, поэтому оповещение никогда не появится, и тест завершится ошибкой.

Мой подход заключается в том, чтобы дождаться элемента, который должен появиться в приложении, а не ждать обработки диалогового окна предупреждения. Если диалоговое окно предупреждения находится над приложением, элемент приложения не будет «существовать», потому что он недоступен/недоступен.

let alertHandler = addUIInterruptionMonitor(withDescription: "Photos or Camera Permission Alert") { (alert) -> Bool in
    if alert.buttons.matching(identifier: "OK").count > 0 {
        alert.buttons["OK"].tap()
        // Required to return focus to app
        app.tap()
        return true
    } else {
        return false
    }
}

app.buttons["Change Avatar"].tap()

if !app.buttons["Use Camera"].waitForExistence(timeout: 5.0) {
    // Cause the alert handler to be invoked if the alert is currently shown.
    XCUIApplication().swipeUp()
}

_ = app.buttons["Use Camera"].waitForExistence(timeout: 2.0)

removeUIInterruptionMonitor(alertHandler)
person Robin Daugherty    schedule 21.04.2018
comment
шаг app.tap не кажется необходимым в iOS 12/Xcode 10.1 - person John M. P. Knox; 13.12.2018
comment
Это кажется необходимым в бета-версии IOS 13. - person Andrew Lombard; 31.07.2019

Так что ответ блина сработал для меня. Однако, думаю, можно упростить. Похоже, что при представлении системного предупреждения возникает какой-то странный тупик или состояние гонки.

Вместо ожидания NSPredicate я просто использовал sleep(2) после того, как должно быть представлено системное предупреждение, и перед попыткой XCUIApplication().tap().

Я также решил использовать XCUIApplication().swipeUp(), поскольку он менее вероятно будет мешать тесту.

Пример использования входа через Facebook

class LoginWithFacebookTest: XCTestCase {

    let app = XCUIApplication()

    var interruptionMonitor: NSObjectProtocol!
    let alertDescription = "“APP_NAME” Wants to Use “facebook.com” to Sign In"

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
        self.removeUIInterruptionMonitor(interruptionMonitor)
    }

    func loginWithFacebookTest() {
        app.launch()

        self.interruptionMonitor = addUIInterruptionMonitor(withDescription: self.alertDescription) { (alert) -> Bool in
            // check for a specific button
            if alert.buttons["Continue"].exists {
                alert.buttons["Continue"].tap()
                return true
            }

            return false
        }

        let loginWithFacebook = app.otherElements["login with facebook"]
        loginWithFacebook.tap()

        // Sleep to give the alert time to show up
        sleep(2)

        // Interact with the app to get the above monitor to fire
        app.swipeUp()
    }
}
person DoesData    schedule 09.08.2019