TL;DR
Firefox's addInitScript can race against page rendering when seeding localStorage for E2E tests. We fixed it by splitting navigation into two helpers: gotoPage (fast, commit) for interactive flows and gotoAgreementPage (reliable, domcontentloaded) for data-dependent pages.
The Problem
We use Playwright's addInitScript to seed localStorage with mock agreement data before each test. This works perfectly in Chromium — the init script runs, sets localStorage, and by the time React mounts, the data is there.
Firefox? Not so much. About 25% of our lifecycle tests were failing with "Loading agreement..." stuck on screen. The page rendered before the init script finished writing to localStorage.
The Diagnosis
Playwright's error context snapshots told the story. On first failure:
- paragraph: Loading agreement...
On retry (with more time):
- heading "Retainer Agreement"
- button "Connect Wallet" # Auth data missing too!
The page loaded fine on retry, but auth data from localStorage wasn't picked up either. Classic timing issue.
The Fix
We had one navigation helper:
export async function gotoPage(page: Page, url: string) {
await page.goto(url, { waitUntil: 'commit' });
await page
.getByRole('link', { name: /Papre/i })
.first()
.waitFor({ state: 'visible', timeout: 30000 });
}
commit returns as soon as the server responds — before scripts execute. Fast for interactive tests, but too fast for Firefox when init scripts need to complete.
We added a second helper:
export async function gotoAgreementPage(page: Page, url: string) {
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page
.getByRole('link', { name: /Papre/i })
.first()
.waitFor({ state: 'visible', timeout: 30000 });
}
domcontentloaded waits for the HTML to be fully parsed and all deferred scripts to execute. This gives addInitScript time to complete before React reads localStorage.
The key insight: you can't use domcontentloaded everywhere. Our wizard/create tests use keyboard shortcuts (Ctrl+Shift+D for auto-fill) immediately after navigation. The extra wait from domcontentloaded caused those tests to timeout — the page wasn't interactive fast enough.
So: gotoPage for interactive flows, gotoAgreementPage for data-dependent pages.
Results
- Before: 23 Firefox failures out of 240 overnight tests
- After: 240/240 pass across Chromium, Firefox, WebKit, Mobile Chrome, Mobile Safari
The Broader Session
Beyond the Firefox fix, we shipped:
- 8 retainer lifecycle tests (full state machine)
- NDA gap tests (one-way variant, terminate action)
- Safety-net gap tests (expired deadline enforcement, dispute evidence)
- GitHub Actions overnight workflow
- 3 app bugs found only through E2E coverage
Builder's Note
Spent most of the day thinking about fragmentation — how splitting yourself into roles creates anxiety. The code mirrored that perfectly: scattered tests across directories, inconsistent browser behavior. Consolidation brought clarity to both. Wholeness is ease.
Building Papre — composable agreement infrastructure for trustless coordination.
Top comments (0)