Skip to content

Commit 6a9e275

Browse files
committed
test(ios-runner): cover alert activation against an app that never idles
Reverting the alert path to a plain `activateElement` kept every test green, so the #2546 failure had no guard: the deadline tests returned before activation, and the ones that reached it used a 10 s timeout on a fixture that had already settled. The fixture now animates without end until an alert button is answered, which is the state that made XCTest hold the event past the caller's deadline in the field. A test that reaches activation there and is given 6 s reports what actually happens: the response comes back in well under a second of activation, and if it ever reports `ALERT_DEADLINE_EXCEEDED` again the fixture says whether a button was activated behind that answer. Reverting the seam costs the run 18 s and produces exactly the original defect — `ALERT_DEADLINE_EXCEEDED` alongside `First actions: 1`. Only a `UIView` animation counts here: a repeating main-thread timer and a `CABasicAnimation` both left the app looking idle and the reverted path still passed.
1 parent 265e29e commit 6a9e275

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,46 @@ @interface AgentDeviceRunnerViewController : UIViewController
6262
@property(nonatomic, assign) NSUInteger firstAlertActions;
6363
@property(nonatomic, assign) NSUInteger replacementAlertActions;
6464
@property(nonatomic, assign) BOOL alertFixtureStarted;
65+
@property(nonatomic, strong) NSTimer *alertActivationBusyBackstop;
6566
@end
6667

6768
@implementation AgentDeviceRunnerViewController
6869

6970
#if TARGET_OS_IOS
71+
// An animation that never ends is what "busy" looks like to XCTest while it decides whether the app
72+
// may receive an event: the app keeps reporting work in flight, which is the state that cost an alert
73+
// command its whole deadline in #2546. It stops the moment an alert button is answered, since that
74+
// answer is the event the runner is trying to land, and the backstop stops it even when no answer
75+
// arrives so a regressed run finishes rather than waiting out XCTest's own timeout. A layer
76+
// animation on its own is not enough; only a UIView animation counts as in-flight work here.
77+
static NSTimeInterval const kAgentDeviceAlertActivationBusyWindow = 20.0;
78+
79+
- (void)startAlertActivationBusy {
80+
if (self.alertActivationBusyBackstop != nil) {
81+
return;
82+
}
83+
self.alertActivationBusyBackstop = [NSTimer scheduledTimerWithTimeInterval:kAgentDeviceAlertActivationBusyWindow
84+
target:self
85+
selector:@selector(stopAlertActivationBusy)
86+
userInfo:nil
87+
repeats:NO];
88+
[UIView animateWithDuration:0.4
89+
delay:0
90+
options:(UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse)
91+
animations:^{
92+
self.alertActionStatus.transform = CGAffineTransformMakeTranslation(0, 8);
93+
}
94+
completion:nil];
95+
}
96+
97+
- (void)stopAlertActivationBusy {
98+
[self.alertActionStatus.layer removeAllAnimations];
99+
self.alertActionStatus.transform = CGAffineTransformIdentity;
100+
[self.alertActivationBusyBackstop invalidate];
101+
self.alertActivationBusyBackstop = nil;
102+
}
103+
104+
70105
- (void)updateAlertActionStatus {
71106
self.alertActionStatus.text = [NSString stringWithFormat:@"First actions: %lu; replacement actions: %lu",
72107
(unsigned long)self.firstAlertActions,
@@ -88,6 +123,7 @@ - (void)presentAlertFixtureReplacement:(BOOL)replacement {
88123
? UIAlertActionStyleCancel : UIAlertActionStyleDefault;
89124
[alert addAction:[UIAlertAction actionWithTitle:buttonTitle style:style handler:^(UIAlertAction *action) {
90125
(void)action;
126+
[self stopAlertActivationBusy];
91127
if (replacement) {
92128
self.replacementAlertActions += 1;
93129
} else {
@@ -110,6 +146,9 @@ - (void)viewDidAppear:(BOOL)animated {
110146
[NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-alert-replacement-regression"]) {
111147
self.alertFixtureStarted = YES;
112148
[self presentAlertFixtureReplacement:NO];
149+
if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-alert-activation-busy"]) {
150+
[self startAlertActivationBusy];
151+
}
113152
}
114153
}
115154
#endif

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+AlertObservationTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,38 @@ extension RunnerTests {
5757
XCTAssertEqual(app.staticTexts["agent-device-alert-actions"].label, "First actions: 0; replacement actions: 0")
5858
}
5959

60+
func testAlertActivationIgnoresAnAppThatNeverSettlesBeforeTheDeadline() throws {
61+
app.launchArguments = [
62+
"--agent-device-alert-replacement-regression",
63+
"--agent-device-alert-activation-busy"
64+
]
65+
app.launch()
66+
defer {
67+
invalidateCachedTarget(reason: "unit_test_cleanup")
68+
app.terminate()
69+
}
70+
XCTAssertTrue(app.alerts.firstMatch.waitForExistence(timeout: appExistenceTimeout))
71+
let alert = try XCTUnwrap(resolveAlert(app: app, deadline: Date().addingTimeInterval(10)))
72+
73+
let deadline = Date().addingTimeInterval(6)
74+
let startedAt = Date()
75+
let response = handleAlert(alert, action: "accept", deadline: deadline)
76+
let elapsed = Date().timeIntervalSince(startedAt)
77+
78+
// The fixture keeps its main thread busy until a button is answered, so this only comes back
79+
// early because activation refused to wait for an app that has no intention of settling (#2546).
80+
XCTAssertLessThan(elapsed, 9, "activation waited \(elapsed)s for a busy app to idle")
81+
XCTAssertTrue(response.ok, String(describing: response.error))
82+
XCTAssertEqual(app.staticTexts["agent-device-alert-actions"].label, "First actions: 1; replacement actions: 0")
83+
if !response.ok, response.error?.code == "ALERT_DEADLINE_EXCEEDED" {
84+
XCTAssertEqual(
85+
app.staticTexts["agent-device-alert-actions"].label,
86+
"First actions: 0; replacement actions: 0",
87+
"a caller told about an expired deadline must not have a button activated behind it"
88+
)
89+
}
90+
}
91+
6092
private func assertReplacementAlertUntouched(action: String, arguments: [String], confirmed: Bool) throws {
6193
app.launchArguments = ["--agent-device-alert-replacement-regression"] + arguments
6294
app.launch()

0 commit comments

Comments
 (0)