Skip to content

Commit f514cde

Browse files
ci(e2e): serve hub-client via vite preview instead of vite dev
Cuts wall time and removes flakes by replacing the dev-mode hub-client server with a prebuilt bundle served through `vite preview`. The win is mostly mechanical: a cold Playwright context downloads ~50 MB of assets (WASM dominates at 32 MB). Through `vite dev` that goes uncompressed and serializes through a single-threaded transform pipeline; through `vite preview` with a gzip middleware it's ~5.6 MB on the wire and served as static files. Under 2 Playwright workers on a 2-core CI runner this was the source of the "preview iframe didn't render in 45s" flakes that the previous commit (75s timeout bump) papered over. CI on chore/e2e-ci across 3 consecutive runs: 0 hard failures, Run E2E tests step 5.3-8.1 min (was 14.1 min, ~42% faster), flakes 3-7 (was 12). Local smoke-all: 1.9 min / 1 flake → 1.1 min / 0 flakes. See `claude-notes/plans/2026-05-11-vite-preview-for-e2e-tests.md` for the 5-phase plan and motivating numbers. Major pieces - hub-client/vite.config.ts: mirror `server.proxy` into `preview.proxy` (preview ignores `server.proxy`). Add a small `configurePreviewServer` plugin that runs `compression()` middleware; override its filter so `application/wasm` is included (mime-db marks it non-compressible by default — in practice it gzips ~6:1). - hub-client/playwright.config.ts: `webServer.command` → `vite preview`; comment why it's not `vite dev` so the next agent doesn't "helpfully" revert it for HMR. - hub-client/ast-renderer.html: moved from `public/` to project root. When it lived in `public/` the build emitted two copies — the transformed one at `dist/public/ast-renderer.html` and a raw copy at `dist/ast-renderer.html` (with a dev-only `<script src="/src/...tsx">` reference). The iframe `src="/ast-renderer.html"` hit the raw one in preview mode and the q2-debug E2E test broke. This was a latent prod bug — `vite dev` happened to mask it via source-path resolution. Test-hook plumbing - src/test-hooks.ts (new): registers `window.__quartoTest = { projectStorage, wasmRenderer }`. Tree-shaken out of any build without `VITE_E2E=1`. - src/main.tsx: kicks off `import('./test-hooks')` and stores the promise on `window.__quartoTestReady`. Top-level `await` here doesn't help (the `load` event fires before module top-level awaits resolve), so tests `await window.__quartoTestReady` before reading the hooks. - e2e/helpers/testHooks.ts (new): typed global augmentation. - e2e/helpers/previewExtraction.ts, projectFactory.ts, share-link-project-set.spec.ts: 5 `await import('/src/services/...ts')` call sites replaced with `await window.__quartoTestReady` + `window.__quartoTest.{projectStorage,wasmRenderer}`. The dev-only source-path imports stopped working under `vite preview` (prod bundles don't expose source paths). - package.json: `test:e2e[:ui]` scripts now set `VITE_E2E=1` and build before running playwright (since preview serves from `dist/`). - .github/workflows/hub-client-e2e.yml: `VITE_E2E: '1'` on the Build TypeScript packages step. Side effects worth knowing - Running `npx playwright test` directly (bypassing `npm run test:e2e`) serves whatever was last built into `dist/` — possibly stale. The npm-script path always rebuilds. - New devDeps `compression` + `@types/compression` (83 transitive packages, dev-only, no shipped code). No production runtime change. - `vite dev`, `vite build` defaults, and the production user bundle are untouched: the compression plugin only fires under `vite preview`, and test-hooks is dead-code-eliminated without `VITE_E2E=1`.
1 parent 9392c83 commit f514cde

13 files changed

Lines changed: 521 additions & 36 deletions

