mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
7b46894413
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
84 lines
2.8 KiB
TypeScript
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';
|