Skip to content

Commit

Permalink
improve alert handling examples
Browse files Browse the repository at this point in the history
  • Loading branch information
shamanec committed Aug 3, 2023
1 parent 37b2b99 commit 2d27aa0
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 29 deletions.
10 changes: 6 additions & 4 deletions xcuitest-sample-proj/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,22 @@ struct YourAppNameApp: App {
}

struct TabBarView: View {
@State private var selectedTab: Int = 0

var body: some View {
TabView {
TabView(selection: $selectedTab) {
CarouselView()
.tabItem {
Label("Carousel", systemImage: "star")
}
}.tag(0)
LoadingElementsView()
.tabItem {
Label("Loading", systemImage: "star")
}
}.tag(1)
CameraPermissionsRequestView()
.tabItem {
Label("Permissions", systemImage: "camera")
}
}.tag(2)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ struct CameraPermissionsRequestView: View {
Text(cameraPermissionsStatus == .allowed ? "Allowed" : "Denied")
.padding()
.accessibilityIdentifier("permission-state")
Text("")
.onAppear {
.onTapGesture {
checkCameraPermissions()
}
}
Expand Down
43 changes: 37 additions & 6 deletions xcuitest-sample-projUITests/Foundations/BaseTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ class BaseTest: XCTestCase {
// Fail-fast tests
continueAfterFailure = false

// Reset camera permissions
app.resetAuthorizationStatus(for: .camera)

guard let testName = cleanedTestName else {
XCTFail("Could not clean current test name")
return
}

if testName.contains("Permissions") {
app.terminate()
deleteApp()
}
// This can be used to delete the app for specific tests in case that is needed for the particular test scenario
// if testName.contains("Permissions") {
// app.terminate()
// deleteApp()
// }

// Start the AUT
app.launchArguments = launchArguments()
Expand Down Expand Up @@ -60,10 +64,14 @@ class BaseTest: XCTestCase {
print(app.debugDescription)
}

// When an alert or other modal UI is an expected part of the test workflow, don't write a UI interruption monitor.
// The test won’t use the monitor because the modal UI isn’t blocking the test.
// A UI test only tries its UI interruption monitors if the elements it needs to interact with to complete the test are blocked by an interruption from an unrelated UI.
// https://developer.apple.com/documentation/xctest/xctestcase/handling_ui_interruptions
/// Accept or deny camera permissions
func handleCameraAlert(allow: Bool) {
// Add UI interruption monitor to handle system alerts
addUIInterruptionMonitor(withDescription: "System Alert") { (alert) -> Bool in
addUIInterruptionMonitor(withDescription: "System Alert") { alert -> Bool in
// Check if the alert is the one you want to handle (e.g., camera permission alert)
if alert.label.contains("Camera") {
if allow {
Expand All @@ -80,13 +88,36 @@ class BaseTest: XCTestCase {
}
}

// When an alert or other modal UI is an expected part of the test workflow, don't write a UI interruption monitor.
// The test won’t use the monitor because the modal UI isn’t blocking the test.
// A UI test only tries its UI interruption monitors if the elements it needs to interact with to complete the test are blocked by an interruption from an unrelated UI.
// https://developer.apple.com/documentation/xctest/xctestcase/handling_ui_interruptions
func handleAlert(title: String, button: String) {
addUIInterruptionMonitor(withDescription: "Alert") { alert -> Bool in
var targetButton: XCUIElement
if !title.isEmpty && alert.label.contains(title) {
targetButton = alert.buttons[button]
} else {
targetButton = alert.buttons[button]
}
if targetButton.exists {
targetButton.tap()
}
return true
}
}

func handleAlert(button: String) {
handleAlert(title: "", button: button)
}

func pressHomeButton() {
device.press(.home)
}

func terminateApp() {
app.terminate()
XCTAssertEqual(app.state, .notRunning, "App was not successfully terminated")
XCTAssert(app.wait(for: .notRunning, timeout: 2), "App was not successfully terminated")
}

/// This function allows to delete the app between tests - to trigger permission alerts again for example
Expand Down
13 changes: 8 additions & 5 deletions xcuitest-sample-projUITests/Helpers/Alerts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import XCTest

class Alerts {
private static let app = BaseTest().getApp()
private static let springboard = BaseTest().getSpringboardApp()

// MARK: - App alerts handling
/// Agnostically handle application alert - wait for alert and tap the first button on it
static func handleAppAlert() {
Expand All @@ -16,13 +19,13 @@ class Alerts {

/// Handle application alert targetting specific button
static func handleAppAlert(_ button: String) {
let alert = BaseTest().getApp().alerts.firstMatch
let alert = app.alerts.firstMatch
handleAppAlert(alert, button)
}

/// Handle application alert by text displayed in title or description and also specific button label
static func handleAppAlert(_ text: String, _ button: String) {
let alert = BaseTest().getApp().alerts.firstMatch
let alert = app.alerts.firstMatch
if alert.staticTexts.element(withLabelContaining: text).exists {
handleAppAlert(alert, button)
return
Expand Down Expand Up @@ -53,13 +56,13 @@ class Alerts {

/// Handle system alert targetting specific button
static func handleSystemAlert(_ button: String) {
let alert = BaseTest().getSpringboardApp().alerts.firstMatch
let alert = springboard.alerts.firstMatch
handleSystemAlert(alert, button)
}

/// Handle system alert by text displayed in title or description and also specific button label
static func handleSystemAlert(_ text: String, _ button: String) {
let alerts = BaseTest().getSpringboardApp().alerts
let alerts = springboard.alerts
for i in 0...alerts.count {
let currentAlert = alerts.element(boundBy: i)
if currentAlert.staticTexts.element(withLabelContaining: text).exists {
Expand All @@ -72,7 +75,7 @@ class Alerts {

/// Handle system alert by element and targetting specific button
static func handleSystemAlert(_ alert: XCUIElement, _ button: String) {
XCTAssertTrue(Elements.waitForElement(alert, TestConstants.Timeout.medium), "Alert element was not found")
Elements.waitForElementExistence(alert, 5)
var alertButton: XCUIElement
if button == "" {
alertButton = alert.buttons.firstMatch
Expand Down
27 changes: 21 additions & 6 deletions xcuitest-sample-projUITests/Helpers/Elements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Elements {
XCTAssertFalse(elementVisible, "Element is still visible(exists) after \(timeoutValue) seconds")
}

/// Wait until an XCUIElementQuery has at least X number of elements
/// Wait until an XCUIElementQuery has at least X number of elements - polls each 300ms for the supplied timeout duration
///
/// - Parameters:
/// - elements: XCUIElementQuery that will be polled
Expand All @@ -96,39 +96,54 @@ class Elements {
XCTAssertTrue(result, "XCUIElementQuery was not filled with \(elementsCount) elements in \(timeoutValue) seconds")
}

func waitForElementHittableAttributeToBe(_ element: XCUIElement, _ timeoutValue: Double, _ visibility: Bool) {
/// Wait for element to exist polling each 100ms for the supplied timeout duration. Can be faster than other approaches
static func waitForElementExistence(_ element: XCUIElement, _ timeoutValue: Double) {
var result = false
let startTime = Date().timeIntervalSince1970

while (Date().timeIntervalSince1970 - startTime) < timeoutValue {
if element.exists {
result = true
break
}
usleep(100_000) // 100ms
}
XCTAssertTrue(result, "Element does not exist after \(timeoutValue) seconds")
}

static func waitForElementHittableAttributeToBe(_ element: XCUIElement, _ timeoutValue: Double, _ visibility: Bool) {
let predicate = "hittable == \(String(visibility))"
let isDisplayedPredicate = NSPredicate(format: predicate)
let expectation = [XCTNSPredicateExpectation(predicate: isDisplayedPredicate, object: element)]
let result = XCTWaiter().wait(for: expectation, timeout: timeoutValue)
XCTAssertEqual(result, .completed, "Element is not displayed and/or hittable after \(timeoutValue) seconds")
}

func waitForElementEnabledAttributeToBe(_ element: XCUIElement, _ timeoutValue: Double, _ enabledValue: Bool) {
static func waitForElementEnabledAttributeToBe(_ element: XCUIElement, _ timeoutValue: Double, _ enabledValue: Bool) {
let predicate = "isEnabled == \(String(enabledValue))"
let isEnabledPredicate = NSPredicate(format: predicate)
let expectation = [XCTNSPredicateExpectation(predicate: isEnabledPredicate, object: element)]
let result = XCTWaiter().wait(for: expectation, timeout: timeoutValue)
XCTAssertEqual(result, .completed, "Element is not enabled after \(timeoutValue) seconds")
}

func waitForElementExistenceToBe(_ element: XCUIElement, _ timeoutValue: Double, _ existence: Bool) {
static func waitForElementExistenceToBe(_ element: XCUIElement, _ timeoutValue: Double, _ existence: Bool) {
let predicate = "exists == \(String(existence))"
let existsPredicate = NSPredicate(format: predicate)
let expectation = [XCTNSPredicateExpectation(predicate: existsPredicate, object: element)]
let result = XCTWaiter().wait(for: expectation, timeout: timeoutValue)
XCTAssertEqual(result, .completed, "Element does not exist after \(timeoutValue) seconds")
}

func waitForElementLabelAttributeToBe(_ element: XCUIElement, _ timeoutValue: Double, _ labelValue: String) {
static func waitForElementLabelAttributeToBe(_ element: XCUIElement, _ timeoutValue: Double, _ labelValue: String) {
let predicate = "label == '\(String(labelValue))'"
let labelEqualsPredicate = NSPredicate(format: predicate)
let expectation = [XCTNSPredicateExpectation(predicate: labelEqualsPredicate, object: element)]
let result = XCTWaiter().wait(for: expectation, timeout: timeoutValue)
XCTAssertEqual(result, .completed, "Element label value is not '\(labelValue)' after \(timeoutValue) seconds")
}

func waitForElementValueAttributeContains(_ element: XCUIElement, _ timeoutValue: Double, _ value: String) {
static func waitForElementValueAttributeContains(_ element: XCUIElement, _ timeoutValue: Double, _ value: String) {
let valueContainsPredicate = NSPredicate(format: "value CONTAINS[c] %@", value)
let expectation = [XCTNSPredicateExpectation(predicate: valueContainsPredicate, object: element)]
let result = XCTWaiter().wait(for: expectation, timeout: timeoutValue)
Expand Down
2 changes: 1 addition & 1 deletion xcuitest-sample-projUITests/Pages/ThirdPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import XCTest

class ThirdPage: BasePage {
private var permissionState: XCUIElement { app.staticTexts["permission-state"] }
var permissionState: XCUIElement { app.staticTexts["permission-state"] }

func getPermissionState() -> String {
return permissionState.label
Expand Down
31 changes: 26 additions & 5 deletions xcuitest-sample-projUITests/SampleAppUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,45 @@ final class SampleAppUITests: BaseTest {
XCTAssertEqual(textToType, typedText)
}

func testAllowCameraPermissions() {
handleCameraAlert(allow: true)
func testAllowCameraPermissionsWithInterruptionMonitor() {
// Set up UIInterruptionMonitor
handleAlert(button: "OK")
let navigation = TabBar(app: getApp())
navigation.openThirdPage()
XCTAssertTrue(getSpringboardApp().alerts.count == 0)
let thirdPage = ThirdPage(app: getApp())
// Tap the text element, couldn't properly build triggering permissions with navigation ;D
thirdPage.permissionState.tap()
// Tap the zero coordinates of the app to trigger the interruption monitor on the alert that appeared
getApp().tapZero()
XCTAssertTrue(getSpringboardApp().alerts.count == 0)
XCTAssertEqual(thirdPage.getPermissionState(), "Allowed")
}

func testDenyCameraPermissions() {
func testDenyCameraPermissionsWithInteruptionMonitor() {
// Set up UIInterruptionMonitor
handleCameraAlert(allow: false)
let navigation = TabBar(app: getApp())
navigation.openThirdPage()
XCTAssertTrue(getSpringboardApp().alerts.count == 0)
let thirdPage = ThirdPage(app: getApp())
// Tap the text element, couldn't properly build triggering permissions with navigation ;D
thirdPage.permissionState.tap()
// Tap the zero coordinates of the app to trigger the interruption monitor on the alert that appeared
getApp().tapZero()
XCTAssertTrue(getSpringboardApp().alerts.count == 0)
XCTAssertEqual(thirdPage.getPermissionState(), "Denied")
}

func testAllowCameraPermissionsWithCustomAlertHandling() {
let navigation = TabBar(app: getApp())
navigation.openThirdPage()
let thirdPage = ThirdPage(app: getApp())
// Tap the text element, couldn't properly build triggering permissions with navigation ;D
thirdPage.permissionState.tap()
Alerts.handleSystemAlert("OK")
XCTAssertTrue(getSpringboardApp().alerts.count == 0)
XCTAssertEqual(thirdPage.getPermissionState(), "Allowed")
}

func testLaunchArgumentNotProvided() {
let firstPage = FirstPage(app: getApp())
XCTAssertEqual(firstPage.argumentText.label, "Argument:Default")
Expand Down

0 comments on commit 2d27aa0

Please sign in to comment.