Skip to content

Commit fe2f429

Browse files
committed
fix(alert): align Apple label choices with ranked policy
1 parent a661e43 commit fe2f429

9 files changed

Lines changed: 524 additions & 55 deletions

File tree

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAlertButtonChoice.swift‎

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import Foundation
33
/// Which of an alert's buttons an `accept` or `dismiss` activates, decided on the button labels
44
/// alone, so the rule is checkable without a screen.
55
///
6-
/// `dismiss` activates only a label `isDismissButton` recognizes. An unknown decline cannot be
6+
/// Earlier label preferences win; equal-rank buttons refuse rather than use their order.
7+
/// `dismiss` activates only a recognized decline label. An unknown decline cannot be
78
/// identified from labels or button position: iOS localizes prompts unevenly, so one prompt can
89
/// pair a localized decline with a still-English affirmative, and a positional guess would tap
910
/// that affirmative and record an authorization (#3627). An unrecognized decline is therefore
@@ -14,49 +15,33 @@ import Foundation
1415
/// `dismiss` take the last actionable element of that window. A consent prompt can never carry
1516
/// that marker.
1617
enum RunnerAlertButtonChoice {
18+
private static let acceptPreferences = [
19+
"^allow while using (?:the )?app$", "^while using the app$", "^allow all$",
20+
"^always allow$", "^allow all the time$", "^allow notifications$", "^allow$",
21+
"^ok$", "^yes$", "^continue$", "^save$", "^confirm", "^turn on$",
22+
"^open settings$", "^done$", "^open$", "^allow once$", "^only this time$"
23+
]
24+
private static let dismissPreferences = [
25+
"^cancel$", "^deny$", "^don.?t allow$", "^not now$", "^no$", "^dismiss$",
26+
"^close$", "^close app$", "^later$", "^skip$", "^ask app not to track$", "^keep browsing$"
27+
]
28+
1729
static func buttonIndex(for labels: [String], action: String, popupMarkerSurface: Bool = false) -> Int? {
18-
if action == "accept" {
19-
if let accept = labels.firstIndex(where: { isAcceptButton($0) }) {
20-
return accept
21-
}
22-
// A lone button is the only way out of the prompt, so accept takes it unless it declines.
23-
return labels.count == 1 && !isDismissButton(labels[0]) ? 0 : nil
24-
}
25-
if let decline = labels.firstIndex(where: { isDismissButton($0) }) {
26-
return decline
30+
let preferences = action == "accept" ? acceptPreferences : dismissPreferences
31+
for pattern in preferences {
32+
let matchingIndices = labels.indices.filter { matches(labels[$0], pattern: pattern) }
33+
if matchingIndices.count > 1 { return nil }
34+
if let index = matchingIndices.first { return index }
2735
}
28-
return popupMarkerSurface ? labels.indices.last : nil
36+
return action != "accept" && popupMarkerSurface ? labels.indices.last : nil
2937
}
3038

3139
static func isAcceptButton(_ label: String) -> Bool {
32-
let normalized = normalize(label)
33-
return [
34-
"ok",
35-
"allow",
36-
"yes",
37-
"continue",
38-
"done",
39-
"open",
40-
"open settings"
41-
].contains(normalized) || normalized.hasPrefix("confirm")
42-
}
43-
44-
static func isDismissButton(_ label: String) -> Bool {
45-
[
46-
"cancel",
47-
"close",
48-
"dismiss",
49-
"don't allow",
50-
"don’t allow",
51-
"not now",
52-
"no",
53-
"keep browsing",
54-
"later",
55-
"ask app not to track"
56-
].contains(normalize(label))
40+
acceptPreferences.contains { matches(label, pattern: $0) }
5741
}
5842

59-
private static func normalize(_ label: String) -> String {
60-
label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
43+
private static func matches(_ label: String, pattern: String) -> Bool {
44+
label.trimmingCharacters(in: .whitespacesAndNewlines)
45+
.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil
6146
}
6247
}

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+AlertButtonChoiceTests.swift‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,43 @@
11
import XCTest
22

33
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
4+
private struct AlertButtonChoiceFixture: Decodable {
5+
let name: String
6+
let labels: [String]
7+
let action: String
8+
let expected: Int?
9+
let xctestPopupMarkerExpected: Int?
10+
let xctestPopupMarker: Bool?
11+
}
12+
413
extension RunnerTests {
14+
func testAlertButtonChoicesMatchGoldenFixture() throws {
15+
let fixtureURL = URL(fileURLWithPath: #filePath)
16+
.deletingLastPathComponent()
17+
.deletingLastPathComponent()
18+
.deletingLastPathComponent()
19+
.deletingLastPathComponent()
20+
.deletingLastPathComponent()
21+
.deletingLastPathComponent()
22+
.appendingPathComponent("contracts/fixtures/ios-alert-button-choice.json")
23+
let fixtures = try JSONDecoder().decode([AlertButtonChoiceFixture].self, from: Data(contentsOf: fixtureURL))
24+
XCTAssertFalse(fixtures.isEmpty)
25+
for fixture in fixtures {
26+
XCTAssertEqual(
27+
RunnerAlertButtonChoice.buttonIndex(for: fixture.labels, action: fixture.action),
28+
fixture.expected, fixture.name
29+
)
30+
if fixture.xctestPopupMarker == true {
31+
XCTAssertEqual(
32+
RunnerAlertButtonChoice.buttonIndex(
33+
for: fixture.labels, action: fixture.action, popupMarkerSurface: true
34+
),
35+
fixture.xctestPopupMarkerExpected, fixture.name
36+
)
37+
}
38+
}
39+
}
40+
541
func testAlertAcceptTreatsOpenAsAffirmative() {
642
XCTAssertTrue(RunnerAlertButtonChoice.isAcceptButton("Open"))
743
}

0 commit comments

Comments
 (0)