Skip to content

Commit b9c8ae9

Browse files
adewaleclaude
andcommitted
refactor(e2e): Extract shared test utilities and add lessons learned
## Changes ### New Files - `e2e/test-utils.ts`: Shared E2E test utilities with typed interfaces - `createSessionWithRetry()`: Handles intermittent API failures - `getSessionWithRetry()`: Handles KV eventual consistency - `SessionResponse` interface: Documents correct API response structure ### Updated Test Files - `e2e/session-race.spec.ts`: Use shared utilities - `e2e/session-persistence.spec.ts`: Use shared utilities - `e2e/connection-storm.spec.ts`: Use shared utilities - `e2e/multiplayer.spec.ts`: Use shared utilities ### Documentation - `docs/LESSONS-LEARNED.md`: Added two new lessons - Lesson 15: E2E Tests Must Use Correct API Response Structure - Lesson 16: CI Tests Need Retry Logic for API Resilience ## Background After debugging CI failures, we identified: 1. Tests were accessing `sessionData.tracks` instead of `sessionData.state.tracks` 2. Tests lacked retry logic for KV eventual consistency and API cold starts 3. The same patterns existed in multiple test files (code duplication) This refactor: - Creates a single source of truth for E2E test patterns - Documents the lessons to prevent recurrence - Makes future tests more resilient by default 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent cf24c94 commit b9c8ae9

File tree

6 files changed

+389
-218
lines changed

6 files changed

+389
-218
lines changed

app/e2e/connection-storm.spec.ts

Lines changed: 42 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
*/
1212

1313
import { test, expect, Page } from '@playwright/test';
14-
15-
const API_BASE = process.env.CI
16-
? 'https://keyboardia.adewale-883.workers.dev'
17-
: 'http://localhost:5173';
14+
import { API_BASE, createSessionWithRetry } from './test-utils';
1815

1916
/**
2017
* Helper to count WebSocket connections by monitoring DevTools.
@@ -43,31 +40,26 @@ async function setupWebSocketMonitor(page: Page): Promise<{ getStats: () => { co
4340
test.describe('Connection Storm Prevention', () => {
4441
test('rapid state changes do not cause WebSocket reconnections', async ({ page, request }) => {
4542
// Create a fresh session
46-
const createRes = await request.post(`${API_BASE}/api/sessions`, {
47-
data: {
48-
tracks: [
49-
{
50-
id: 'storm-test-track',
51-
name: 'Storm Test',
52-
sampleId: 'kick',
53-
steps: Array(16).fill(false),
54-
parameterLocks: Array(16).fill(null),
55-
volume: 1,
56-
muted: false,
57-
playbackMode: 'oneshot',
58-
transpose: 0,
59-
stepCount: 16,
60-
},
61-
],
62-
tempo: 120,
63-
swing: 0,
64-
version: 1,
65-
},
43+
const { id: sessionId } = await createSessionWithRetry(request, {
44+
tracks: [
45+
{
46+
id: 'storm-test-track',
47+
name: 'Storm Test',
48+
sampleId: 'kick',
49+
steps: Array(16).fill(false),
50+
parameterLocks: Array(16).fill(null),
51+
volume: 1,
52+
muted: false,
53+
playbackMode: 'oneshot',
54+
transpose: 0,
55+
stepCount: 16,
56+
},
57+
],
58+
tempo: 120,
59+
swing: 0,
60+
version: 1,
6661
});
6762

68-
expect(createRes.ok()).toBe(true);
69-
const { id: sessionId } = await createRes.json();
70-
7163
// Set up WebSocket monitoring
7264
const monitor = await setupWebSocketMonitor(page);
7365

@@ -140,31 +132,26 @@ test.describe('Connection Storm Prevention', () => {
140132

141133
test('debug overlay shows stable connection count during interactions', async ({ page, request }) => {
142134
// Create a fresh session
143-
const createRes = await request.post(`${API_BASE}/api/sessions`, {
144-
data: {
145-
tracks: [
146-
{
147-
id: 'debug-test-track',
148-
name: 'Debug Test',
149-
sampleId: 'snare',
150-
steps: Array(16).fill(false),
151-
parameterLocks: Array(16).fill(null),
152-
volume: 1,
153-
muted: false,
154-
playbackMode: 'oneshot',
155-
transpose: 0,
156-
stepCount: 16,
157-
},
158-
],
159-
tempo: 120,
160-
swing: 0,
161-
version: 1,
162-
},
135+
const { id: sessionId } = await createSessionWithRetry(request, {
136+
tracks: [
137+
{
138+
id: 'debug-test-track',
139+
name: 'Debug Test',
140+
sampleId: 'snare',
141+
steps: Array(16).fill(false),
142+
parameterLocks: Array(16).fill(null),
143+
volume: 1,
144+
muted: false,
145+
playbackMode: 'oneshot',
146+
transpose: 0,
147+
stepCount: 16,
148+
},
149+
],
150+
tempo: 120,
151+
swing: 0,
152+
version: 1,
163153
});
164154

165-
expect(createRes.ok()).toBe(true);
166-
const { id: sessionId } = await createRes.json();
167-
168155
// Navigate with debug mode enabled
169156
await page.goto(`${API_BASE}/s/${sessionId}?debug=1`);
170157
await page.waitForLoadState('networkidle');
@@ -218,18 +205,13 @@ test.describe('Connection Storm Prevention', () => {
218205
});
219206

220207
test('connection remains stable during tempo changes', async ({ page, request }) => {
221-
const createRes = await request.post(`${API_BASE}/api/sessions`, {
222-
data: {
223-
tracks: [],
224-
tempo: 120,
225-
swing: 0,
226-
version: 1,
227-
},
208+
const { id: sessionId } = await createSessionWithRetry(request, {
209+
tracks: [],
210+
tempo: 120,
211+
swing: 0,
212+
version: 1,
228213
});
229214

230-
expect(createRes.ok()).toBe(true);
231-
const { id: sessionId } = await createRes.json();
232-
233215
const monitor = await setupWebSocketMonitor(page);
234216

235217
await page.goto(`${API_BASE}/s/${sessionId}`);

app/e2e/multiplayer.spec.ts

Lines changed: 42 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { test, expect, BrowserContext, Page } from '@playwright/test';
2+
import { API_BASE, createSessionWithRetry } from './test-utils';
23

34
/**
45
* Multiplayer E2E tests - Phase 9-12 features
@@ -8,11 +9,6 @@ import { test, expect, BrowserContext, Page } from '@playwright/test';
89
* to the same Keyboardia session via WebSocket.
910
*/
1011

