Files
pad/web/e2e/fixtures.ts
T
xarmian 7b46894413 fix(e2e): silence cross-actor SSE creation toasts suite-wide (BUG-2334)
The e2e suite shares one pad instance and one workspace, so items seeded
by OTHER concurrently-running specs arrive over SSE and stack
"X created: ..." info toasts bottom-right — directly over bottom-right UI
(the graph drawer's detail card), turning unrelated specs' clicks into a
race. pane-content-link-anchors:238 paid a ~40-minute rerun tail at
nearly every merge gate.

The fix is a narrowly-scoped test-surface kill switch, not a retry:

- `quietExternalToasts()` (toast store): reads a localStorage flag no
  production code ever sets; never throws whatever storage does.
- The ONE call site announcing another actor's SSE work — the external
  `item_created` toast in the workspace layout — checks it. Toasts the
  page earns with its own actions are untouched, so specs still exercise
  the real toast surface (copy-dialog's no-force-click policy keeps its
  protective value).
- The shared e2e fixture installs the flag on every context via
  `quietCrossActorToasts()`; collab-persistence's self-built contexts
  install it explicitly; account-delete's contexts never enter workspace
  routes and stay bare.
- sse-toast-quiet.spec.ts pins BOTH sides: the quiet leg anchors on the
  layout branch's own by-uuid GET (pre-attached response log — no
  arm-order race; SSE-stream response gates the create; bounded settle
  before the negative assert), and a deliberately unflagged CONTROL
  context proves the product toast still fires — the real behavior
  cannot silently regress behind the suite-wide silence.

Evidence: three consecutive full local suite runs with ZERO failures
(baseline: 1-3 interception/load flakes per run); unit tests pin the
helper's contract. Reviewed to fresh-angle CLEAN over four Codex rounds
(vacuous-anchor, arm-order, SSE-connectedness, and self-built-context
holes all found and fixed by the loop).

Claude-Session: https://claude.ai/code/session_01WFBYxdBuSZs2tjipATxAZu
2026-08-11 15:43:42 +00:00

84 lines
2.8 KiB
TypeScript

import { test as base, type BrowserContext } from '@playwright/test';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
/**
* Shared test fixtures.
*
* Specs import `test` from here (not `@playwright/test`) so every
* BrowserContext is pre-authenticated with the admin's API token and
* every test gets the resolved suite fixture (baseURL, workspace slug,
* derived admin username).
*
* We use a Bearer token rather than a session cookie because the Pad
* server binds sessions to the originating User-Agent (see
* internal/server/middleware_auth.go). A session minted in node.js and
* replayed from a headless Chromium context would be silently rejected;
* tokens have no such binding.
*/
export interface SuiteFixture {
baseURL: string;
workspaceSlug: string;
adminEmail: string;
adminUsername: string;
apiToken: string;
}
function loadSuiteFixture(): SuiteFixture {
const path = resolve(HERE, '.auth', 'fixture.json');
return JSON.parse(readFileSync(path, 'utf8')) as SuiteFixture;
}
async function applyTokenAuth(context: BrowserContext, fixture: SuiteFixture) {
await context.setExtraHTTPHeaders({
Authorization: `Bearer ${fixture.apiToken}`
});
}
/**
* BUG-2334: silence CROSS-ACTOR "X created: …" SSE toasts in a context.
* Specs share one workspace, so other specs' seeding otherwise stacks
* toasts over bottom-right UI and races unrelated clicks. Page-own toasts
* are unaffected (see quietExternalToasts in the toast store). Applied to
* every built-in `context`; specs that build their OWN workspace-visiting
* contexts (browser.newContext) must call this too —
* sse-toast-quiet.spec.ts's control leg is the one deliberate exception,
* pinning the flag-off product behavior.
*/
export async function quietCrossActorToasts(context: BrowserContext): Promise<void> {
await context.addInitScript(() => {
try {
localStorage.setItem('pad:e2e-quiet-external-toasts', '1');
} catch {
// storage unavailable — fall through to prod behavior
}
});
}
// Standalone helper for the occasional spec that wants to build its
// own isolated context (e.g. to assert unauthed behaviour).
export function suiteFixture(): SuiteFixture {
return loadSuiteFixture();
}
// Extended test — use `import { test } from './fixtures'` in specs.
// The `context` fixture override applies the Bearer header once per
// context so downstream `page` navigation is authenticated.
export const test = base.extend<{ fixture: SuiteFixture }>({
context: async ({ context }, run) => {
const fixture = loadSuiteFixture();
await applyTokenAuth(context, fixture);
await quietCrossActorToasts(context);
await run(context);
},
fixture: async ({}, run) => {
await run(loadSuiteFixture());
}
});
export { expect } from '@playwright/test';