test(mobile): check the title fade's animation, wait 10 s for the alert (#8741)

## 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 <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Matt Toohey
2026-10-31 10:37:08 +11:00
committed by GitHub
parent b7b545f090
commit 6e6f4e8a0d

View File

@@ -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)