From c9f293d8ec6d128871a9c8e31d37085f4259f16c Mon Sep 17 00:00:00 2001 From: Aqil Aziz Date: Thu, 21 May 2026 06:52:19 +0700 Subject: [PATCH] test(e2e): wait for route readiness ## Summary - Replaces the fixed post-hash `browser.pause(2_000)` with a route readiness wait. - `navigateViaHash()` now waits for the target hash, `document.readyState === "complete"`, and a mounted React root before returning. - `walkOnboarding()` now waits for `#/home` and a Home-page marker after the onboarding next button unmounts. - Documents the navigation-readiness pattern in the E2E guide. ## Problem - #1864 reports a first-navigation race after onboarding: the hash changes, but the target panel can be empty because React has not settled yet. - The old helper returned after a fixed pause, so specs could start looking for panel text before the routed view mounted. ## Solution - Add `waitForHashRouteReady()` to make hash navigation wait on concrete browser/app signals. - Add `waitForPostOnboardingHome()` so the onboarding walker does not hand control back until Home is actually ready. - Throw navigation readiness failures immediately instead of hiding them behind a later text timeout. ## Submission Checklist - [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) - helper behavior tightened; E2E suite is the exercising path. - [x] **Diff coverage >= 80%** - N/A locally: E2E helper/doc change; CI E2E jobs are authoritative. - [x] Coverage matrix updated - N/A: E2E helper behavior change, no feature row added/removed/renamed. - [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` - N/A: no matrix feature ID applies. - [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy)) - [x] Manual smoke checklist updated if this touches release-cut surfaces ([`docs/RELEASE-MANUAL-SMOKE.md`](../docs/RELEASE-MANUAL-SMOKE.md)) - N/A: no release-cut surface. - [x] Linked issue closed via `Closes #NNN` in the `## Related` section ## Impact - Runtime/user impact: none; test helper only. - E2E impact: hash navigation and post-onboarding transitions now wait on real readiness signals instead of fixed sleeps. - Failure mode improves: route readiness failures surface at navigation time with the target hash in the error. ## Related - Closes #1864 - Follow-up PR(s)/TODOs: N/A --- ## AI Authored PR Metadata (required for Codex/Linear PRs) ### Linear Issue - Key: N/A - URL: N/A ### Commit & Branch - Branch: `codex/1864-e2e-navigation-readiness` - Commit SHA: `15f56af3c6c865fbd322010d25b047a998ebd964` ### Validation Run - [x] `pnpm --filter openhuman-app exec prettier --check test/e2e/helpers/shared-flows.ts` - passed - [x] `pnpm typecheck` - passed - [x] Focused tests: N/A, E2E helper change not run locally - [x] Rust fmt/check (if changed): `cargo fmt --all --check` - passed; `git diff --check` - passed - [x] Tauri fmt/check (if changed): N/A ### Validation Blocked - `command:` full E2E rerun - `error:` not run locally; requires built desktop app/Appium harness - `impact:` remote E2E CI remains authoritative for the harness change ### Behavior Changes - Intended behavior change: E2E helpers wait for route/onboarding readiness before specs continue. - User-visible effect: none. ### Parity Contract - Legacy behavior preserved: same routes and onboarding flow; only readiness timing changed. - Guard/fallback/dispatch parity checks: existing text assertions remain in specs after helper navigation. ### Duplicate / Superseded PR Handling - Duplicate PR(s): N/A - Canonical PR: this PR - Resolution (closed/superseded/updated): N/A ## Summary by CodeRabbit * **Tests** * Enhanced E2E helpers for more reliable hash-based navigation and for stronger verification that onboarding completes and the Home page is fully settled. * **Documentation** * Updated E2E testing guide with cross-platform navigation guidance recommending the hash navigation helper and noting post-onboarding Home-page readiness checks. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2304?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) Co-authored-by: aqilaziz Co-authored-by: Steven Enamakel --- app/test/e2e/helpers/shared-flows.ts | 99 +++++++++++++++++++++++++++- gitbooks/developing/e2e-testing.md | 4 ++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/app/test/e2e/helpers/shared-flows.ts b/app/test/e2e/helpers/shared-flows.ts index 6db917189..0eef2d073 100644 --- a/app/test/e2e/helpers/shared-flows.ts +++ b/app/test/e2e/helpers/shared-flows.ts @@ -113,19 +113,92 @@ const HASH_TO_SIDEBAR_LABEL = { '/settings/intelligence': 'Intelligence', }; +function normalizeHash(value) { + const raw = String(value || ''); + const withPrefix = raw.startsWith('#') ? raw : `#${raw}`; + return withPrefix.replace(/\/$/, ''); +} + +function routeReadySelector(hash) { + const path = normalizeHash(hash).replace(/^#/, ''); + const selectors = { + '/notifications': '[data-testid="integration-notifications-section"]', + '/settings/cron-jobs': '[data-testid="cron-jobs-panel"]', + '/settings/privacy': '[data-testid="settings-privacy-panel"]', + '/settings/migration': '[data-testid="migration-form"]', + '/settings/voice': '[data-testid="voice-providers-section"]', + '/settings/memory-data': '[data-testid="memory-workspace"]', + '/settings/intelligence': '[data-testid="memory-workspace"]', + }; + return selectors[path] || null; +} + +async function routeSignature() { + return browser.execute(() => { + const root = document.getElementById('root'); + return (root?.innerText || root?.textContent || '').trim().slice(0, 500); + }); +} + +async function waitForHashRouteReady(hash, options = {}) { + const { timeout = 10_000, previousSignature = '', allowSameSignature = false } = options; + const expected = normalizeHash(hash); + const readySelector = routeReadySelector(hash); + await browser.waitUntil( + async () => + Boolean( + await browser.execute( + ({ target, selector, before, allowSame }) => { + if (document.readyState !== 'complete') return false; + const current = window.location.hash.replace(/\/$/, ''); + if (current !== target) return false; + const root = document.getElementById('root'); + if (!root) return false; + if (selector && root.querySelector(selector)) return true; + + const signature = (root.innerText || root.textContent || '').trim().slice(0, 500); + if (!signature) return false; + return allowSame || signature !== before; + }, + { + target: expected, + selector: readySelector, + before: previousSignature, + allowSame: allowSameSignature, + } + ) + ), + { + timeout, + interval: 250, + timeoutMsg: `hash route ${expected} did not become ready within ${timeout}ms`, + } + ); +} + export async function navigateViaHash(hash) { const normalized = String(hash).replace(/\/$/, '') || hash; if (supportsExecuteScript()) { + const beforeHash = normalizeHash(await browser.execute(() => window.location.hash)); + const beforeSignature = await routeSignature(); + const targetHash = normalizeHash(hash); try { await browser.execute(h => { window.location.hash = h; }, hash); - await browser.pause(2_000); + await waitForHashRouteReady(hash, { + previousSignature: beforeSignature, + allowSameSignature: beforeHash === targetHash, + }); const currentHash = await browser.execute(() => window.location.hash); console.log(`[E2E] Navigated to ${hash} (current: ${currentHash})`); } catch (err) { console.log(`[E2E] Hash navigation to ${hash} failed:`, err); + const detail = err instanceof Error ? err.message : String(err); + const wrapped = new Error(`[E2E] Hash navigation to ${hash} failed: ${detail}`); + wrapped.cause = err; + throw wrapped; } return; } @@ -378,6 +451,29 @@ export async function dismissBootCheckGateIfVisible(timeoutMs = 12_000): Promise return everSeen; } +async function waitForPostOnboardingHome(logPrefix, timeout = 20_000) { + if (supportsExecuteScript()) { + await browser.waitUntil( + async () => + Boolean(await browser.execute(() => window.location.hash.replace(/\/$/, '') === '#/home')), + { + timeout: Math.min(timeout, 10_000), + interval: 300, + timeoutMsg: 'onboarding completed but hash did not settle on #/home', + } + ); + } + + const homeText = await waitForHomePage(timeout); + if (!homeText) { + const tree = await dumpAccessibilityTree(); + console.log(`${logPrefix} Home page not ready after onboarding. Tree:\n`, tree.slice(0, 4000)); + throw new Error('Onboarding dismissed but Home page did not become ready'); + } + + console.log(`${logPrefix} Post-onboarding Home page confirmed: found "${homeText}"`); +} + /** * Walk through onboarding by advancing the `data-testid="onboarding-next-button"` * until it unmounts. The button is rendered on every step (see @@ -448,6 +544,7 @@ export async function walkOnboarding(logPrefix = '[E2E]', maxSteps = 12): Promis if (status === 'gone') { console.log(`${logPrefix} Onboarding dismissed after ${step} step(s)`); + await waitForPostOnboardingHome(logPrefix); return; } if (status === 'gone-but-onboarding-hash') { diff --git a/gitbooks/developing/e2e-testing.md b/gitbooks/developing/e2e-testing.md index affba06de..1a67782b0 100644 --- a/gitbooks/developing/e2e-testing.md +++ b/gitbooks/developing/e2e-testing.md @@ -126,6 +126,10 @@ Use `waitForTestId(testId)` and `clickTestId(testId)` from `element-helpers.ts` 3. **Use `hasAppChrome()`** instead of checking for `XCUIElementTypeMenuBar` 4. **Use `waitForWebView()`** instead of checking for `XCUIElementTypeWebView` 5. For macOS-only tests, use `process.platform` guards or separate spec files +6. Use `navigateViaHash(route)` for hash routes; it waits for the hash, + `document.readyState`, and a mounted React root before returning. After + onboarding, `walkOnboarding()` also waits for `#/home` plus a Home-page + marker before specs navigate elsewhere. ---