From 64f5169c921ebd3788df0e910744217a6ee39870 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 19 Jun 2026 20:33:20 -0400 Subject: [PATCH] test: stop iPad UI tests flaking on dropped sidebar taps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `goToSection` drove the iPad sidebar (a `NavigationSplitView` `List(selection:)` row) with a single fire-and-forget tap and no verification. Synthesized taps against a selection-driven sidebar row are occasionally dropped before the app has quiesced (a periodic `TimelineView` keeps it non-idle at launch), so the row never becomes selected, the detail column stays on the previous section, and the following content assertion fails — e.g. `testAppListsEditableWithoutHardSession` timing out on `ruleCard-Sleep`. iPhone is unaffected because it taps a tab-bar button instead. Confirm the navigation landed before returning: each section's detail names its navigation bar after the same label as the tab/row (`navigationTitle("Rules")` -> `navigationBars["Rules"]`), so tap, wait for that bar, and re-tap if it didn't appear. The iPhone tab-bar path is unchanged; the happy path returns as soon as the bar appears. Validated on iPad Pro 11-inch (M5): the previously-flaky test, both NavigationChrome tests (all three sidebar paths), and a cross-class batch of UI tests all pass. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01WReTVa1zD6CHKRY8zqS9Pb --- OpenAppLockUITests/UITestSupport.swift | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/OpenAppLockUITests/UITestSupport.swift b/OpenAppLockUITests/UITestSupport.swift index aeda882..8c16d4d 100644 --- a/OpenAppLockUITests/UITestSupport.swift +++ b/OpenAppLockUITests/UITestSupport.swift @@ -57,13 +57,37 @@ extension XCUIApplication { /// chrome: the bottom tab bar in compact width (iPhone, iPad multitasking) or /// the left sidebar in regular width (full-screen iPad). Keeps the rest of the /// UI suite agnostic to which device it runs on. - private func goToSection(tabLabel: String, sidebarIdentifier: String) { + /// + /// Each section's detail names its navigation bar after the same label as the + /// tab/row (`navigationTitle("Rules")` → `navigationBars["Rules"]`), giving a + /// device-agnostic "we actually landed" post-condition. + private func goToSection( + tabLabel: String, sidebarIdentifier: String, + file: StaticString = #filePath, line: UInt = #line + ) { let tab = tabBars.buttons[tabLabel] if tab.waitForExistence(timeout: 2) { tab.tap() - } else { - element(sidebarIdentifier).waitToAppear().tap() + return + } + + // iPad sidebar (NavigationSplitView). A single synthesized tap on a + // selection-driven sidebar row is occasionally dropped before the app has + // quiesced — the row never becomes selected and the detail stays on the + // previous section, so a following content assertion flakes. Confirm the + // target section's detail actually appeared and re-tap if it didn't, + // rather than assuming the first tap took. + let item = element(sidebarIdentifier).waitToAppear(file: file, line: line) + let sectionBar = navigationBars[tabLabel] + for _ in 0..<5 { + item.tap() + if sectionBar.waitForExistence(timeout: 2) { return } } + XCTAssertTrue( + sectionBar.exists, + "Sidebar navigation to \(tabLabel) never landed after repeated taps", + file: file, line: line + ) } /// Waits for the post-onboarding shell to appear in whichever chrome the