Skip to content

Commit 22520ba

Browse files
authored
Fix flaky iOS scene-editor save UI test (#786)
tapUntilGone gated the save tap solely on isHittable. Under CI load the top-bar save button's accessibility frame can never settle to hittable within the timeout, so the loop waited out the whole budget without ever tapping and failed with 'Element still present after 20.0s'. Keep the proven isHittable hit-tested fast path and add a geometry fallback: once the button's frame has settled (fully below the status bar, ruling out the transient dead-pixel position, and stable across two samples) tap it via tapCenter. This guarantees a tap eventually fires even when isHittable never flips true, while still avoiding the dead-pixel/scroll-away hazard the isHittable gate originally fixed.
1 parent 54538da commit 22520ba

1 file changed

Lines changed: 24 additions & 5 deletions

File tree

ios/iosUITests/HammerUITest.swift

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,21 +164,40 @@ class HammerUITest: XCTestCase {
164164
/// The save button lives in the top bar, and right after the edit that surfaces it its
165165
/// accessibility frame can be briefly *stale* — reported up under the status bar (a dead pixel)
166166
/// before the layout settles. A blind coordinate tap there does nothing, and repeatedly tapping
167-
/// that dead spot can even scroll/navigate the app away. So only tap when the button is actually
168-
/// hittable (settled), using a hit-tested `tap()` that re-resolves its real position; otherwise
169-
/// wait for it to settle. Re-tap until it's gone, which also covers a dropped tap or a late IME
170-
/// keystroke re-dirtying the buffer right after a save.
167+
/// that dead spot can even scroll/navigate the app away. So never tap the stale frame: tap only
168+
/// once the button has *settled*.
169+
///
170+
/// `isHittable` is the fast settled-signal and the preferred path (a hit-tested `tap()`
171+
/// re-resolves the real position). But under simulator/CI load it can never flip true within the
172+
/// timeout, which would leave a purely-`isHittable`-gated loop waiting out the whole budget
173+
/// without ever tapping. So fall back to a coordinate `tapCenter` once the frame is demonstrably
174+
/// settled by geometry — fully below the status bar (ruling out the dead-pixel position) and
175+
/// stable across two samples. Re-tap until it's gone, which also covers a dropped tap or a late
176+
/// IME keystroke re-dirtying the buffer right after a save.
171177
func tapUntilGone(_ tag: String, timeout: TimeInterval = HammerUITest.defaultTimeout,
172178
file: StaticString = #file, line: UInt = #line) {
173179
let el = waitFor(tag, timeout: timeout, file: file, line: line)
174180
let deadline = Date().addingTimeInterval(timeout)
181+
var lastFrame = CGRect.null
175182
repeat {
176183
if !el.exists { return }
177184
if el.isHittable {
178185
el.tap()
179186
if poll({ !el.exists }, timeout: 3) { return }
180187
} else {
181-
_ = poll({ el.isHittable || !el.exists }, timeout: 1)
188+
// Geometry fallback for when `isHittable` never settles under load. A settled top-bar
189+
// button sits fully below the status bar and holds a stable frame; the transient
190+
// dead-pixel frame reads under the status bar and is discarded here.
191+
let frame = el.frame
192+
let statusBarMaxY = app.statusBars.firstMatch.frame.maxY
193+
let settled = frame.height > 0 && frame.minY >= statusBarMaxY && frame == lastFrame
194+
lastFrame = frame
195+
if settled {
196+
tapCenter(el)
197+
if poll({ !el.exists }, timeout: 3) { return }
198+
} else {
199+
_ = poll({ el.isHittable || !el.exists }, timeout: 0.5)
200+
}
182201
}
183202
} while Date() < deadline
184203
XCTFail("Element still present after \(timeout)s: \(tag)", file: file, line: line)

0 commit comments

Comments
 (0)