Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
09ee2b1
docs: Add logging guidelines spec based on loggingsucks.com analysis
claude Dec 22, 2025
9febe93
docs: Expand logging spec with Cloudflare and Observability 2.0 guidance
claude Dec 22, 2025
ce34220
docs: Rename to Observability 2.0 and archive legacy spec
claude Dec 22, 2025
ed5e801
docs: Add comprehensive wide events catalog, remove 30-day retention
claude Dec 22, 2025
3b7c5f8
docs: Add explicit DAU assumptions to event volume estimates
claude Dec 23, 2025
5a958cc
docs: Simplify Observability 2.0 to research document
claude Jan 3, 2026
757c181
docs: Add 1k DAU baseline and cost modeling to Observability 2.0 rese…
claude Jan 3, 2026
1406dc1
docs: Add Observability 2.0 implementation spec with wide events
claude Jan 15, 2026
68d362d
docs: Reorganize Observability 2.0 docs to match project structure
claude Jan 15, 2026
5998be4
docs: Fix implementation spec for wrangler.jsonc and playerId
claude Jan 15, 2026
f622cfa
docs: Add wide event design principles to research doc
claude Jan 15, 2026
0c36b22
docs: Add isCreator field and design decisions to impl spec
claude Jan 15, 2026
8271efa
docs: Add isPublished field to track published vs editable sessions
claude Jan 15, 2026
d164245
docs: Expand wide event design principles with industry research
claude Jan 15, 2026
820b4d1
docs: Add expected behavioral patterns for published vs editable
claude Jan 15, 2026
a0c9ca4
docs: Add sourceSessionId, deviceType, and remix/publish examples
claude Jan 15, 2026
e12d8fb
docs: Use IP + User-Agent hash for creator identity, not playerId
claude Jan 15, 2026
8c76c9c
docs: Fix audit issues in Observability 2.0 specs
claude Jan 15, 2026
e1ee854
docs: Reconcile internal consistency between Obs 2.0 documents
claude Jan 15, 2026
4338178
docs: Add ws_session_end reliability and timing context
adewale Jan 16, 2026
bc27bb2
docs: Add client error transport spec with 3 implementation phases
adewale Jan 16, 2026
9bef01c
docs: Add warnings array for recovered errors in wide events
adewale Jan 16, 2026
633757d
docs: Add Cloudflare deployment and infrastructure metadata to wide e…
adewale Jan 16, 2026
6a7e225
docs: Add slug, expected, and metadata fields to ErrorEvent schema
adewale Jan 16, 2026
17882f3
docs: Update Design Decisions tables with new metadata fields
adewale Jan 16, 2026
c78b7a2
docs: Align with Observability 2.0 consensus - embed errors in wide e…
adewale Jan 16, 2026
60d69ea
feat: Implement Observability 2.0 wide events
adewale Jan 16, 2026
cbe2f74
feat: Implement deferred observability items
adewale Jan 16, 2026
59947b1
feat: Add full-stack E2E test runner with wrangler dev
adewale Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions app/e2e/pitch-contour-alignment.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect } from './global-setup';
import { API_BASE } from './test-utils';

