Skip to content

Instantly share code, notes, and snippets.

@BrentMifsud
Last active February 16, 2023 02:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BrentMifsud/1c7232fb95c47463a037e73fecf09c80 to your computer and use it in GitHub Desktop.
Save BrentMifsud/1c7232fb95c47463a037e73fecf09c80 to your computer and use it in GitHub Desktop.
Xcode UI Testing Helpers
import XCTest
/// Bundle identifiers for apples native apps. Used to open other apps during UI testing.
///
/// More can be found here: https://support.apple.com/en-ca/guide/deployment/depece748c41/web
enum AppleBundleIdentifiers: String {
case safari = "com.apple.mobilesafari"
case springboard = "com.apple.springboard"
}
extension XCUIApplication {
convenience init(appleBundleID: AppleBundleIdentifiers) {
self.init(bundleIdentifier: appleBundleID.rawValue)
}
}
extension XCTestCase {
/// Waits for the existence of a ui element before performing some action.
/// - Parameters:
/// - element: the `XCUIElement` to wait for
/// - timeout: The `TimeInterval` to wait for the `XCUIElement` existence
/// - optional: Whether the XCUIElement is optional or not. Use this for views that may or may not appear.
/// - onElementFound: closure that runs if the element is found
/// - onRequiredElementMissing: closure that runs when a non-optional element is not found.
func waitForExistance(
of element: XCUIElement,
timeout: TimeInterval = 2,
optional: Bool,
onElementFound: (XCUIElement) -> Void = { _ in },
onRequiredElementMissing: (() -> Void)? = nil
) {
if element.waitForExistence(timeout: timeout) {
onElementFound(element)
} else if !optional {
XCTFail("Element: \(element) did not become visible in provided time.")
onRequiredElementMissing?()
}
}
/// ui interruption monitors are super unreliable for picking up alerts. Use springboard to intercept the alert.
///
/// This method returns the monitor so that it can be safely disposed of after using `removeUIInterruptionMonitor(monitor:)`
/// - Parameters:
/// - alertPredicate: an NSPredicate for finding the alert dialog.
/// - timeout: the `TimeInterval` to wait for the alert to appear
/// - optional: Is the alert optional
/// - alertTrigger: the action that will trigger the alert to pop-up
/// - alertHandler: what to do with the alert
func handleSystemAlert(
alertPredicate: NSPredicate,
timeout: TimeInterval = 2,
optional: Bool,
alertTrigger: () -> Void,
alertHandler: (XCUIElement) -> Void
) {
alertTrigger()
let systemAlert = Springboard.springboard.alerts.matching(alertPredicate).element
if systemAlert.waitForExistence(timeout: timeout) {
alertHandler(systemAlert)
} else if !optional {
XCTFail("""
Expected system alert with predicate: "\(alertPredicate.predicateFormat)" did not become visible within provided timeout
""")
}
}
/// Take a screenshot and add it as an attachement to the test case
/// - Parameter name: description to append ot the name of the screenshot
func takeScreenshot(named name: String) {
// Take the screenshot
let fullScreenshot = XCUIScreen.main.screenshot()
// Create a new attachment to save our screenshot
// and give it a name consisting of the "named"
// parameter and the device name, so we can find
// it later.
let screenshotAttachment = XCTAttachment(
uniformTypeIdentifier: "public.png",
name: "\(name)-\(UUID()).png",
payload: fullScreenshot.pngRepresentation,
userInfo: nil
)
// Usually Xcode will delete attachments after
// the test has run; we don't want that!
screenshotAttachment.lifetime = .keepAlways
// Add the attachment to the test log,
// so we can retrieve it later
add(screenshotAttachment)
}
/// Utilize safari to open a deeplink during a UI Test
func open(deepLink urlString: String, for app: XCUIApplication) {
openFromSafari("\(urlString)")
XCTAssert(app.wait(for: .runningForeground, timeout: 5))
}
private func openFromSafari(_ urlString: String) {
let safari = XCUIApplication(appleBundleID: .safari)
safari.launch()
// Make sure Safari is really running before asserting
XCTAssert(safari.wait(for: .runningForeground, timeout: 5))
// Type the deeplink and execute it
let firstLaunchContinueButton = safari.buttons["Continue"]
if firstLaunchContinueButton.exists {
firstLaunchContinueButton.tap()
}
safari.textFields["Address"].tap()
let keyboardTutorialButton = safari.buttons["Continue"]
if keyboardTutorialButton.exists {
keyboardTutorialButton.tap()
}
safari.typeText(urlString)
safari.buttons["go"].tap()
let openButton = safari.buttons["Open"]
_ = openButton.waitForExistence(timeout: 2)
if openButton.exists {
openButton.tap()
}
}
}
extension XCUIElement {
func labelContains(text: String) -> Bool {
let predicate = NSPredicate(format: "label CONTAINS %@", text)
return staticTexts.matching(predicate).firstMatch.exists
}
func clearText() {
guard let stringValue = self.value as? String else {
return
}
var deleteString = String()
for _ in stringValue {
deleteString += XCUIKeyboardKey.delete.rawValue
}
typeText(deleteString)
}
enum ScrollDirection {
case up
case down
case left
case right
}
/// Scrolls to a given `XCUIElement` and fails if it is not visible within the provided timeout.
/// - Parameters:
/// - direction: the direction to scroll
/// - element: the element to find
/// - timeout: how long to scroll before giving up
func scrollToElement(scrollDirection direction: ScrollDirection = .down, element: XCUIElement, timeout: TimeInterval = 5) {
let timeOutDate = Date() + timeout
while !element.visible() && Date() < timeOutDate {
switch direction {
case .down:
swipeUp()
case .up:
swipeDown()
case .left:
swipeRight()
case .right:
swipeLeft()
}
}
if !element.visible() {
XCTFail("Scrolling to element timed out. Element not visible.")
}
}
/// Returns whether the `XCUIElement` is visible on screen
func visible() -> Bool {
guard self.exists && !self.frame.isEmpty else {
return false
}
return XCUIApplication().windows.element(boundBy: 0).frame.contains(self.frame)
}
}
/// A singleton representing iOS's homescreen application.
///
/// Can be used to perform automated tasks outside of your own app.
class Springboard {
static let springboard = XCUIApplication(appleBundleID: .springboard)
private init() {}
/// Delete the app via springboard
/// - Parameter appName: the name of your app as seen underneath your app icon on the home screen
///
/// Note: Tested only on iOS 16. It SHOULD work on iOS 15 and potentially iOS 14 as well
class func deleteApp(named appName: String) {
XCUIApplication().terminate()
let appIcon = springboard.icons[appName]
if appIcon.waitForExistence(timeout: 1) {
// long press the app icon to reveal the context menu
appIcon.press(forDuration: 1.3)
// tap the remove app button from the context menu
springboard.buttons["Remove App"].tap()
// tap delete app button after the alert appears
let deleteAppButton = springboard.alerts.buttons["Delete App"]
if deleteAppButton.waitForExistence(timeout: 1) {
deleteAppButton.tap()
} else {
fatalError("Failed to delete app. Could not find Delete App Button")
}
// tap confirm delete button after the alert appears
let confirmDeleteButton = springboard.alerts.buttons["Delete"]
if confirmDeleteButton.waitForExistence(timeout: 1) {
confirmDeleteButton.tap()
} else {
fatalError("Failed to delete app. Could not find confirm deletion button")
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment