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


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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_stack_entry_start -->

[![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)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: aqilaziz <gonzes7@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
Aqil Aziz
2026-05-20 16:52:19 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent 0f5e91421b
commit c9f293d8ec
2 changed files with 102 additions and 1 deletions
+98 -1
View File
@@ -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') {
+4
View File
@@ -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.
---