/**
* PitchContour SVG Alignment Tests
Expand Down Expand Up @@ -48,7 +49,7 @@
}

// Wait for WebSocket connection before adding tracks
await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 });

Check failure on line 52 in app/e2e/pitch-contour-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/pitch-contour-alignment.spec.ts:35:3 › PitchContour alignment › CSS dimensions match JavaScript constants (CRITICAL regression test)

1) [chromium] › e2e/pitch-contour-alignment.spec.ts:35:3 › PitchContour alignment › CSS dimensions match JavaScript constants (CRITICAL regression test) Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: locator('.connection-status--connected') Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for locator('.connection-status--connected') 50 | 51 | // Wait for WebSocket connection before adding tracks > 52 | await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 }); | ^ 53 | 54 | // Add a track to get step cells (button has star prefix like "★ 808 Kick") 55 | const addTrackButton = page.getByRole('button', { name: /808 Kick/i }); at /home/runner/work/keyboardia/keyboardia/app/e2e/pitch-contour-alignment.spec.ts:52:65

Check failure on line 52 in app/e2e/pitch-contour-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/pitch-contour-alignment.spec.ts:35:3 › PitchContour alignment › CSS dimensions match JavaScript constants (CRITICAL regression test)

1) [chromium] › e2e/pitch-contour-alignment.spec.ts:35:3 › PitchContour alignment › CSS dimensions match JavaScript constants (CRITICAL regression test) Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: locator('.connection-status--connected') Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for locator('.connection-status--connected') 50 | 51 | // Wait for WebSocket connection before adding tracks > 52 | await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 }); | ^ 53 | 54 | // Add a track to get step cells (button has star prefix like "★ 808 Kick") 55 | const addTrackButton = page.getByRole('button', { name: /808 Kick/i }); at /home/runner/work/keyboardia/keyboardia/app/e2e/pitch-contour-alignment.spec.ts:52:65

Check failure on line 52 in app/e2e/pitch-contour-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/pitch-contour-alignment.spec.ts:35:3 › PitchContour alignment › CSS dimensions match JavaScript constants (CRITICAL regression test)

1) [chromium] › e2e/pitch-contour-alignment.spec.ts:35:3 › PitchContour alignment › CSS dimensions match JavaScript constants (CRITICAL regression test) Error: expect(locator).toBeVisible() failed Locator: locator('.connection-status--connected') Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for locator('.connection-status--connected') 50 | 51 | // Wait for WebSocket connection before adding tracks > 52 | await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 }); | ^ 53 | 54 | // Add a track to get step cells (button has star prefix like "★ 808 Kick") 55 | const addTrackButton = page.getByRole('button', { name: /808 Kick/i }); at /home/runner/work/keyboardia/keyboardia/app/e2e/pitch-contour-alignment.spec.ts:52:65

// Add a track to get step cells (button has star prefix like "★ 808 Kick")
const addTrackButton = page.getByRole('button', { name: /808 Kick/i });
Expand Down Expand Up @@ -129,10 +130,8 @@
version: 1,
};

// Post session to API (use same port as playwright webServer)
const apiBase = 'http://localhost:5175';

const createRes = await request.post(`${apiBase}/api/sessions`, {
// Post session to API (respects BASE_URL env for full-stack testing)
const createRes = await request.post(`${API_BASE}/api/sessions`, {
data: sessionData,
});

Expand All @@ -147,7 +146,7 @@
await page.goto(`/s/${sessionId}`);

// Wait for WebSocket connection to ensure state is fully synced
await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 });

Check failure on line 149 in app/e2e/pitch-contour-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/pitch-contour-alignment.spec.ts:100:3 › PitchContour alignment › SVG contour dots align with step cells for session with pitch variation

2) [chromium] › e2e/pitch-contour-alignment.spec.ts:100:3 › PitchContour alignment › SVG contour dots align with step cells for session with pitch variation Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: locator('.connection-status--connected') Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for locator('.connection-status--connected') 147 | 148 | // Wait for WebSocket connection to ensure state is fully synced > 149 | await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 }); | ^ 150 | 151 | // Wait for track row to appear 152 | await expect(page.locator('.track-row')).toBeVisible({ timeout: 10000 }); at /home/runner/work/keyboardia/keyboardia/app/e2e/pitch-contour-alignment.spec.ts:149:65

Check failure on line 149 in app/e2e/pitch-contour-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/pitch-contour-alignment.spec.ts:100:3 › PitchContour alignment › SVG contour dots align with step cells for session with pitch variation

2) [chromium] › e2e/pitch-contour-alignment.spec.ts:100:3 › PitchContour alignment › SVG contour dots align with step cells for session with pitch variation Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: locator('.connection-status--connected') Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for locator('.connection-status--connected') 147 | 148 | // Wait for WebSocket connection to ensure state is fully synced > 149 | await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 }); | ^ 150 | 151 | // Wait for track row to appear 152 | await expect(page.locator('.track-row')).toBeVisible({ timeout: 10000 }); at /home/runner/work/keyboardia/keyboardia/app/e2e/pitch-contour-alignment.spec.ts:149:65

Check failure on line 149 in app/e2e/pitch-contour-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/pitch-contour-alignment.spec.ts:100:3 › PitchContour alignment › SVG contour dots align with step cells for session with pitch variation

2) [chromium] › e2e/pitch-contour-alignment.spec.ts:100:3 › PitchContour alignment › SVG contour dots align with step cells for session with pitch variation Error: expect(locator).toBeVisible() failed Locator: locator('.connection-status--connected') Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for locator('.connection-status--connected') 147 | 148 | // Wait for WebSocket connection to ensure state is fully synced > 149 | await expect(page.locator('.connection-status--connected')).toBeVisible({ timeout: 10000 }); | ^ 150 | 151 | // Wait for track row to appear 152 | await expect(page.locator('.track-row')).toBeVisible({ timeout: 10000 }); at /home/runner/work/keyboardia/keyboardia/app/e2e/pitch-contour-alignment.spec.ts:149:65

// Wait for track row to appear
await expect(page.locator('.track-row')).toBeVisible({ timeout: 10000 });
Expand Down
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"test:e2e:full": "npx playwright test --project=chromium --project=webkit",
"test:e2e:mobile": "npx playwright test e2e/mobile-iphone.spec.ts --project=mobile-safari",
"test:e2e:all-local": "npx playwright test --project=chromium && npm run test:e2e:mobile",
"test:e2e:full-stack": "tsx scripts/test-e2e-full-stack.ts",
"test:e2e:full-stack:smoke": "tsx scripts/test-e2e-full-stack.ts --smoke",
"validate:sync": "tsx scripts/validate-sync-checklist.ts",
"validate:samples": "bash scripts/validate-sample-volume.sh",
"validate:manifests": "tsx scripts/validate-manifests.ts",
Expand Down
23 changes: 14 additions & 9 deletions app/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export default defineConfig({
],

use: {
baseURL: 'http://localhost:5175',
// Support PLAYWRIGHT_BASE_URL for full-stack testing against wrangler dev
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5175',
headless: true,

// Tracing for debugging failures
Expand Down Expand Up @@ -79,12 +80,16 @@ export default defineConfig({
},
],

webServer: {
command: process.env.USE_MOCK_API
? 'USE_MOCK_API=1 npm run dev -- --port 5175'
: 'npm run dev -- --port 5175',
port: 5175,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
// Only start webServer when not using external server (PLAYWRIGHT_BASE_URL)
// Full-stack testing via test:e2e:full-stack manages wrangler dev externally
webServer: process.env.PLAYWRIGHT_BASE_URL
? undefined
: {
command: process.env.USE_MOCK_API
? 'USE_MOCK_API=1 npm run dev -- --port 5175'
: 'npm run dev -- --port 5175',
port: 5175,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});
184 changes: 184 additions & 0 deletions app/scripts/test-e2e-full-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env npx tsx
/**
* Full-Stack E2E Test Runner
*
* Runs E2E tests against the real Cloudflare Worker (wrangler dev) instead of
* just the Vite dev server. This tests the complete stack including:
* - Cloudflare Worker API endpoints
* - Durable Objects (WebSocket, state persistence)
* - KV storage
* - Observability 2.0 wide events
*
* Usage:
* npm run test:e2e:full-stack # Run all E2E tests against wrangler dev
* npm run test:e2e:full-stack -- --smoke # Run only smoke tests
*
* Prerequisites:
* - Project must be built first (script handles this)
* - Port 8787 must be available for wrangler dev
*/