.github/workflows/hub-client-e2e.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,16 @@ jobs:
103103
# q2-demos/* currently fail their vite build (they import an
104104
# absolute path "/src/wasm-js-bridge/cache.js" that only exists
105105
# in hub-client/), and trace-viewer is not under test here.
106+
#
107+
# VITE_E2E=1 on the hub-client build pulls in src/test-hooks.ts so
108+
# the Playwright suite can reach internal services via
109+
# `window.__quartoTest` (production bundles don't expose source
110+
# paths the way `vite dev` did). The flag is consumed only by the
111+
# `if (import.meta.env.VITE_E2E === '1')` branch in src/main.tsx;
112+
# production-user builds leave it unset and tree-shake the hooks out.
106113
- name: Build TypeScript packages (ts-packages + hub-client)
114+
env:
115+
VITE_E2E: '1'
107116
run: |
108117
for pkg in ts-packages/*/; do
109118
if [ -f "$pkg/package.json" ]; then
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Switch hub-client e2e to `vite preview` instead of `vite dev`
2+
3+
**Date:** 2026-05-11
4+
**Branch:** `chore/e2e-ci` (PR #172) — stay on this branch, validate by pushing to the open PR
5+
**Worktree:** `.worktrees/e2e-ci`
6+
**Status:** Planned — not started
7+
8+
## Overview
9+
10+
Goal: cut wall-clock and reduce flakiness in the `Hub-Client E2E Tests`
11+
workflow by serving the hub-client to Playwright from a prebuilt bundle
12+
instead of vite's dev server.
13+
14+
### Motivation
15+
16+
Each Playwright test creates a fresh browser context, which downloads
17+
roughly:
18+
19+
| Asset | Size (uncompressed) |
20+
|---|---|
21+
| `wasm_quarto_hub_client_bg.wasm` | **32 MB** |
22+
| `automerge_wasm_bg.wasm` | 1.8 MB |
23+
| `web-tree-sitter.wasm` | 192 KB |
24+
| dart-sass dynamic-import bundle | ~5 MB |
25+
| Monaco editor chunks | ~3 MB |
26+
| Hundreds of small TS/JSX modules | ~5 MB total |
27+
28+
Per fresh context, **~50 MB of bytes through `vite dev`**, served by a
29+
single-threaded dev server that also has to run its plugin pipeline on
30+
every TS/JSX module on demand. With 2 Playwright workers contending for
31+
one dev server on a 2-core runner, the cold-context page load can hold
32+
up another worker's test for tens of seconds.
33+
34+
Working theory: the "preview iframe didn't render in 45s" timeouts we
35+
keep hitting (and the 5-10 "flaky" retries per run) are mostly **page
36+
loads** blocked on vite dev's serialized module pipeline + uncompressed
37+
binary serving, not actual render time. A static prebuilt bundle served
38+
via `vite preview` should remove this whole class of contention:
39+
40+
- Gzip/brotli compression for binary assets (32 MB → ~8-12 MB on the wire)
41+
- No transform pipeline → no per-request serialization point
42+
- ~10 bundled JS chunks vs. ~500 separate dev-mode module requests
43+
- HTTP cache reuse across same-worker tests is more predictable
44+
45+
### Target outcome
46+
47+
| Metric | Current | Target |
48+
|---|---|---|
49+
| Workflow total | ~16 min | sub-12 min |
50+
| `Run E2E tests` step | ~7 min | sub-5 min |
51+
| Flaky tests | 5-10 / run | ≤ 2 / run |
52+
| Hard failures | 0-1 / run | 0 across ≥ 3 runs |
53+
54+
## Approach
55+
56+
Stay on `chore/e2e-ci`. Each iteration: edit, build locally, run smoke-all
57+
locally with 2 workers, then push to PR #172 to validate on CI. The
58+
existing 75s preview-render timeout (commit `81cc5264`) is a safety net
59+
during the switch; once preview is stable, drop it back to 45s in the
60+
same series.
61+
62+
## Work items
63+
64+
### Phase 0 — Baseline measurement (so we know we improved)
65+
66+
- [ ] Record current local timing: `cd hub-client && CI=1 npx playwright test --grep smoke-all --workers=2 --reporter=line --timeout=90000` (with current `vite dev`). Capture: total wall, flaky count, slowest 5 tests.
67+
- [ ] Record current CI timing from the most recent green run (`25647967388`): total job, `Run E2E tests`, `Build TypeScript packages` durations.
68+
69+
### Phase 1 — Vite preview config
70+
71+
- [ ] `hub-client/vite.config.ts`: lift the proxy block into a `preview.proxy` mirror (vite preview ignores `server.proxy`). Same target/target-handling/rewrite as the existing `server.proxy`. The shared constant `hubTarget` is fine to reuse.
72+
- [ ] Confirm `vite build` emits the WASM file as a static asset under `dist/assets/` (the local `dist/` already shows `wasm_quarto_hub_client_bg-<hash>.wasm` and `automerge_wasm_bg-<hash>.wasm`, so this works today).
73+
- [ ] Verify HTTP `Content-Encoding: gzip` is sent for the WASM by `vite preview` by hitting it manually: `cd hub-client && npm run build && npm run preview -- --port 5173 &; curl -sI http://localhost:5173/assets/wasm_quarto_hub_client*.wasm -H 'Accept-Encoding: gzip'`. If preview doesn't gzip by default, decide whether to: (a) accept the uncompressed 32 MB and rely on the no-transform-pipeline win alone, (b) front it with `compression` middleware via a small Express wrapper, or (c) live with current size and focus on the other wins.
74+
75+
### Phase 2 — Playwright wiring
76+
77+
- [ ] `hub-client/playwright.config.ts`: change `webServer.command` from `npm run dev` to `npm run preview -- --port 5173`. Keep `url: 'http://localhost:5173'`. Keep the `reuseExistingServer` setting. Bump `timeout` if 120s isn't enough to cover the preview-server startup (it should be near-instant).
78+
- [ ] Decide where `VITE_HUB_SERVER` is set:
79+
- Option A: bake it into the build (set on the workflow's "Build TypeScript packages" step env). Cleanest. The hub URL would be hard-coded into the built JS, which is fine for CI but not for shareable artifacts.
80+
- Option B: read it at runtime via vite preview's `preview.proxy` config. Vite reads `process.env.*` at config-eval time when starting the preview server, so passing it via the `webServer.env` in playwright.config.ts should work.
81+
- Pick Option B if it works — keeps the build artifact reusable; only the preview-server invocation needs the env.
82+
- [ ] Local run: same command as Phase 0 baseline. Confirm tests pass, capture timings.
83+
84+
### Phase 3 — CI integration
85+
86+
- [ ] If we used Option B above: no workflow change beyond what's already there.
87+
- [ ] If we used Option A: add `VITE_HUB_SERVER: http://localhost:3030` to the "Build TypeScript packages" step env in `.github/workflows/hub-client-e2e.yml`.
88+
- [ ] Push to `chore/e2e-ci`. Watch the new PR-triggered run. Compare against the Phase 0 CI baseline.
89+
90+
### Phase 4 — Iterate / validate
91+
92+
- [ ] If first run is green with good timings: trigger 2 more runs via `gh workflow run hub-client-e2e.yml --ref chore/e2e-ci` to check stability across consecutive runs.
93+
- [ ] If first run has new failures (production-build code-path differences, missing assets, broken proxy): diagnose, fix, push again.
94+
- [ ] If the preview switch alone clears flakes: drop the 75s preview timeout back to 45s in a follow-up commit to confirm it's the preview mode (not the timeout headroom) that's doing the work.
95+
96+
### Phase 5 — Cleanup
97+
98+
- [ ] If preview migration sticks, consider squashing the timeout-bump commit (`81cc5264`) into the preview commit via `git revise` — or keep them separate so the bisect history tells the story.
99+
- [ ] Update the squashed CI commit's message (or add a fresh commit) describing the dev→preview switch.
100+
- [ ] Update the PR description / comment with before/after timing numbers.
101+
- [ ] If a measurable speed-up landed, add a sentence to `hub-client/playwright.config.ts` explaining why it's `preview` (so a future agent doesn't "helpfully" revert it to `dev` for HMR).
102+
103+
## Risks / unknowns
104+
105+
- **`preview.proxy` may not honor every option of `server.proxy`**. The shared proxy logic should be straightforward (forward `/auth/*` and the ws upgrade to `http://localhost:3030`), but worth checking against vite docs and an actual local run before pushing.
106+
- **Production-only code paths in hub-client**. If anything reads `import.meta.env.PROD` and behaves differently in built mode, e2e would suddenly hit it. Could surface real bugs (good!) or break tests for non-bug reasons (need fixing). The hub-client doesn't seem to have many of these from a quick scan, but full local-run validation in Phase 1 catches it.
107+
- **Source maps and debugging**. Built mode has external source maps; failure investigation needs `playwright show-trace` or careful artifact-spelunking, same as today. Likely no regression.
108+
- **The `optimizeDeps.exclude: ['wasm-quarto-hub-client']` line is dev-only**; vite build handles the alias via `resolve.alias` regardless. The local `dist/` already proves this works.
109+
- **Cache busting**. Vite build produces hashed filenames. If anything in test setup or `e2e/helpers/` hard-codes a JS asset path (it shouldn't — everything goes through the HTML entry), it'd break.
110+
- **HMR is gone in preview** — fine for CI, not a regression because we never used HMR in tests.
111+
- **First-test-in-worker is still slower than subsequent** (browser cache cold), but the absolute number should be much smaller. Acceptable.
112+
113+
## Success criteria
114+
115+
The PR's next push triggers a CI run that:
116+
117+
1. Completes the `Run E2E tests` step in **under 5 minutes**.
118+
2. Reports **0 hard failures**.
119+
3. Reports **≤ 2 flaky tests**.
120+
4. Total workflow under **12 minutes**.
121+
122+
Then run the workflow 2 more times via `workflow_dispatch` and confirm the
123+
numbers are stable, not lucky-once.
124+
125+
## Estimated effort
126+
127+
30-90 minutes including local validation. The risk is that the
128+
production build surfaces a hub-client bug that doesn't exist in dev
129+
mode, in which case scope expands.

hub-client/e2e/helpers/previewExtraction.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import type { Page } from '@playwright/test';
9+
import type {} from './testHooks';
910

1011
/**
1112
* Wait for the preview iframe to render content.
@@ -101,8 +102,10 @@ export async function getPreviewHtml(
101102
documentPath: string,
102103
): Promise<string> {
103104
return page.evaluate(async (docPath) => {
104-
const renderer = await import('/src/services/wasmRenderer.ts');
105-
const result = await renderer.renderToHtml({ documentPath: docPath });
105+
await window.__quartoTestReady;
106+
const hooks = window.__quartoTest;
107+
if (!hooks) throw new Error('__quartoTest missing — rebuild with VITE_E2E=1');
108+
const result = await hooks.wasmRenderer.renderToHtml({ documentPath: docPath });
106109
return result.html ?? '';
107110
}, documentPath);
108111
}
@@ -121,7 +124,10 @@ export async function getPreviewCss(page: Page): Promise<string> {
121124
throw new Error('No active preview iframe found');
122125
}
123126

124-
const renderer = await import('/src/services/wasmRenderer.ts');
127+
await window.__quartoTestReady;
128+
const hooks = window.__quartoTest;
129+
if (!hooks) throw new Error('__quartoTest missing — rebuild with VITE_E2E=1');
130+
const renderer = hooks.wasmRenderer;
125131
const links = iframe.contentDocument.querySelectorAll('link[rel="stylesheet"]');
126132
let combinedCss = '';
127133

@@ -187,8 +193,10 @@ export async function getRenderDiagnostics(
187193
warnings: RenderDiagnostic[];
188194
}> {
189195
return page.evaluate(async (docPath) => {
190-
const renderer = await import('/src/services/wasmRenderer.ts');
191-
const result = await renderer.renderToHtml({ documentPath: docPath });
196+
await window.__quartoTestReady;
197+
const hooks = window.__quartoTest;
198+
if (!hooks) throw new Error('__quartoTest missing — rebuild with VITE_E2E=1');
199+
const result = await hooks.wasmRenderer.renderToHtml({ documentPath: docPath });
192200
return {
193201
success: result.success,
194202
error: result.error,

hub-client/e2e/helpers/projectFactory.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { SERVER_INFO_PATH } from './globalSetup';
2020
import type { ServerInfo } from './globalSetup';
2121
import { expect, type Page } from '@playwright/test';
22+
import type {} from './testHooks';
2223

2324
export interface ProjectFile {
2425
path: string;
@@ -160,8 +161,10 @@ export async function seedProjectInBrowser(
160161
): Promise<string> {
161162
return page.evaluate(
162163
async ({ indexDocId, syncServer, name }) => {
163-
const ps = await import('/src/services/projectStorage.ts');
164-
const entry = await ps.addProject(indexDocId, syncServer, name);
164+
await window.__quartoTestReady;
165+
const hooks = window.__quartoTest;
166+
if (!hooks) throw new Error('__quartoTest missing — rebuild with VITE_E2E=1');
167+
const entry = await hooks.projectStorage.addProject(indexDocId, syncServer, name);
165168
return entry.id;
166169
},
167170
{ indexDocId, syncServer, name },
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Type declarations for the in-browser test hooks installed by
3+
* `src/test-hooks.ts`. The hooks are registered on `window.__quartoTest`
4+
* only when the bundle is built with `VITE_E2E=1` (see playwright.config.ts).
5+
*
6+
* This file is for types only — the runtime accessor lives inside the
7+
* `page.evaluate(...)` callbacks because page.evaluate ships callback
8+
* source to the browser, where Node-side helpers aren't reachable.
9+
*
10+
* Always `await window.__quartoTestReady` before reading `window.__quartoTest`
11+
* — the hooks module loads asynchronously and the page's `load` event fires
12+
* before module top-level awaits resolve.
13+
*
14+
* Usage from a test/helper:
15+
*
16+
* ```ts
17+
* import type {} from './testHooks'; // ambient global augmentation
18+
*
19+
* await page.evaluate(async () => {
20+
* await window.__quartoTestReady;
21+
* const hooks = window.__quartoTest!;
22+
* return hooks.projectStorage.addProject(...);
23+
* });
24+
* ```
25+
*/
26+
import type * as projectStorage from '../../src/services/projectStorage';
27+
import type * as wasmRenderer from '../../src/services/wasmRenderer';
28+
29+
export interface QuartoTestHooks {
30+
projectStorage: typeof projectStorage;
31+
wasmRenderer: typeof wasmRenderer;
32+
}
33+
34+
declare global {
35+
interface Window {
36+
__quartoTest?: QuartoTestHooks;
37+
__quartoTestReady?: Promise<unknown>;
38+
}
39+
}

