forked from mirror/crumbs-original
perf(e2e): log in once and share the admin session via storage state
Every test in the `app` project submitted the login form through the authenticatedPage fixture: a page load, an Argon2 verification on the server, a redirect and a networkidle wait. Over 137 tests that was roughly half of the median test's 1.8s. A new `login-state` setup project (tests/e2e/auth.setup.ts) authenticates once after auth.spec has created the account and saves the session cookie to data/test-storage-state.json. The `app` project loads it into every context, so setupAndLogin takes its "session still valid" path and the fixture is reduced to a page load. pwa.spec and mcp.spec describe anonymous visitors and opt out with an empty storageState so their baseline is unchanged. Local run, 4 workers: summed test time 276s -> 256s, median 1.8s -> 1.6s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
committed by
Fred Cabassut
co-authored by
Claude Fable 5.1
parent
af7186675e
commit
7bf06edd97
@@ -219,6 +219,7 @@ Write tests first or alongside features. Tests serve as living documentation of
|
|||||||
- Config: `playwright.config.ts` (Chromium only, builds + previews app on port 4173)
|
- Config: `playwright.config.ts` (Chromium only, builds + previews app on port 4173)
|
||||||
- Test database: `./data/test-crumbs.db` (cleaned via `global-setup.ts`)
|
- Test database: `./data/test-crumbs.db` (cleaned via `global-setup.ts`)
|
||||||
- Auth fixture: `tests/e2e/helpers/fixtures.ts` provides `authenticatedPage` (handles setup/login race conditions across parallel workers)
|
- Auth fixture: `tests/e2e/helpers/fixtures.ts` provides `authenticatedPage` (handles setup/login race conditions across parallel workers)
|
||||||
|
- **Shared login state**: `tests/e2e/auth.setup.ts` (project `login-state`) logs in once and saves the admin session cookie to `data/test-storage-state.json`; the `app` project loads it into every context so tests start authenticated instead of submitting the login form each time. A spec that needs an anonymous baseline opts out with `test.use({ storageState: { cookies: [], origins: [] } })` (see `pwa.spec.ts`, `mcp.spec.ts`). Never write a test that revokes or logs out the admin's own session — it would break every test in other workers
|
||||||
- Auth tests use `test.describe.serial` because they depend on sequential database state
|
- Auth tests use `test.describe.serial` because they depend on sequential database state
|
||||||
- **Offline/online tests**: After `setOffline(false)`, always manually dispatch the `online` event via `page.evaluate(() => window.dispatchEvent(new Event('online')))` — Playwright doesn't reliably emit it in headless/CI environments, causing flaky `waitForResponse` timeouts
|
- **Offline/online tests**: After `setOffline(false)`, always manually dispatch the `online` event via `page.evaluate(() => window.dispatchEvent(new Event('online')))` — Playwright doesn't reliably emit it in headless/CI environments, causing flaky `waitForResponse` timeouts
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -1,4 +1,5 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import { STORAGE_STATE_FILE } from './tests/e2e/global-setup';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests/e2e',
|
testDir: './tests/e2e',
|
||||||
@@ -22,10 +23,20 @@ export default defineConfig({
|
|||||||
use: { ...devices['Desktop Chrome'] }
|
use: { ...devices['Desktop Chrome'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'app',
|
// Logs in once and saves the session cookie for the `app` project.
|
||||||
testIgnore: 'auth.spec.ts',
|
name: 'login-state',
|
||||||
|
testMatch: 'auth.setup.ts',
|
||||||
dependencies: ['auth-setup'],
|
dependencies: ['auth-setup'],
|
||||||
use: { ...devices['Desktop Chrome'] }
|
use: { ...devices['Desktop Chrome'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'app',
|
||||||
|
testIgnore: ['auth.spec.ts', 'auth.setup.ts'],
|
||||||
|
dependencies: ['login-state'],
|
||||||
|
// Every context starts with the admin already logged in. Specs that
|
||||||
|
// need an unauthenticated baseline opt out with
|
||||||
|
// `test.use({ storageState: { cookies: [], origins: [] } })`.
|
||||||
|
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE_FILE }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
webServer: {
|
webServer: {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { test as setup } from '@playwright/test';
|
||||||
|
import { setupAndLogin } from './helpers/fixtures';
|
||||||
|
import { STORAGE_STATE_FILE } from './global-setup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in as the admin once and persist the session cookie. The `app` project
|
||||||
|
* loads this storage state into every browser context, so tests start already
|
||||||
|
* authenticated instead of each one submitting the login form (~0.8s per test,
|
||||||
|
* roughly half of a typical test's duration).
|
||||||
|
*
|
||||||
|
* The admin's own session is never revoked by any test: admin-users only
|
||||||
|
* resets/revokes victim accounts, and auth.spec's logout scenario runs in the
|
||||||
|
* auth-setup project with its own context before this setup executes.
|
||||||
|
*/
|
||||||
|
setup('authenticate as admin and save storage state', async ({ page }) => {
|
||||||
|
await setupAndLogin(page);
|
||||||
|
await page.context().storageState({ path: STORAGE_STATE_FILE });
|
||||||
|
});
|
||||||
@@ -2,6 +2,8 @@ import { writeFileSync } from 'fs';
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
export const TEST_CREDENTIALS_FILE = './data/test-credentials.json';
|
export const TEST_CREDENTIALS_FILE = './data/test-credentials.json';
|
||||||
|
/** Admin session cookie captured by auth.setup.ts and shared by the `app` project. */
|
||||||
|
export const STORAGE_STATE_FILE = './data/test-storage-state.json';
|
||||||
|
|
||||||
export default function globalSetup() {
|
export default function globalSetup() {
|
||||||
// The test database is wiped before the webServer starts (see the
|
// The test database is wiped before the webServer starts (see the
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { setupAndLogin } from './helpers/fixtures.js';
|
|||||||
* 3. Test tool invocations via JSON-RPC over Streamable HTTP
|
* 3. Test tool invocations via JSON-RPC over Streamable HTTP
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Start unauthenticated: the "rejects requests without API key" scenario must
|
||||||
|
// not carry the shared admin session cookie, and the apiKey fixture logs in itself.
|
||||||
|
base.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
const test = base.extend<{ apiKey: string }>({
|
const test = base.extend<{ apiKey: string }>({
|
||||||
apiKey: async ({ page }, use) => {
|
apiKey: async ({ page }, use) => {
|
||||||
await setupAndLogin(page);
|
await setupAndLogin(page);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
// These scenarios describe what an anonymous visitor receives, so drop the
|
||||||
|
// shared admin session the `app` project injects.
|
||||||
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
test.describe('PWA', () => {
|
test.describe('PWA', () => {
|
||||||
test('Scenario: Web manifest provides correct app metadata', async ({ page }) => {
|
test('Scenario: Web manifest provides correct app metadata', async ({ page }) => {
|
||||||
// When the browser requests the web manifest
|
// When the browser requests the web manifest
|
||||||
|
|||||||
Reference in New Issue
Block a user