import { spawn, execSync, ChildProcess } from 'child_process';

const WRANGLER_PORT = 8787;
const WRANGLER_URL = `http://localhost:${WRANGLER_PORT}`;
const MAX_STARTUP_WAIT_MS = 120_000; // 2 minutes
const HEALTH_CHECK_INTERVAL_MS = 1000;

let wranglerProcess: ChildProcess | null = null;

/**
* Check if wrangler dev is ready by hitting the health endpoint
*/
async function isWranglerReady(): Promise<boolean> {
try {
const response = await fetch(`${WRANGLER_URL}/api/health`, {
signal: AbortSignal.timeout(2000),
});
return response.ok;
} catch {
return false;
}
}

/**
* Wait for wrangler dev to be ready
*/
async function waitForWrangler(): Promise<void> {
const startTime = Date.now();
console.log(`⏳ Waiting for wrangler dev to be ready on port ${WRANGLER_PORT}...`);

while (Date.now() - startTime < MAX_STARTUP_WAIT_MS) {
if (await isWranglerReady()) {
console.log(`✅ Wrangler dev is ready (took ${Math.round((Date.now() - startTime) / 1000)}s)`);
return;
}
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
}

throw new Error(`Wrangler dev failed to start within ${MAX_STARTUP_WAIT_MS / 1000}s`);
}