11-
// Use local dev server - multiplayer tests require live WebSocket connections
12-
const API_BASE = process.env.CI
13-
? 'https://keyboardia.adewale-883.workers.dev'
14-
: 'http://localhost:5173';
15-
1612
test.describe('Multiplayer real-time sync', () => {
1713
let context1: BrowserContext;
1814
let context2: BrowserContext;
@@ -22,30 +18,25 @@ test.describe('Multiplayer real-time sync', () => {
2218

2319
test.beforeEach(async ({ browser, request }) => {
2420
// Create a fresh session for each test
25-
const createRes = await request.post(`${API_BASE}/api/sessions`, {
26-
data: {
27-
tracks: [
28-
{
29-
id: 'mp-test-track',
30-
name: 'Test',
31-
sampleId: 'kick',
32-
steps: Array(64).fill(false),
33-
parameterLocks: Array(64).fill(null),
34-
volume: 1,
35-
muted: false,
36-
playbackMode: 'oneshot',
37-
transpose: 0,
38-
stepCount: 16,
39-
},
40-
],
41-
tempo: 120,
42-
swing: 0,
43-
version: 1,
44-
},
21+
const data = await createSessionWithRetry(request, {
22+
tracks: [
23+
{
24+
id: 'mp-test-track',
25+
name: 'Test',
26+
sampleId: 'kick',
27+
steps: Array(64).fill(false),
28+
parameterLocks: Array(64).fill(null),
29+
volume: 1,
30+
muted: false,
31+
playbackMode: 'oneshot',
32+
transpose: 0,
33+
stepCount: 16,
34+
},
35+
],
36+
tempo: 120,
37+
swing: 0,
38+
version: 1,
4539
});
46-
47-
expect(createRes.ok()).toBe(true);
48-
const data = await createRes.json();
4940
sessionId = data.id;
5041
console.log('[TEST] Created multiplayer test session:', sessionId);
5142

@@ -255,30 +246,26 @@ test.describe('Multiplayer real-time sync', () => {
255246
test.describe('Multiplayer connection resilience', () => {
256247
test('client reconnects after brief disconnection', async ({ browser, request }) => {
257248
// Create a session
258-
const createRes = await request.post(`${API_BASE}/api/sessions`, {
259-
data: {
260-
tracks: [
261-
{
262-
id: 'reconnect-test',
263-
name: 'Test',
264-
sampleId: 'kick',
265-
steps: Array(64).fill(false),
266-
parameterLocks: Array(64).fill(null),
267-
volume: 1,
268-
muted: false,
269-
playbackMode: 'oneshot',
270-
transpose: 0,
271-
stepCount: 16,
272-
},
273-
],
274-
tempo: 120,
275-
swing: 0,
276-
version: 1,
277-
},
249+
const { id: sessionId } = await createSessionWithRetry(request, {
250+
tracks: [
251+
{
252+
id: 'reconnect-test',
253+
name: 'Test',
254+
sampleId: 'kick',
255+
steps: Array(64).fill(false),
256+
parameterLocks: Array(64).fill(null),
257+
volume: 1,
258+
muted: false,
259+
playbackMode: 'oneshot',
260+
transpose: 0,
261+
stepCount: 16,
262+
},
263+
],
264+
tempo: 120,
265+
swing: 0,
266+
version: 1,
278267
});
279268

280-
const { id: sessionId } = await createRes.json();
281-
282269
const context = await browser.newContext();
283270
const page = await context.newPage();
284271

@@ -310,17 +297,13 @@ test.describe('Multiplayer input validation', () => {
310297
// FIXME: Server may not clamp tempo on session creation
311298
test.skip('invalid tempo values are clamped by server', async ({ request }) => {
312299
// Create a session with invalid tempo via API
313-
const createRes = await request.post(`${API_BASE}/api/sessions`, {
314-
data: {
315-
tracks: [],
316-
tempo: 999, // Invalid: above max of 180
317-
swing: 0,
318-
version: 1,
319-
},
300+
const { id: sessionId } = await createSessionWithRetry(request, {
301+
tracks: [],
302+
tempo: 999, // Invalid: above max of 180
303+
swing: 0,
304+
version: 1,
320305
});
321306

322-
const { id: sessionId } = await createRes.json();
323-
324307
// Server should clamp it - check via debug endpoint
325308
const debugRes = await request.get(`${API_BASE}/api/debug/session/${sessionId}`);
326309
const debug = await debugRes.json();

app/e2e/session-persistence.spec.ts

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { test, expect, APIRequestContext } from '@playwright/test';
1+
import { test, expect } from '@playwright/test';
2+
import { API_BASE, createSessionWithRetry } from './test-utils';
23

34
/**
45
* Session persistence tests - Phase 6 Observability
@@ -7,34 +8,6 @@ import { test, expect, APIRequestContext } from '@playwright/test';
78
* loaded and displayed in the browser without data loss.
89
*/
910

10-
// Use local dev server when running locally, production when deployed
11-
const API_BASE = process.env.CI
12-
? 'https://keyboardia.adewale-883.workers.dev'
13-
: 'http://localhost:5173';
14-
15-
/**
16-
* Helper to create a session with retry logic for intermittent API failures.
17-
* CI environments may experience rate limiting or cold starts.
18-
*/
19-
async function createSessionWithRetry(
20-
request: APIRequestContext,
21-
data: Record<string, unknown>,
22-
maxRetries = 3
23-
): Promise<{ id: string }> {
24-
let lastError: Error | null = null;
25-
for (let attempt = 0; attempt < maxRetries; attempt++) {
26-
const res = await request.post(`${API_BASE}/api/sessions`, { data });
27-
if (res.ok()) {
28-
return res.json();
29-
}
30-
lastError = new Error(`Session create failed: ${res.status()} ${res.statusText()}`);
31-
console.log(`[TEST] Session create attempt ${attempt + 1} failed, retrying...`);
32-
// Wait before retry with exponential backoff
33-
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
34-
}
35-
throw lastError ?? new Error('Session create failed after retries');
36-
}
37-
3811
test.describe('Session persistence integrity', () => {
3912
test('session created via API should load with correct tracks', async ({ page, request }) => {
4013
// Step 1: Create a session via API with known data

0 commit comments

Comments
 (0)