How to Deal With iOS System Alerts in UI Tests
- #UI Tests
- #XCUITest
• 5 min read
XCUITest is a whole new world of attractions in UI testing. Yet, problems such as handling system alerts remain tricky regardless of the tool. In this article, I will take you through the evolution of our battle with system alerts to show how we solved this QA challenge.
Problem statement
Before we get to the problem, I must mention the hero of today's article — Gemini Photos, an app with the main functionality of scanning Photos Library and detecting similar photos.
We open Gemini Photos for the first time and, sure enough, the system asks for permission to scan our Photos Library.
This is the essence of our problem: when the application is launched, the system asks for permission to use photos which interrupts our testing process. The alert appears on every fresh app launch. Therefore, our objective is to find a way to first close the alert and then proceed with the test.
So begins our crusade against system alerts.
Running the first UI test
On the first try, we run a simple test to skip the onboarding flow on the first app launch and get straight to the purchase screen.
func testSkip() {
skipButton.tap()
waitUntilVisible(element: startPlanButton)
XCTAssertTrue(startPlanButton.exists)
}
The first test run fails: the system alert appears. We try to get into the alert's UI elements via UI Tests Recorder, but to no avail: it’s excluded from the UI elements hierarchy on the application screen.
What's the reason? Basically, Xcode doesn't recognize this type of alert in the expected test flow, blocking and interrupting it.
One more time with Interruption Monitors
This is where XCTestCase UI Interruption Monitors prove their worth. See Apple Documentation if you want to get deep into the theory behind Handling UI Interruptions.
I'll go straight to practice:
func buildSystemAlertsHandler(type: TypeOfHandle)
-> NSObjectProtocol {
addUIInterruptionMonitor(withDescription:
"System alert") { (alert) -> Bool in
let actionButton =
alert.buttons[type.rawValue]
guard actionButton.exists else { return
false }
actionButton.tap()
return true
}
}
Where:
TypeOfHandle
is the enum of consent types on the alert that requests permission to access Photos:
enum TypeOfHandle: String, CaseIterable {
case select = "Select"
case allowPhotos = "Allow Access to All Photos
case decline = "Don’t Allow"
}
-
In the
addUIInterruptionMonitor()
block, we pass XCUIElement representing the top-level UI element—the alert in our case—and returntrue
if the alert is handled andfalse
otherwise. -
To add the final touch, we call the
buildSystemAlertsHandler()
method in thesetUp()
function. This ensures that the Photos alert is handled before running any tests on the application during each test run.
The variety of handle types (i.e., select
, allowPhotos
, and decline
) enables testing for different application states. For example, we can test how Gemini Photos behaves when its main functionality is blocked or limited by a user who denies access to Photos or restricts access via “Select Photos…” We extend the TypeOfHandle
enum a bit later with the following cases: accept
, ok
, modify
, and revert.
static var yesCases: [TypeOfHandle] {
[.accept, .allowPhotos, .ok, .delete, .modify, .revert]
}
We now use .yesCases
inside the buildUnivarsalYesSystemAlertsHandler()
function whenever test preparation requires handling several system alerts in a row.
It’s all settled, isn’t it?
In what felt like a victory in an already demanding battle, we run into another issue. Our success story could have ended there, but another system alert appeared when deleting a photo.
No big deal: just apply the same buildSystemAlertsHandler()
to handle it. However, the method sits idle this time as the test flow sees no interruption.
Apparently, the delete alert is included in the test flow. The documentation cautions against using a UI interruption monitor in such cases because it will not work:
Can XCUITest handle the delete alert?
On the one hand, the XCUITest framework seems to recognize the alert as directly related to the current UI.
On the other hand, the same alert can't be accessed via standard XCUIQuery methods because it simply isn't visible in the UI elements hierarchy on the application screen.
Proving interruption
It feels like XCUITest demands more “proofs.” Thus, we must take action to give it a hint that this alert really blocks the UI. Our solution is to use tap()
or swipe()
on XCUIApplication until the UI Interruption monitor recognizes and handles the alert. To that end, we use XCTNSPredicateExpectation()
:
func confirmAlertWithExpectationUsingTap(_ app: XCUIApplication) {
var alertPresent = false
let predicate = NSPredicate(block: { evaluatedObject, _ in
let application = evaluatedObject as! XCUIApplication
application.tap()
return alertPresent
})
addUIInterruptionMonitor(withDescription: "System Alert") { (alert) -> Bool in
alert.buttons.element(boundBy: 1).tap()
alertPresent = true
return true
}
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: app)
wait(for: [expectation], timeout: 10)
}
Here, [expectation]
is fulfilled by tapping or swiping XCUIApplication till alertPresent
returns false
. But as soon as alertPresent
is true
, the UI interruption monitor gets into action by performing tap()
on the alert's button, which we specify in:
alert.buttons.element(boundBy: 1).tap()
Finally!
We have found a reliable way to handle system alerts.
Conclusions
This was the journey of how we got acquainted with system alerts in XCUITest, mastered and, to some extent, outsmarted them with the help of XCTestCase UI Interruption Monitors. Nevertheless, I am sure this journey is not over, and other challenges await.