/**
* Start wrangler dev in the background
*/
function startWrangler(): ChildProcess {
console.log('🚀 Starting wrangler dev...');

const proc = spawn('npx', ['wrangler', 'dev', '--port', String(WRANGLER_PORT)], {
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
shell: true,
});

// Log wrangler output with prefix
proc.stdout?.on('data', (data) => {
const lines = data.toString().split('\n').filter((l: string) => l.trim());
lines.forEach((line: string) => console.log(` [wrangler] ${line}`));
});

proc.stderr?.on('data', (data) => {
const lines = data.toString().split('\n').filter((l: string) => l.trim());
lines.forEach((line: string) => console.log(` [wrangler] ${line}`));
});

proc.on('error', (err) => {
console.error('❌ Failed to start wrangler:', err.message);
});

return proc;
}

/**
* Stop wrangler dev
*/
function stopWrangler(): void {
if (wranglerProcess) {
console.log('🛑 Stopping wrangler dev...');
wranglerProcess.kill('SIGTERM');
wranglerProcess = null;
}
}

/**
* Run playwright E2E tests
*/
function runE2ETests(smokeOnly: boolean): number {
console.log(`\n🧪 Running E2E tests against ${WRANGLER_URL}...\n`);

const args = smokeOnly
? ['playwright', 'test', '--project=chromium', 'e2e/track-reorder.spec.ts', 'e2e/plock-editor.spec.ts', 'e2e/pitch-contour-alignment.spec.ts']
: ['playwright', 'test'];

try {
execSync(`npx ${args.join(' ')}`, {
stdio: 'inherit',
env: {
...process.env,
// Override the base URL to point to wrangler dev
// PLAYWRIGHT_BASE_URL: Used by playwright.config.ts for browser navigation
// BASE_URL: Used by test-utils.ts for direct API requests
PLAYWRIGHT_BASE_URL: WRANGLER_URL,
BASE_URL: WRANGLER_URL,
},
});
return 0;
} catch {
// execSync throws on non-zero exit code
return 1;
}
}

/**
* Build the project
*/
function buildProject(): void {
console.log('📦 Building project...');
execSync('npm run build', { stdio: 'inherit' });
console.log('✅ Build complete\n');
}

/**
* Main entry point
*/
async function main(): Promise<void> {
const args = process.argv.slice(2);
const smokeOnly = args.includes('--smoke');
let exitCode = 0;

// Cleanup handler
const cleanup = () => {
stopWrangler();
process.exit(exitCode);
};

process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);

try {
// Step 1: Build
buildProject();

// Step 2: Start wrangler dev
wranglerProcess = startWrangler();

// Step 3: Wait for wrangler to be ready
await waitForWrangler();

// Step 4: Run E2E tests
exitCode = runE2ETests(smokeOnly);

if (exitCode === 0) {
console.log('\n✅ All E2E tests passed!');
} else {
console.log('\n❌ Some E2E tests failed');
}
} catch (error) {
console.error('\n❌ Error:', error instanceof Error ? error.message : error);
exitCode = 1;
} finally {
cleanup();
}
}

main();
Loading
Loading