thevoidforge 21.0.11 → 21.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.claude/commands/ai.md +69 -0
- package/dist/.claude/commands/architect.md +121 -0
- package/dist/.claude/commands/assemble.md +201 -0
- package/dist/.claude/commands/assess.md +75 -0
- package/dist/.claude/commands/blueprint.md +135 -0
- package/dist/.claude/commands/build.md +116 -0
- package/dist/.claude/commands/campaign.md +201 -0
- package/dist/.claude/commands/cultivation.md +166 -0
- package/dist/.claude/commands/current.md +128 -0
- package/dist/.claude/commands/dangerroom.md +74 -0
- package/dist/.claude/commands/debrief.md +178 -0
- package/dist/.claude/commands/deploy.md +99 -0
- package/dist/.claude/commands/devops.md +143 -0
- package/dist/.claude/commands/gauntlet.md +140 -0
- package/dist/.claude/commands/git.md +104 -0
- package/dist/.claude/commands/grow.md +146 -0
- package/dist/.claude/commands/imagine.md +126 -0
- package/dist/.claude/commands/portfolio.md +50 -0
- package/dist/.claude/commands/prd.md +113 -0
- package/dist/.claude/commands/qa.md +107 -0
- package/dist/.claude/commands/review.md +151 -0
- package/dist/.claude/commands/security.md +100 -0
- package/dist/.claude/commands/test.md +96 -0
- package/dist/.claude/commands/thumper.md +116 -0
- package/dist/.claude/commands/treasury.md +100 -0
- package/dist/.claude/commands/ux.md +118 -0
- package/dist/.claude/commands/vault.md +189 -0
- package/dist/.claude/commands/void.md +108 -0
- package/dist/CHANGELOG.md +1918 -0
- package/dist/CLAUDE.md +250 -0
- package/dist/HOLOCRON.md +856 -0
- package/dist/VERSION.md +123 -0
- package/dist/docs/NAMING_REGISTRY.md +478 -0
- package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
- package/dist/docs/methods/ASSEMBLER.md +142 -0
- package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
- package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
- package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
- package/dist/docs/methods/CAMPAIGN.md +568 -0
- package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
- package/dist/docs/methods/DEEP_CURRENT.md +184 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
- package/dist/docs/methods/FIELD_MEDIC.md +261 -0
- package/dist/docs/methods/FORGE_ARTIST.md +108 -0
- package/dist/docs/methods/FORGE_KEEPER.md +268 -0
- package/dist/docs/methods/GAUNTLET.md +344 -0
- package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
- package/dist/docs/methods/HEARTBEAT.md +168 -0
- package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
- package/dist/docs/methods/MUSTER.md +148 -0
- package/dist/docs/methods/PRD_GENERATOR.md +186 -0
- package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
- package/dist/docs/methods/QA_ENGINEER.md +337 -0
- package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
- package/dist/docs/methods/SUB_AGENTS.md +335 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
- package/dist/docs/methods/TESTING.md +359 -0
- package/dist/docs/methods/THUMPER.md +175 -0
- package/dist/docs/methods/TIME_VAULT.md +120 -0
- package/dist/docs/methods/TREASURY.md +184 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
- package/dist/docs/patterns/README.md +52 -0
- package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
- package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
- package/dist/docs/patterns/ai-classifier.ts +195 -0
- package/dist/docs/patterns/ai-eval.ts +272 -0
- package/dist/docs/patterns/ai-orchestrator.ts +341 -0
- package/dist/docs/patterns/ai-router.ts +194 -0
- package/dist/docs/patterns/ai-tool-schema.ts +237 -0
- package/dist/docs/patterns/api-route.ts +241 -0
- package/dist/docs/patterns/backtest-engine.ts +499 -0
- package/dist/docs/patterns/browser-review.ts +292 -0
- package/dist/docs/patterns/combobox.tsx +300 -0
- package/dist/docs/patterns/component.tsx +262 -0
- package/dist/docs/patterns/daemon-process.ts +338 -0
- package/dist/docs/patterns/data-pipeline.ts +297 -0
- package/dist/docs/patterns/database-migration.ts +466 -0
- package/dist/docs/patterns/e2e-test.ts +629 -0
- package/dist/docs/patterns/error-handling.ts +312 -0
- package/dist/docs/patterns/execution-safety.ts +601 -0
- package/dist/docs/patterns/financial-transaction.ts +342 -0
- package/dist/docs/patterns/funding-plan.ts +462 -0
- package/dist/docs/patterns/game-entity.ts +137 -0
- package/dist/docs/patterns/game-loop.ts +113 -0
- package/dist/docs/patterns/game-state.ts +143 -0
- package/dist/docs/patterns/job-queue.ts +225 -0
- package/dist/docs/patterns/kongo-integration.ts +164 -0
- package/dist/docs/patterns/middleware.ts +363 -0
- package/dist/docs/patterns/mobile-screen.tsx +139 -0
- package/dist/docs/patterns/mobile-service.ts +167 -0
- package/dist/docs/patterns/multi-tenant.ts +382 -0
- package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
- package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
- package/dist/docs/patterns/prompt-template.ts +195 -0
- package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
- package/dist/docs/patterns/service.ts +224 -0
- package/dist/docs/patterns/sse-endpoint.ts +118 -0
- package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
- package/dist/docs/patterns/third-party-script.ts +68 -0
- package/dist/scripts/thumper/gom-jabbar.sh +241 -0
- package/dist/scripts/thumper/relay.sh +610 -0
- package/dist/scripts/thumper/scan.sh +359 -0
- package/dist/scripts/thumper/thumper.sh +190 -0
- package/dist/scripts/thumper/water-rings.sh +76 -0
- package/package.json +1 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: E2E Testing with Playwright
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Page Object Model for test organization
|
|
6
|
+
* - axe-core a11y scan as default fixture (every page gets a free a11y check)
|
|
7
|
+
* - Deterministic state: clean temp dir per test, explicit waits only
|
|
8
|
+
* - Network mocking for external APIs
|
|
9
|
+
* - Auth helper for authenticated flows
|
|
10
|
+
* - CWV measurement (opt-in)
|
|
11
|
+
* - Flaky test management: retry, quarantine, no waitForTimeout
|
|
12
|
+
*
|
|
13
|
+
* Testing pyramid position: Unit (many, fast) → Integration (moderate) → E2E (few, slow, critical paths only)
|
|
14
|
+
* Target: 10-15 E2E tests for a typical full-stack app. Max 2 min CI time.
|
|
15
|
+
*
|
|
16
|
+
* Agents: Batman (test strategy), Samwise (a11y), Éowyn (enchantment), Nightwing (regression)
|
|
17
|
+
* Hawkeye (Gauntlet Round 2.5 smoke tests)
|
|
18
|
+
*
|
|
19
|
+
* Framework adaptations:
|
|
20
|
+
* Next.js: webServer.command = 'next dev -p 3199' (or 'next build && next start')
|
|
21
|
+
* Express: webServer.command = 'PORT=3199 npx tsx src/server.ts'
|
|
22
|
+
* Django: webServer.command = 'python manage.py runserver 3199 --settings=project.settings.test'
|
|
23
|
+
* Rails: webServer.command = 'RAILS_ENV=test bin/rails server -p 3199'
|
|
24
|
+
*
|
|
25
|
+
* === Django Adaptation ===
|
|
26
|
+
*
|
|
27
|
+
* # playwright.config.ts — same structure, different webServer
|
|
28
|
+
* webServer: {
|
|
29
|
+
* command: 'python manage.py runserver 3199 --settings=myapp.settings.test',
|
|
30
|
+
* port: 3199,
|
|
31
|
+
* reuseExistingServer: !process.env.CI,
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* # Auth helper: POST to Django login endpoint or use session cookie directly
|
|
35
|
+
* # Network mocking: same Playwright route.fulfill() — framework doesn't matter
|
|
36
|
+
* # a11y: same axe-core fixture — it runs in the browser, not the server
|
|
37
|
+
*
|
|
38
|
+
* === Key Rules ===
|
|
39
|
+
*
|
|
40
|
+
* 1. NEVER use waitForTimeout() — it's a flake factory. Use explicit waits:
|
|
41
|
+
* await page.waitForSelector(), await expect(locator).toBeVisible()
|
|
42
|
+
* 2. NEVER use networkidle — it's non-deterministic. Wait for specific elements.
|
|
43
|
+
* 3. ONE assertion concept per test. Split journeys into focused test cases.
|
|
44
|
+
* 4. Clean state per test. Use beforeEach to reset, not afterAll to teardown.
|
|
45
|
+
* 5. E2E tests are expensive — only test critical user journeys.
|
|
46
|
+
* Unit/integration tests cover edge cases, validation, and error handling.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { test as base, expect, type Page, type BrowserContext } from '@playwright/test';
|
|
50
|
+
import AxeBuilder from '@axe-core/playwright';
|
|
51
|
+
|
|
52
|
+
// ── Types ───────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** Auth session info returned from login helper. */
|
|
55
|
+
interface AuthSession {
|
|
56
|
+
token: string;
|
|
57
|
+
userId: string;
|
|
58
|
+
cookies: Array<{
|
|
59
|
+
name: string;
|
|
60
|
+
value: string;
|
|
61
|
+
domain: string;
|
|
62
|
+
path: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Core Web Vitals measurement result. */
|
|
67
|
+
interface CWVResult {
|
|
68
|
+
lcp: number | null; // Largest Contentful Paint (ms)
|
|
69
|
+
fid: number | null; // First Input Delay (ms)
|
|
70
|
+
cls: number | null; // Cumulative Layout Shift (score)
|
|
71
|
+
ttfb: number | null; // Time to First Byte (ms)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── axe-core Fixture ────────────────────────────────
|
|
75
|
+
// Every test that uses { page } also gets a free a11y scan helper.
|
|
76
|
+
// Import `test` from this file instead of @playwright/test.
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Known pre-existing a11y violations. Track these for resolution —
|
|
80
|
+
* excluding them prevents false failures while still catching regressions.
|
|
81
|
+
* Remove rules from this list as they're fixed.
|
|
82
|
+
*/
|
|
83
|
+
const KNOWN_A11Y_EXCLUSIONS: string[] = [
|
|
84
|
+
// 'color-contrast', // Example: dark theme needs design review
|
|
85
|
+
// 'landmark-one-main', // Example: some pages lack <main>
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export const test = base.extend<{
|
|
89
|
+
axe: AxeBuilder;
|
|
90
|
+
expectAccessible: (page: Page) => Promise<void>;
|
|
91
|
+
}>({
|
|
92
|
+
axe: async ({ page }, use) => {
|
|
93
|
+
await use(new AxeBuilder({ page }));
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
expectAccessible: async ({}, use) => {
|
|
97
|
+
await use(async (targetPage: Page) => {
|
|
98
|
+
const results = await new AxeBuilder({ page: targetPage })
|
|
99
|
+
.disableRules(KNOWN_A11Y_EXCLUSIONS)
|
|
100
|
+
.analyze();
|
|
101
|
+
|
|
102
|
+
if (results.violations.length > 0) {
|
|
103
|
+
const summary = results.violations
|
|
104
|
+
.map(
|
|
105
|
+
(v) =>
|
|
106
|
+
`[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} node${v.nodes.length === 1 ? '' : 's'})`
|
|
107
|
+
)
|
|
108
|
+
.join('\n ');
|
|
109
|
+
throw new Error(`Accessibility violations:\n ${summary}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export { expect };
|
|
116
|
+
|
|
117
|
+
// ── Page Object Model ───────────────────────────────
|
|
118
|
+
// Encapsulate page interaction behind a stable API.
|
|
119
|
+
// When selectors change, update the POM — not every test.
|
|
120
|
+
|
|
121
|
+
export class LoginPage {
|
|
122
|
+
constructor(private page: Page) {}
|
|
123
|
+
|
|
124
|
+
async goto(): Promise<void> {
|
|
125
|
+
await this.page.goto('/login');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async fillEmail(email: string): Promise<void> {
|
|
129
|
+
await this.page.getByLabel('Email').fill(email);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async fillPassword(password: string): Promise<void> {
|
|
133
|
+
await this.page.getByLabel('Password').fill(password);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async submit(): Promise<void> {
|
|
137
|
+
await this.page.getByRole('button', { name: /sign in|log in/i }).click();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async login(email: string, password: string): Promise<void> {
|
|
141
|
+
await this.fillEmail(email);
|
|
142
|
+
await this.fillPassword(password);
|
|
143
|
+
await this.submit();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async expectError(message: string | RegExp): Promise<void> {
|
|
147
|
+
await expect(this.page.getByRole('alert')).toContainText(message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async expectRedirectToDashboard(): Promise<void> {
|
|
151
|
+
await this.page.waitForURL('**/dashboard**');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export class DashboardPage {
|
|
156
|
+
constructor(private page: Page) {}
|
|
157
|
+
|
|
158
|
+
async goto(): Promise<void> {
|
|
159
|
+
await this.page.goto('/dashboard');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async expectLoaded(): Promise<void> {
|
|
163
|
+
// Wait for a specific element, not networkidle
|
|
164
|
+
await expect(this.page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async expectProjectVisible(name: string): Promise<void> {
|
|
168
|
+
await expect(this.page.getByText(name)).toBeVisible();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async createProject(name: string): Promise<void> {
|
|
172
|
+
await this.page.getByRole('button', { name: /create|new project/i }).click();
|
|
173
|
+
await this.page.getByLabel('Project name').fill(name);
|
|
174
|
+
await this.page.getByRole('button', { name: /create|save/i }).click();
|
|
175
|
+
// Wait for the project to appear — explicit, not timeout
|
|
176
|
+
await expect(this.page.getByText(name)).toBeVisible();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Auth Helper ─────────────────────────────────────
|
|
181
|
+
// Login via API (fast), not via UI (slow). Reuse session across tests.
|
|
182
|
+
// Store auth state to a file so Playwright can load it without re-logging in.
|
|
183
|
+
|
|
184
|
+
const AUTH_STATE_PATH = 'e2e/.auth/session.json';
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Authenticate via API and save session state for reuse.
|
|
188
|
+
* Call once in a setup project, then reuse in test projects.
|
|
189
|
+
*
|
|
190
|
+
* playwright.config.ts:
|
|
191
|
+
* projects: [
|
|
192
|
+
* { name: 'setup', testMatch: /auth\.setup\.ts/ },
|
|
193
|
+
* { name: 'tests', dependencies: ['setup'], use: { storageState: AUTH_STATE_PATH } },
|
|
194
|
+
* ]
|
|
195
|
+
*/
|
|
196
|
+
export async function loginViaAPI(
|
|
197
|
+
context: BrowserContext,
|
|
198
|
+
baseURL: string,
|
|
199
|
+
credentials: { email: string; password: string }
|
|
200
|
+
): Promise<AuthSession> {
|
|
201
|
+
const response = await context.request.post(`${baseURL}/api/auth/login`, {
|
|
202
|
+
data: credentials,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!response.ok()) {
|
|
206
|
+
throw new Error(`Auth failed: ${response.status()} ${await response.text()}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const body = await response.json();
|
|
210
|
+
|
|
211
|
+
// Save browser state (cookies, localStorage) for reuse
|
|
212
|
+
await context.storageState({ path: AUTH_STATE_PATH });
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
token: body.token,
|
|
216
|
+
userId: body.userId,
|
|
217
|
+
cookies: await context.cookies(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Auth setup file — run once before all tests.
|
|
223
|
+
*
|
|
224
|
+
* // e2e/auth.setup.ts
|
|
225
|
+
* import { test as setup } from '@playwright/test';
|
|
226
|
+
* import { loginViaAPI } from './fixtures';
|
|
227
|
+
*
|
|
228
|
+
* setup('authenticate', async ({ context, baseURL }) => {
|
|
229
|
+
* await loginViaAPI(context, baseURL!, {
|
|
230
|
+
* email: 'test@example.com',
|
|
231
|
+
* password: 'test-password-123',
|
|
232
|
+
* });
|
|
233
|
+
* });
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
// ── Network Mocking ─────────────────────────────────
|
|
237
|
+
// Intercept external API calls to make tests deterministic.
|
|
238
|
+
// Mock at the network level, not the application level.
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Mock an external API endpoint.
|
|
242
|
+
*
|
|
243
|
+
* Usage:
|
|
244
|
+
* await mockAPI(page, 'https://api.stripe.com/v1/charges', {
|
|
245
|
+
* status: 200,
|
|
246
|
+
* body: { id: 'ch_123', amount: 1000, status: 'succeeded' },
|
|
247
|
+
* });
|
|
248
|
+
*/
|
|
249
|
+
export async function mockAPI(
|
|
250
|
+
page: Page,
|
|
251
|
+
urlPattern: string | RegExp,
|
|
252
|
+
response: {
|
|
253
|
+
status?: number;
|
|
254
|
+
body: Record<string, unknown>;
|
|
255
|
+
headers?: Record<string, string>;
|
|
256
|
+
delay?: number; // Simulate network latency (ms) — use sparingly
|
|
257
|
+
}
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
await page.route(urlPattern, async (route) => {
|
|
260
|
+
if (response.delay) {
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, response.delay));
|
|
262
|
+
}
|
|
263
|
+
await route.fulfill({
|
|
264
|
+
status: response.status ?? 200,
|
|
265
|
+
contentType: 'application/json',
|
|
266
|
+
headers: response.headers ?? {},
|
|
267
|
+
body: JSON.stringify(response.body),
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Mock a failing external API.
|
|
274
|
+
*
|
|
275
|
+
* Usage:
|
|
276
|
+
* await mockAPIError(page, /api\.stripe\.com/, 503, 'Service Unavailable');
|
|
277
|
+
*/
|
|
278
|
+
export async function mockAPIError(
|
|
279
|
+
page: Page,
|
|
280
|
+
urlPattern: string | RegExp,
|
|
281
|
+
status: number,
|
|
282
|
+
message: string
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
await page.route(urlPattern, (route) =>
|
|
285
|
+
route.fulfill({
|
|
286
|
+
status,
|
|
287
|
+
contentType: 'application/json',
|
|
288
|
+
body: JSON.stringify({ error: { message } }),
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Abort external requests entirely — simulates network failure.
|
|
295
|
+
*
|
|
296
|
+
* Usage:
|
|
297
|
+
* await mockNetworkFailure(page, /api\.external\.com/);
|
|
298
|
+
*/
|
|
299
|
+
export async function mockNetworkFailure(
|
|
300
|
+
page: Page,
|
|
301
|
+
urlPattern: string | RegExp
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
await page.route(urlPattern, (route) => route.abort('connectionrefused'));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── WebSocket Mock ──────────────────────────────────
|
|
307
|
+
// Intercept WebSocket connections for real-time feature testing.
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Mock a WebSocket connection.
|
|
311
|
+
*
|
|
312
|
+
* Usage:
|
|
313
|
+
* const wsMock = await mockWebSocket(page, 'wss://api.example.com/ws');
|
|
314
|
+
* wsMock.send({ type: 'message', data: 'hello' });
|
|
315
|
+
* wsMock.close();
|
|
316
|
+
*
|
|
317
|
+
* Note: Playwright's native WebSocket mocking (page.routeWebSocket) was added
|
|
318
|
+
* in Playwright 1.48. For older versions, use a local WebSocket test server.
|
|
319
|
+
*/
|
|
320
|
+
export async function mockWebSocket(
|
|
321
|
+
page: Page,
|
|
322
|
+
url: string | RegExp
|
|
323
|
+
): Promise<{
|
|
324
|
+
send: (data: Record<string, unknown>) => void;
|
|
325
|
+
close: () => void;
|
|
326
|
+
onMessage: (handler: (data: string) => void) => void;
|
|
327
|
+
}> {
|
|
328
|
+
const handlers: Array<(data: string) => void> = [];
|
|
329
|
+
|
|
330
|
+
// Playwright 1.48+ — use page.routeWebSocket
|
|
331
|
+
const ws = await page.routeWebSocket(url, (ws) => {
|
|
332
|
+
ws.onMessage((message) => {
|
|
333
|
+
for (const handler of handlers) {
|
|
334
|
+
handler(typeof message === 'string' ? message : message.toString());
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
send: (data) => {
|
|
341
|
+
// Send data from the "server" side to the client
|
|
342
|
+
ws.send(JSON.stringify(data));
|
|
343
|
+
},
|
|
344
|
+
close: () => {
|
|
345
|
+
ws.close();
|
|
346
|
+
},
|
|
347
|
+
onMessage: (handler) => {
|
|
348
|
+
handlers.push(handler);
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Core Web Vitals Measurement ─────────────────────
|
|
354
|
+
// Opt-in CWV measurement for performance-critical pages.
|
|
355
|
+
// Not a pass/fail gate by default — use for tracking and regression detection.
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Measure Core Web Vitals on a page.
|
|
359
|
+
*
|
|
360
|
+
* Usage:
|
|
361
|
+
* const cwv = await measureCWV(page);
|
|
362
|
+
* expect(cwv.lcp).toBeLessThan(2500); // Good LCP threshold
|
|
363
|
+
* expect(cwv.cls).toBeLessThan(0.1); // Good CLS threshold
|
|
364
|
+
*
|
|
365
|
+
* Note: FID requires user interaction — use INP (Interaction to Next Paint) for
|
|
366
|
+
* automated measurement. This helper measures LCP, CLS, and TTFB reliably.
|
|
367
|
+
* FID is returned as null unless explicit interaction triggers it.
|
|
368
|
+
*/
|
|
369
|
+
export async function measureCWV(page: Page): Promise<CWVResult> {
|
|
370
|
+
return await page.evaluate(() => {
|
|
371
|
+
return new Promise<CWVResult>((resolve) => {
|
|
372
|
+
const result: CWVResult = { lcp: null, fid: null, cls: null, ttfb: null };
|
|
373
|
+
|
|
374
|
+
// TTFB — available immediately from Navigation Timing API
|
|
375
|
+
const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
376
|
+
if (navEntry) {
|
|
377
|
+
result.ttfb = navEntry.responseStart - navEntry.requestStart;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// LCP — observe until we get a stable value
|
|
381
|
+
const lcpObserver = new PerformanceObserver((list) => {
|
|
382
|
+
const entries = list.getEntries();
|
|
383
|
+
if (entries.length > 0) {
|
|
384
|
+
result.lcp = entries[entries.length - 1]!.startTime;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
388
|
+
|
|
389
|
+
// CLS — accumulate layout shift scores
|
|
390
|
+
let clsScore = 0;
|
|
391
|
+
const clsObserver = new PerformanceObserver((list) => {
|
|
392
|
+
for (const entry of list.getEntries()) {
|
|
393
|
+
const layoutShift = entry as PerformanceEntry & { hadRecentInput: boolean; value: number };
|
|
394
|
+
if (!layoutShift.hadRecentInput) {
|
|
395
|
+
clsScore += layoutShift.value;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
result.cls = clsScore;
|
|
399
|
+
});
|
|
400
|
+
clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
401
|
+
|
|
402
|
+
// Collect after a short stabilization period
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
lcpObserver.disconnect();
|
|
405
|
+
clsObserver.disconnect();
|
|
406
|
+
resolve(result);
|
|
407
|
+
}, 3000);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* CWV thresholds per Google's "Good" classification.
|
|
414
|
+
* Use with expect: expect(cwv.lcp).toBeLessThan(CWV_THRESHOLDS.lcp.good)
|
|
415
|
+
*/
|
|
416
|
+
export const CWV_THRESHOLDS = {
|
|
417
|
+
lcp: { good: 2500, needsImprovement: 4000 }, // ms
|
|
418
|
+
fid: { good: 100, needsImprovement: 300 }, // ms
|
|
419
|
+
cls: { good: 0.1, needsImprovement: 0.25 }, // score
|
|
420
|
+
ttfb: { good: 800, needsImprovement: 1800 }, // ms
|
|
421
|
+
} as const;
|
|
422
|
+
|
|
423
|
+
// ── Flaky Test Annotation ───────────────────────────
|
|
424
|
+
// Use the @flaky tag to mark tests with known intermittent failures.
|
|
425
|
+
// Quarantined tests run in a separate CI job and don't block the main pipeline.
|
|
426
|
+
//
|
|
427
|
+
// Protocol:
|
|
428
|
+
// 1. Test fails intermittently (3+ times in a week) → add @flaky tag
|
|
429
|
+
// 2. Huntress (stability monitor) tracks flake rate
|
|
430
|
+
// 3. Quarantined tests run in a separate "flaky" CI job
|
|
431
|
+
// 4. Fix the root cause → remove @flaky tag → test returns to main suite
|
|
432
|
+
// 5. If unfixable after 2 weeks → rewrite the test or demote to manual
|
|
433
|
+
//
|
|
434
|
+
// NEVER use @flaky to suppress real bugs. The test must pass consistently
|
|
435
|
+
// when the feature works correctly — flakiness comes from test infrastructure,
|
|
436
|
+
// timing, or external dependencies, not from product bugs.
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Mark a test as flaky with a tracking reason.
|
|
440
|
+
*
|
|
441
|
+
* Usage:
|
|
442
|
+
* test('payment flow completes', {
|
|
443
|
+
* tag: ['@flaky'],
|
|
444
|
+
* annotation: { type: 'flaky', description: 'Stripe webhook timing — tracking in #234' },
|
|
445
|
+
* }, async ({ page }) => { ... });
|
|
446
|
+
*
|
|
447
|
+
* In playwright.config.ts, separate flaky tests into their own project:
|
|
448
|
+
* projects: [
|
|
449
|
+
* { name: 'stable', grep: /^(?!.*@flaky)/, retries: 1 },
|
|
450
|
+
* { name: 'flaky', grep: /@flaky/, retries: 3 },
|
|
451
|
+
* ]
|
|
452
|
+
*/
|
|
453
|
+
|
|
454
|
+
// ── Example Tests ───────────────────────────────────
|
|
455
|
+
// These show the complete pattern for common E2E scenarios.
|
|
456
|
+
|
|
457
|
+
/*
|
|
458
|
+
// ── Example 1: Login flow with Page Object Model ───
|
|
459
|
+
|
|
460
|
+
import { test, expect } from './fixtures';
|
|
461
|
+
import { LoginPage, DashboardPage } from './fixtures';
|
|
462
|
+
|
|
463
|
+
test.describe('Authentication', () => {
|
|
464
|
+
test('user can log in with valid credentials', async ({ page, expectAccessible }) => {
|
|
465
|
+
const loginPage = new LoginPage(page);
|
|
466
|
+
const dashboard = new DashboardPage(page);
|
|
467
|
+
|
|
468
|
+
await loginPage.goto();
|
|
469
|
+
await loginPage.login('user@example.com', 'password123');
|
|
470
|
+
await dashboard.expectLoaded();
|
|
471
|
+
|
|
472
|
+
// Every page gets a free a11y check
|
|
473
|
+
await expectAccessible(page);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('shows error on invalid credentials', async ({ page, expectAccessible }) => {
|
|
477
|
+
const loginPage = new LoginPage(page);
|
|
478
|
+
|
|
479
|
+
await loginPage.goto();
|
|
480
|
+
await loginPage.login('user@example.com', 'wrong-password');
|
|
481
|
+
await loginPage.expectError(/invalid credentials/i);
|
|
482
|
+
|
|
483
|
+
await expectAccessible(page);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// ── Example 2: CRUD flow with network mocking ──────
|
|
488
|
+
|
|
489
|
+
import { test, expect, mockAPI } from './fixtures';
|
|
490
|
+
|
|
491
|
+
test.describe('Projects', () => {
|
|
492
|
+
test('creates a project and sees it in the list', async ({ page, expectAccessible }) => {
|
|
493
|
+
const dashboard = new DashboardPage(page);
|
|
494
|
+
await dashboard.goto();
|
|
495
|
+
await dashboard.createProject('My New Project');
|
|
496
|
+
await dashboard.expectProjectVisible('My New Project');
|
|
497
|
+
|
|
498
|
+
await expectAccessible(page);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('handles API failure gracefully', async ({ page, expectAccessible }) => {
|
|
502
|
+
// Mock the project creation endpoint to fail
|
|
503
|
+
await mockAPI(page, '**/api/projects', {
|
|
504
|
+
status: 500,
|
|
505
|
+
body: { error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } },
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const dashboard = new DashboardPage(page);
|
|
509
|
+
await dashboard.goto();
|
|
510
|
+
|
|
511
|
+
await page.getByRole('button', { name: /create|new project/i }).click();
|
|
512
|
+
await page.getByLabel('Project name').fill('Doomed Project');
|
|
513
|
+
await page.getByRole('button', { name: /create|save/i }).click();
|
|
514
|
+
|
|
515
|
+
// Verify error state is shown — not a blank screen
|
|
516
|
+
await expect(page.getByRole('alert')).toBeVisible();
|
|
517
|
+
|
|
518
|
+
await expectAccessible(page);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ── Example 3: WebSocket real-time updates ──────────
|
|
523
|
+
|
|
524
|
+
import { test, expect, mockWebSocket } from './fixtures';
|
|
525
|
+
|
|
526
|
+
test('receives real-time notifications via WebSocket', async ({ page }) => {
|
|
527
|
+
const ws = await mockWebSocket(page, /\/ws\/notifications/);
|
|
528
|
+
|
|
529
|
+
await page.goto('/dashboard');
|
|
530
|
+
|
|
531
|
+
// Server pushes a notification
|
|
532
|
+
ws.send({ type: 'notification', title: 'Build complete', level: 'success' });
|
|
533
|
+
|
|
534
|
+
// Verify it appears in the UI
|
|
535
|
+
await expect(page.getByText('Build complete')).toBeVisible();
|
|
536
|
+
|
|
537
|
+
ws.close();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// ── Example 4: CWV measurement (opt-in) ────────────
|
|
541
|
+
|
|
542
|
+
import { test, expect, measureCWV, CWV_THRESHOLDS } from './fixtures';
|
|
543
|
+
|
|
544
|
+
test('landing page meets CWV thresholds', async ({ page }) => {
|
|
545
|
+
await page.goto('/');
|
|
546
|
+
|
|
547
|
+
const cwv = await measureCWV(page);
|
|
548
|
+
|
|
549
|
+
// These are aspirational — adjust thresholds to your project
|
|
550
|
+
if (cwv.lcp !== null) {
|
|
551
|
+
expect(cwv.lcp).toBeLessThan(CWV_THRESHOLDS.lcp.good);
|
|
552
|
+
}
|
|
553
|
+
if (cwv.cls !== null) {
|
|
554
|
+
expect(cwv.cls).toBeLessThan(CWV_THRESHOLDS.cls.good);
|
|
555
|
+
}
|
|
556
|
+
if (cwv.ttfb !== null) {
|
|
557
|
+
expect(cwv.ttfb).toBeLessThan(CWV_THRESHOLDS.ttfb.good);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
*/
|
|
561
|
+
|
|
562
|
+
// ── Playwright Config Reference ─────────────────────
|
|
563
|
+
// playwright.config.ts — adapt webServer.command to your framework.
|
|
564
|
+
//
|
|
565
|
+
// import { defineConfig, devices } from '@playwright/test';
|
|
566
|
+
//
|
|
567
|
+
// export default defineConfig({
|
|
568
|
+
// testDir: './e2e',
|
|
569
|
+
// forbidOnly: !!process.env.CI,
|
|
570
|
+
// retries: 1,
|
|
571
|
+
// reporter: process.env.CI ? 'github' : 'list',
|
|
572
|
+
//
|
|
573
|
+
// use: {
|
|
574
|
+
// baseURL: 'http://127.0.0.1:3199',
|
|
575
|
+
// // Network isolation — block external requests in browser
|
|
576
|
+
// launchOptions: {
|
|
577
|
+
// args: ['--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE 127.0.0.1'],
|
|
578
|
+
// },
|
|
579
|
+
// trace: 'on-first-retry',
|
|
580
|
+
// },
|
|
581
|
+
//
|
|
582
|
+
// projects: [
|
|
583
|
+
// // Auth setup runs first, saves session state
|
|
584
|
+
// { name: 'setup', testMatch: /auth\.setup\.ts/ },
|
|
585
|
+
//
|
|
586
|
+
// // Stable tests use saved auth, retry once
|
|
587
|
+
// {
|
|
588
|
+
// name: 'stable',
|
|
589
|
+
// dependencies: ['setup'],
|
|
590
|
+
// use: { storageState: 'e2e/.auth/session.json' },
|
|
591
|
+
// grep: /^(?!.*@flaky)/,
|
|
592
|
+
// retries: 1,
|
|
593
|
+
// },
|
|
594
|
+
//
|
|
595
|
+
// // Flaky tests get more retries, don't block the pipeline
|
|
596
|
+
// {
|
|
597
|
+
// name: 'flaky',
|
|
598
|
+
// dependencies: ['setup'],
|
|
599
|
+
// use: { storageState: 'e2e/.auth/session.json' },
|
|
600
|
+
// grep: /@flaky/,
|
|
601
|
+
// retries: 3,
|
|
602
|
+
// },
|
|
603
|
+
// ],
|
|
604
|
+
//
|
|
605
|
+
// // Framework-specific webServer commands:
|
|
606
|
+
// webServer: {
|
|
607
|
+
// // Next.js: command: 'next dev -p 3199',
|
|
608
|
+
// // Express: command: 'PORT=3199 npx tsx src/server.ts',
|
|
609
|
+
// // Django: command: 'python manage.py runserver 3199 --settings=project.settings.test',
|
|
610
|
+
// // Rails: command: 'RAILS_ENV=test bin/rails server -p 3199',
|
|
611
|
+
// command: 'PORT=3199 npx tsx src/server.ts',
|
|
612
|
+
// port: 3199,
|
|
613
|
+
// reuseExistingServer: !process.env.CI,
|
|
614
|
+
// timeout: 30_000,
|
|
615
|
+
// },
|
|
616
|
+
// });
|
|
617
|
+
|
|
618
|
+
// ── Setup Checklist ─────────────────────────────────
|
|
619
|
+
// When adding E2E tests to a project:
|
|
620
|
+
//
|
|
621
|
+
// - [ ] npm i -D @playwright/test @axe-core/playwright
|
|
622
|
+
// - [ ] npx playwright install chromium
|
|
623
|
+
// - [ ] Create playwright.config.ts (adapt webServer command)
|
|
624
|
+
// - [ ] Create e2e/fixtures.ts (copy axe fixture + auth helper from this pattern)
|
|
625
|
+
// - [ ] Create e2e/auth.setup.ts (login via API, save session)
|
|
626
|
+
// - [ ] Write first smoke test: page loads, no a11y violations
|
|
627
|
+
// - [ ] Add to CI: npx playwright test (separate job, max 2 min budget)
|
|
628
|
+
// - [ ] Add e2e/.auth/ to .gitignore
|
|
629
|
+
// - [ ] Add test-results/ and playwright-report/ to .gitignore
|