Files
Michał Pierzchała aa5b07faa5 fix: keep iOS runner hot across app closes (#700)
* fix: keep iOS runner hot across app closes

* fix: keep iOS snapshots fast after relaunch

* fix: bound compact iOS snapshots on broken AX trees

* docs: clarify iOS snapshot backend strategy

* test: update Settings replay selectors

* fix: stabilize Settings replay selectors

* fix: fall back for selector interactions

* chore: simplify flat interactive snapshot query
2026-06-06 21:26:12 +02:00

190 lines
6.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
extension RunnerTests {
struct RunnerAlert {
let root: XCUIElement
let ownerApp: XCUIApplication
let buttons: [XCUIElement]
}
func resolveAlert(app activeApp: XCUIApplication) -> RunnerAlert? {
#if !os(macOS)
if let systemModal = firstBlockingSystemModal(in: springboard) {
return runnerAlert(root: systemModal, ownerApp: springboard)
}
#endif
if let alert = firstExistingElement(in: activeApp.alerts.allElementsBoundByIndex) {
return runnerAlert(root: alert, ownerApp: activeApp)
}
if let popup = firstDismissPopupWindow(in: activeApp) {
return runnerAlert(root: popup, ownerApp: activeApp)
}
return nil
}
func handleAlert(_ alert: RunnerAlert, action: String) -> Response {
if action == "accept" || action == "dismiss" {
guard let button = chooseAlertButton(alert.buttons, action: action) else {
return Response(ok: false, error: ErrorPayload(message: "alert \(action) button not found"))
}
let outcome = activateElement(app: alert.ownerApp, element: button, action: "alert \(action)")
if let response = unsupportedResponse(for: outcome) {
return response
}
sleepFor(0.2)
if alertStillVisible(alert, actionButtonLabel: button.label) {
let frame = button.frame
if !frame.isNull && !frame.isEmpty {
let coordinateOutcome = tapAt(app: alert.ownerApp, x: frame.midX, y: frame.midY)
if let response = unsupportedResponse(for: coordinateOutcome) {
return response
}
sleepFor(0.2)
}
}
if alertStillVisible(alert, actionButtonLabel: button.label) {
return Response(
ok: false,
error: ErrorPayload(
code: "INTERACTION_FAILED",
message: "alert \(action) did not dismiss the visible alert",
hint: "The alert button was found but the system still reports the alert after tapping it."
)
)
}
return Response(ok: true, data: DataPayload(message: action == "accept" ? "accepted" : "dismissed"))
}
return Response(
ok: true,
data: DataPayload(
message: preferredAlertTitle(alert.root, buttons: alert.buttons),
items: alert.buttons.map { $0.label.trimmingCharacters(in: .whitespacesAndNewlines) }
)
)
}
private func runnerAlert(root: XCUIElement, ownerApp: XCUIApplication) -> RunnerAlert? {
let buttons = actionableElements(in: root).filter { isEnabledElement($0) }
guard !buttons.isEmpty else {
return nil
}
return RunnerAlert(root: root, ownerApp: ownerApp, buttons: buttons)
}
private func alertStillVisible(_ alert: RunnerAlert, actionButtonLabel: String) -> Bool {
guard let current = resolveAlert(app: alert.ownerApp) else {
return false
}
let previousTitle = preferredAlertTitle(alert.root, buttons: alert.buttons)
let currentTitle = preferredAlertTitle(current.root, buttons: current.buttons)
if previousTitle == currentTitle {
return true
}
let normalizedActionLabel = actionButtonLabel.trimmingCharacters(in: .whitespacesAndNewlines)
return current.buttons.contains { button in
button.label.trimmingCharacters(in: .whitespacesAndNewlines) == normalizedActionLabel
}
}
private func firstExistingElement(in elements: [XCUIElement]) -> XCUIElement? {
elements.first { isVisibleElement($0) }
}
private func firstDismissPopupWindow(in app: XCUIApplication) -> XCUIElement? {
safeElementsQuery {
app.windows.allElementsBoundByIndex
}.first { window in
if !isVisibleElement(window) { return false }
if isDismissPopupMarker(window.label) || isDismissPopupMarker(window.identifier) {
return true
}
return safeElementsQuery {
window.descendants(matching: .any).allElementsBoundByIndex
}.contains { descendant in
isDismissPopupMarker(descendant.label) || isDismissPopupMarker(descendant.identifier)
}
}
}
private func chooseAlertButton(_ buttons: [XCUIElement], action: String) -> XCUIElement? {
if action == "accept" {
if let accept = buttons.first(where: { isAcceptButton($0.label) }) {
return accept
}
return buttons.count == 1 && !isDismissButton(buttons[0].label) ? buttons[0] : nil
}
return buttons.first(where: { isDismissButton($0.label) }) ?? buttons.last
}
private func isAcceptButton(_ label: String) -> Bool {
let normalized = label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return [
"ok",
"allow",
"yes",
"continue",
"done",
"open settings"
].contains(normalized) || normalized.hasPrefix("confirm")
}
private func isDismissButton(_ label: String) -> Bool {
[
"cancel",
"close",
"dismiss",
"don't allow",
"don’t allow",
"not now",
"no",
"keep browsing",
"later"
].contains(label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased())
}
private func preferredAlertTitle(_ element: XCUIElement, buttons: [XCUIElement]) -> String {
let buttonLabels = Set(buttons.map { $0.label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() })
let descendants = element.descendants(matching: .any).allElementsBoundByIndex
for descendant in descendants {
let text = descendant.label.trimmingCharacters(in: .whitespacesAndNewlines)
if text.isEmpty ||
isGenericAlertLabel(text) ||
buttonLabels.contains(text.lowercased()) ||
descendant.elementType == .navigationBar ||
actionableTypes.contains(descendant.elementType)
{
continue
}
return text
}
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
return label.isEmpty || isGenericAlertLabel(label) ? "Alert" : label
}
private func isGenericAlertLabel(_ label: String) -> Bool {
let normalized = label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return isDismissPopupMarker(normalized) ||
normalized.hasPrefix("vertical scroll bar") ||
normalized.hasPrefix("horizontal scroll bar") ||
normalized == "tab bar"
}
private func isVisibleElement(_ element: XCUIElement) -> Bool {
element.exists && !element.frame.isNull && !element.frame.isEmpty
}
private func isEnabledElement(_ element: XCUIElement) -> Bool {
var enabled = false
_ = RunnerObjCExceptionCatcher.catchException({
enabled = element.exists && element.isEnabled
})
return enabled
}
private func isDismissPopupMarker(_ label: String) -> Bool {
label.trimmingCharacters(in: .whitespacesAndNewlines).caseInsensitiveCompare("dismiss popup") == .orderedSame
}
}