hub-client/e2e/share-link-project-set.spec.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
createProjectOnServer,
2222
getServerUrl,
2323
} from './helpers/projectFactory';
24+
import type {} from './helpers/testHooks';
2425

2526
/**
2627
* Bootstrap the receiver browser with an existing synced project set.
@@ -201,8 +202,10 @@ test.describe('Share link → synced project set', () => {
201202
.poll(
202203
() =>
203204
sharePage.evaluate(async (docId) => {
204-
const ps = await import('/src/services/projectStorage.ts');
205-
const entry = await ps.getProjectByIndexDocId(`automerge:${docId}`);
205+
await window.__quartoTestReady;
206+
const hooks = window.__quartoTest;
207+
if (!hooks) throw new Error('__quartoTest missing — rebuild with VITE_E2E=1');
208+
const entry = await hooks.projectStorage.getProjectByIndexDocId(`automerge:${docId}`);
206209
return !!entry;
207210
}, sharedIndexDocId),
208211
{ timeout: 15000 },
@@ -268,8 +271,10 @@ test.describe('Share link → synced project set', () => {
268271
// nothing about — this is exactly the state Bug A produces.
269272
await bootstrapPage.evaluate(
270273
async ({ indexDocId, server }) => {
271-
const ps = await import('/src/services/projectStorage.ts');
272-
await ps.addProject(`automerge:${indexDocId}`, server, 'Orphan Demo');
274+
await window.__quartoTestReady;
275+
const hooks = window.__quartoTest;
276+
if (!hooks) throw new Error('__quartoTest missing — rebuild with VITE_E2E=1');
277+
await hooks.projectStorage.addProject(`automerge:${indexDocId}`, server, 'Orphan Demo');
273278
},
274279
{ indexDocId: orphanIndexDocId, server: syncServer },
275280
);

hub-client/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
"test:integration:watch": "vitest --config vitest.integration.config.ts",
2121
"test:wasm": "vitest run --config vitest.wasm.config.ts",
2222
"test:wasm:watch": "vitest --config vitest.wasm.config.ts",
23-
"test:e2e": "npm run build:wasm && playwright test",
24-
"test:e2e:ui": "npm run build:wasm && playwright test --ui",
23+
"test:e2e": "npm run build:wasm && VITE_E2E=1 npm run build && playwright test",
24+
"test:e2e:ui": "npm run build:wasm && VITE_E2E=1 npm run build && playwright test --ui",
2525
"test:visual": "playwright test --config playwright.visual.config.ts",
2626
"test:visual:update": "playwright test --config playwright.visual.config.ts --update-snapshots=missing",
2727
"test:ci": "npm run test && npm run test:integration && npm run test:wasm",
@@ -56,13 +56,15 @@
5656
"@playwright/test": "^1.50.0",
5757
"@testing-library/jest-dom": "^6.6.3",
5858
"@testing-library/react": "^16.1.0",
59+
"@types/compression": "^1.8.1",
5960
"@types/ejs": "^3.1.5",
6061
"@types/node": "^24.10.1",
6162
"@types/react": "^19.2.5",
6263
"@types/react-dom": "^19.2.3",
6364
"@types/ws": "^8.18.1",
6465
"@vitejs/plugin-react": "^5.1.1",
6566
"@vitest/coverage-v8": "^4.0.17",
67+
"compression": "^1.8.1",
6668
"eslint": "^9.39.1",
6769
"eslint-plugin-react-hooks": "^7.0.1",
6870
"eslint-plugin-react-refresh": "^0.4.24",

hub-client/playwright.config.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,32 @@ export default defineConfig({
6060
// },
6161
],
6262

63-
// Run local dev server before tests
63+
// Serve the prebuilt hub-client bundle via `vite preview` rather than
64+
// `vite dev`. Why this matters for E2E throughput:
65+
// - A cold Playwright browser context fetches ~50 MB of assets
66+
// (the ~32 MB WASM dominates). Through `vite dev` that goes
67+
// uncompressed and serialized through the transform pipeline;
68+
// through `vite preview` (with our gzip middleware in vite.config.ts)
69+
// it's ~5.6 MB on the wire and served as static files.
70+
// - `vite dev` also re-transforms ~500 TS/JSX modules per page load;
71+
// the production bundle is ~10 chunks.
72+
// - Under 2 Playwright workers contending for one dev server on a
73+
// 2-core CI runner this was the source of the "preview iframe
74+
// didn't render in 45s" flakes. Do NOT revert this to `npm run dev`
75+
// to get HMR back — HMR isn't used by tests and dev mode reintroduces
76+
// the contention.
6477
webServer: {
65-
command: 'npm run dev',
78+
command: 'npm run preview -- --port 5173',
6679
url: 'http://localhost:5173',
6780
// Reuse existing server in dev mode for faster iteration
6881
reuseExistingServer: !process.env.CI,
69-
// Timeout for server to start
82+
// Timeout for server to start (preview is near-instant; the bundle
83+
// must already be built, which the CI workflow does explicitly).
7084
timeout: 120000,
71-
// Vite proxies /auth/* and the websocket to VITE_HUB_SERVER
72-
// (default http://localhost:3000). globalSetup starts the e2e hub
73-
// on port 3030, so point Vite at that port.
85+
// Vite preview reads VITE_HUB_SERVER at config-eval time to wire
86+
// preview.proxy at the target. globalSetup starts the e2e hub on
87+
// port 3030, so point preview at that port. The env is server-side
88+
// only — it isn't baked into the built client bundle.
7489
env: {
7590
VITE_HUB_SERVER: 'http://localhost:3030',
7691
},

hub-client/src/main.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import { savePreAuthHash, restorePreAuthHash } from './utils/routing'
1212
// Order matters: restore first (consumes saved value), then save current.
1313
restorePreAuthHash() || savePreAuthHash();
1414

15+
// E2E test hooks. Tree-shaken out unless VITE_E2E=1 was set at build time.
16+
// We expose the dynamic-import promise on `window.__quartoTestReady` so
17+
// `page.evaluate` callbacks can `await` it. Top-level await here would
18+
// not help: it blocks React mount but the browser's `load` event (which
19+
// playwright `goto` waits for) fires before module top-level awaits
20+
// resolve, so a naive `window.__quartoTest` access can still race.
21+
if (import.meta.env.VITE_E2E === '1') {
22+
(window as Window & { __quartoTestReady?: Promise<unknown> }).__quartoTestReady =
23+
import('./test-hooks');
24+
}
25+
1526
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
1627

1728
const root = (

0 commit comments

Comments
 (0)