From 6e6f4e8a0de2ee85c4a6366ecdb15e1fad7fe429 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Sat, 31 Oct 2026 10:37:08 +1100 Subject: [PATCH] test(mobile): check the title fade's animation, wait 10 s for the alert (#8741) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Two RunnerTests fixes from the 23–30 Oct flake audit, both in `mobile/ios/RunnerTests/RunnerTests.swift`. Both are test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### `testLargeTitleTextFadesWithScrollAndCompactTitleCanReverse` (3 runs) - **Runs:** 40748163526 (job 123028155856), 40826294064 (#8679, job 123290415194, passed on attempt 2) and 40921441156 (#8694, job 123617149320), all in Mobile (iOS), attempt 1. None of the branches touches the navigation bar. Signature: `XCTAssertLessThan(visibleOpacity(of: centeredTitle), 0.99)` read 0.9999499, 1.0 and 1.0. - **Cause:** collapsing adds a 0.18 s `CABasicAnimation` on the compact title mask's opacity (`IosNavigationBar.swift` `applyScroll`). The test slept 60 ms and expected the presentation opacity to be mid-fade. The presentation layer follows wall-clock media time. When the Flutter app in the test host keeps the main thread busy, `Task.sleep` resumes after the fade has finished. - **Change:** drop the 60 ms sleep and the mid-fade samples. Right after `scroll(to: boundary)`, which runs the handler synchronously, check that the mask's model opacity is 1. Also check that it carries a `"titleFade"` opacity `CABasicAnimation` from 0 to 1, lasting 0.1–0.5 s. Nothing turns the run loop before the check, so the animation is still attached however long the main thread stalls. After the scroll back, a new `XCTAssertNil` checks that reversing removes the fade. The later 240 ms waits and their end-state checks are unchanged. - **Mutations that should fail it:** - Delete `compactTitleMask.add(fade, forKey: "titleFade")`: the `XCTUnwrap` throws. - Delete `removeAllAnimations()`: the new `XCTAssertNil` fails. The old test missed this. - Set `fade.duration = 0.001`: the band's lower bound fails. ### `testNativeRemovalConfirmationUsesDestructiveAlertAndCancelsOnce` (1 run) - **Run:** 40950548609 (#8433, Dart only, job 123717768224), attempt 1. Signature: `fulfillment(of: [completed], timeout: 3)` expired. - **Cause:** the busy test-host main thread that #8678 handled for other classes. The cancel reply comes from UIKit's dismissal completion, after the dismiss animation runs on that thread. - **Change:** raise the wait to 10 s, matching #8678's `callbackTimeout`. The assertion is unchanged. Dropping `resolve` from the dismissal completion still fails it, and so does resolving before `alert.dismiss`. ### Related issue None found. **Conflicts with #8568** ("Adapt iPhone Duo layouts, rail navigation, and native sheets"). #8568 rewrites the fade test into a loop but keeps the 60 ms sleep and the mid-fade samples. It already conflicts with `main`, and this PR adds `RunnerTests.swift` to its conflicts. #8665 only adds a voice-note test. ### Testing Not compiled against the app or run locally, because Xcode and RunnerTests bog this machine down. `swiftc -parse` accepts the file. The new assertions, copied into a stub `XCTestCase`, typecheck with `swiftc -typecheck` against the iOS simulator SDK and XCTest (Swift 5 mode). The full file needs Flutter and the Buzz module, so it wasn't typechecked as a whole. **The Mobile (iOS) lane is the check.** Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8742 fix(desktop): deep-linked message hold (product change) - #8743 test(desktop): four specs racing scroll settles and focus - #8744 test(desktop): five settle classes and remote-owned-mentions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Matt Toohey Co-authored-by: Claude Opus 5.5 --- mobile/ios/RunnerTests/RunnerTests.swift | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/mobile/ios/RunnerTests/RunnerTests.swift b/mobile/ios/RunnerTests/RunnerTests.swift index b10bcb17ac..a4851bb102 100644 --- a/mobile/ios/RunnerTests/RunnerTests.swift +++ b/mobile/ios/RunnerTests/RunnerTests.swift @@ -115,7 +115,7 @@ class RunnerTests: XCTestCase { add(attachment) coordinator.cancel() coordinator.cancel() - await fulfillment(of: [completed], timeout: 3) + await fulfillment(of: [completed], timeout: 10) XCTAssertNil(parent.presentedViewController) XCTAssertEqual(replies, [false]) } @@ -744,12 +744,22 @@ class RunnerTests: XCTestCase { bar.view().layoutIfNeeded() XCTAssertLessThan(title.textColor.cgColor.alpha, 0.6) messenger.scroll(to: boundary) - try await Task.sleep(nanoseconds: 60_000_000) - XCTAssertFalse(try XCTUnwrap(centeredTitle.layer.mask).bounds.isEmpty) - XCTAssertGreaterThan(visibleOpacity(of: centeredTitle), 0.01) - XCTAssertLessThan(visibleOpacity(of: centeredTitle), 0.99) + // The host app can hold the main thread past the whole fade, so check the + // animation itself rather than sampling its presentation mid-flight. + let mask = try XCTUnwrap(centeredTitle.layer.mask) + XCTAssertFalse(mask.bounds.isEmpty) + XCTAssertEqual(mask.opacity, 1) + let fade = try XCTUnwrap(mask.animation(forKey: "titleFade") as? CABasicAnimation, + "Collapsing must fade the compact title in") + XCTAssertEqual(fade.keyPath, "opacity") + XCTAssertEqual((fade.fromValue as? NSNumber)?.floatValue, 0) + XCTAssertEqual((fade.toValue as? NSNumber)?.floatValue, 1) + // A band around the 0.18 s design value: a near-zero duration is a snap. + XCTAssertGreaterThanOrEqual(fade.duration, 0.1) + XCTAssertLessThanOrEqual(fade.duration, 0.5) XCTAssertEqual(title.textColor.cgColor.alpha, 0, accuracy: 0.01) messenger.scroll(to: boundary * 0.7) + XCTAssertNil(mask.animation(forKey: "titleFade"), "Reversing must cancel the fade") try await Task.sleep(nanoseconds: 240_000_000) XCTAssertEqual(visibleOpacity(of: centeredTitle), 0, accuracy: 0.01) XCTAssertGreaterThan(title.textColor.cgColor.alpha, 0.1)