Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0b5b0ed
spec: Add AudioWorklet engine spec for scheduling, LFO, metering, and…
claude Feb 27, 2026
d8bb291
feat: Build AudioWorklet engine — worklets, metrics, hosts, and integ…
claude Feb 27, 2026
f0705ca
chore: Add node_modules to root .gitignore
claude Feb 27, 2026
0167741
fix: Audit fixes for AudioWorklet implementation
claude Feb 27, 2026
4727e52
test: Add property-based tests for AudioWorklet engine modules
claude Feb 27, 2026
45cd5e3
fix: Remove config dead code and optimize main-thread hotspots
claude Feb 28, 2026
720bc34
feat: Move MIDI export to Web Worker and add dead code audit tooling
claude Feb 28, 2026
e724fb1
fix: Relax floating-point precision in percentile property test
claude Mar 2, 2026
aecb3f0
refactor: Remove dead synthesis.ts module and test
claude Mar 8, 2026
ba99b4d
refactor: Remove dead note-player.ts module and test
claude Mar 8, 2026
9462cec
refactor: Remove unused isWorkletSchedulerEnabled() function
claude Mar 8, 2026
b7da04d
feat: Enable LoopRuler feature flag
claude Mar 8, 2026
5b1c876
feat: Wire in track metering with VU meters in MixerPanel
claude Mar 8, 2026
8b21696
fix: Fix metering pipeline bugs — per-track RMS, index reclamation, r…
claude Mar 8, 2026
c8827b2
fix: Guard p-lock volume reset against stop() race, document type dup…
claude Mar 8, 2026
bf7c96e
fix: Break circular midiExport import, guard computePeaks for width=0
claude Mar 8, 2026
373feaf
feat: Wire in worklet scheduler behind feature flag (default: off)
claude Mar 8, 2026
c69beed
feat: Wire in XY Pad controller with preset selector in effects panel
claude Mar 8, 2026
c1090ba
feat: Wire in pitch-shift worklet for high-quality large pitch shifts
claude Mar 8, 2026
85cb130
feat: Wire in shared LFO worklet for AdvancedSynthEngine
claude Mar 8, 2026
b0d7627
refactor: Replace lazyAudioLoader with direct audioEngine imports
claude Mar 8, 2026
8f02760
feat: Route-level code splitting + remove lazyAudioLoader
claude Mar 9, 2026
c4bca77
fix: Route all instrument types through TrackBusManager for VU metering
adewale Mar 15, 2026
4dc1c6b
fix: Resolve CI type errors for worklets and MIDI worker imports
adewale Mar 15, 2026
ddbf841
fix: Use object destructuring for Playwright fixture parameter
adewale Mar 15, 2026
984e7d3
fix: Resolve CI failures — duplicate function, unused var, rAF in tes…
adewale Mar 19, 2026
328867e
fix: Update E2E tests for AudioWorklet branch UI changes
adewale Mar 20, 2026
e547bd7
feat: Unify XY pad system — batched updates, synth wiring, layout hie…
adewale Mar 21, 2026
bcce1b3
docs: Add lessons 21–27 from XY Pad unification
adewale Mar 21, 2026
e044921
fix: Address 8 codebase issues from lessons 21–27
adewale Mar 21, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
app/.wrangler/
app/test-output.mid
15 changes: 7 additions & 8 deletions app/e2e/feature-flags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,21 @@ function isWebkit(browserName: string): boolean {
}

test.describe('Feature Flags', () => {
test.describe('Loop Ruler (default: OFF)', () => {
test.describe('Loop Ruler (default: ON)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await waitForAppReady(page);
});

test('loop ruler is NOT visible by default', async ({ page }) => {
// Loop ruler should not be rendered when feature flag is off (default)
test('loop ruler is visible by default', async ({ page }) => {
const loopRuler = page.locator('.loop-ruler');
await expect(loopRuler).toHaveCount(0);
await expect(loopRuler).toHaveCount(1);
});

test('loop handles are NOT visible by default', async ({ page }) => {
// Loop handles should not exist when feature flag is off
const loopHandles = page.locator('.loop-handle');
await expect(loopHandles).toHaveCount(0);
test('loop ruler supports drag interaction', async ({ page }) => {
// The loop ruler is interactive — dragging defines a loop region
const loopRuler = page.locator('.loop-ruler');
await expect(loopRuler).toBeVisible();
});
});

Expand Down
12 changes: 6 additions & 6 deletions app/e2e/landscape-alignment.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/**
* Landscape Mobile Alignment E2E Tests
*
Expand Down Expand Up @@ -29,7 +29,7 @@

// Click the instrument button by name
const sampleBtn = page.getByRole('button', { name: instrumentName }).first();
await expect(sampleBtn).toBeVisible({ timeout: 5000 });

Check failure on line 32 in app/e2e/landscape-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/landscape-alignment.spec.ts:224:3 › Landscape Mobile Alignment › screenshot comparison - visual alignment check

2) [chromium] › e2e/landscape-alignment.spec.ts:224:3 › Landscape Mobile Alignment › screenshot comparison - visual alignment check Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect.toBeVisible: Target page, context or browser has been closed 30 | // Click the instrument button by name 31 | const sampleBtn = page.getByRole('button', { name: instrumentName }).first(); > 32 | await expect(sampleBtn).toBeVisible({ timeout: 5000 }); | ^ 33 | await sampleBtn.click(); 34 | 35 | // Wait for track row to appear at addTrack (/home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:32:27) at /home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:226:5

Check failure on line 32 in app/e2e/landscape-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/landscape-alignment.spec.ts:224:3 › Landscape Mobile Alignment › screenshot comparison - visual alignment check

2) [chromium] › e2e/landscape-alignment.spec.ts:224:3 › Landscape Mobile Alignment › screenshot comparison - visual alignment check Error: expect.toBeVisible: Target page, context or browser has been closed 30 | // Click the instrument button by name 31 | const sampleBtn = page.getByRole('button', { name: instrumentName }).first(); > 32 | await expect(sampleBtn).toBeVisible({ timeout: 5000 }); | ^ 33 | await sampleBtn.click(); 34 | 35 | // Wait for track row to appear at addTrack (/home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:32:27) at /home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:226:5

Check failure on line 32 in app/e2e/landscape-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/landscape-alignment.spec.ts:154:3 › Landscape Mobile Alignment › all track rows should have consistent vertical alignment

1) [chromium] › e2e/landscape-alignment.spec.ts:154:3 › Landscape Mobile Alignment › all track rows should have consistent vertical alignment Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect.toBeVisible: Target page, context or browser has been closed 30 | // Click the instrument button by name 31 | const sampleBtn = page.getByRole('button', { name: instrumentName }).first(); > 32 | await expect(sampleBtn).toBeVisible({ timeout: 5000 }); | ^ 33 | await sampleBtn.click(); 34 | 35 | // Wait for track row to appear at addTrack (/home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:32:27) at /home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:157:5

Check failure on line 32 in app/e2e/landscape-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/landscape-alignment.spec.ts:154:3 › Landscape Mobile Alignment › all track rows should have consistent vertical alignment

1) [chromium] › e2e/landscape-alignment.spec.ts:154:3 › Landscape Mobile Alignment › all track rows should have consistent vertical alignment Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect.toBeVisible: Target page, context or browser has been closed 30 | // Click the instrument button by name 31 | const sampleBtn = page.getByRole('button', { name: instrumentName }).first(); > 32 | await expect(sampleBtn).toBeVisible({ timeout: 5000 }); | ^ 33 | await sampleBtn.click(); 34 | 35 | // Wait for track row to appear at addTrack (/home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:32:27) at /home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:157:5

Check failure on line 32 in app/e2e/landscape-alignment.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › e2e/landscape-alignment.spec.ts:154:3 › Landscape Mobile Alignment › all track rows should have consistent vertical alignment

1) [chromium] › e2e/landscape-alignment.spec.ts:154:3 › Landscape Mobile Alignment › all track rows should have consistent vertical alignment Error: expect.toBeVisible: Target page, context or browser has been closed 30 | // Click the instrument button by name 31 | const sampleBtn = page.getByRole('button', { name: instrumentName }).first(); > 32 | await expect(sampleBtn).toBeVisible({ timeout: 5000 }); | ^ 33 | await sampleBtn.click(); 34 | 35 | // Wait for track row to appear at addTrack (/home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:32:27) at /home/runner/work/keyboardia/keyboardia/app/e2e/landscape-alignment.spec.ts:157:5
await sampleBtn.click();

// Wait for track row to appear
Expand Down Expand Up @@ -152,10 +152,10 @@
});

test('all track rows should have consistent vertical alignment', async ({ page }) => {
// Add multiple tracks
await addTrack(page);
await addTrack(page);
await addTrack(page);
// Add multiple tracks using different instruments
await addTrack(page, /808 Kick/);
await addTrack(page, /808 Snare/);
await addTrack(page, /808 Clap/);

// Wait for all track rows
const trackRows = page.locator('.track-row');
Expand Down Expand Up @@ -222,8 +222,8 @@
});

test('screenshot comparison - visual alignment check', async ({ page }) => {
await addTrack(page);
await addTrack(page);
await addTrack(page, /808 Kick/);
await addTrack(page, /808 Snare/);

// Wait for tracks to be ready
await expect(page.locator('.track-row')).toHaveCount(2, { timeout: 5000 });
Expand Down
199 changes: 199 additions & 0 deletions app/e2e/mixer-layout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* Mixer Panel Layout Tests
*
* Verifies that VU meters and other mixer channel elements are properly
* contained within their channel boundaries and scroll correctly.
*
* Bug: .track-meter has height: 100% which causes it to overflow
* the mixer channel, pushing the fader and volume controls outside
* the channel's visible bounds.
*/

import { test as base, expect, type Page } from '@playwright/test';

const test = base;

/**
* Navigate to the app, create a session, and add tracks.
*/
async function setupSession(page: Page) {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');

const startButton = page.locator(
'.landing-btn.primary, button:has-text("Start Session"), button:has-text("Start"), button:has-text("Create")'
).first();
const isLanding = await startButton.isVisible({ timeout: 3000 }).catch(() => false);

if (isLanding) {
await startButton.click();
await page.waitForURL(/\/s\//, { timeout: 15000 });
}

// Wait for app to be ready — sample picker should be visible at bottom
await page.locator('.sample-picker, .track-row, .app').first().waitFor({
state: 'visible',
timeout: 15000,
});

// Wait for WebSocket connection (best-effort in mock mode)
await page.locator('.connection-status--connected').waitFor({
state: 'visible',
timeout: 10000,
}).catch(() => { /* mock mode may not have WS */ });

// Add first track via sample picker
const kickBtn = page.locator('button:has-text("808 Kick"), button:has-text("Kick")').first();
if (await kickBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await kickBtn.click();
await page.locator('.track-row').first().waitFor({ state: 'visible', timeout: 5000 });
}

// Add second track
const snareBtn = page.locator('button:has-text("808 Snare"), button:has-text("Snare")').first();
if (await snareBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await snareBtn.click();
await expect(page.locator('.track-row')).toHaveCount(2, { timeout: 5000 });
}
}

test.describe('Mixer Panel Layout', () => {
test('VU meter stays within mixer channel bounds', async ({ page }) => {
await setupSession(page);

// Open the mixer panel
const mixerBtn = page.locator('.mixer-btn, button:has-text("Mixer")').first();
await expect(mixerBtn).toBeVisible({ timeout: 5000 });
await mixerBtn.click();
await expect(page.locator('.mixer-panel-container.expanded')).toBeVisible({ timeout: 5000 });

// Check that all channel children stay within the channel bounds
const layoutCheck = await page.evaluate(() => {
const channels = document.querySelectorAll('.mixer-channel');
const results: Array<{
channelIndex: number;
channelHeight: number;
overflowingChildren: Array<{ class: string; height: number; bottom: number; channelBottom: number }>;
}> = [];

channels.forEach((channel, i) => {
const channelRect = channel.getBoundingClientRect();
const overflowing: Array<{ class: string; height: number; bottom: number; channelBottom: number }> = [];

Array.from(channel.children).forEach(child => {
const childRect = child.getBoundingClientRect();
// Allow 1px tolerance for rounding
if (childRect.bottom > channelRect.bottom + 1) {
overflowing.push({
class: child.className,
height: Math.round(childRect.height),
bottom: Math.round(childRect.bottom),
channelBottom: Math.round(channelRect.bottom),
});
}
});

results.push({
channelIndex: i,
channelHeight: Math.round(channelRect.height),
overflowingChildren: overflowing,
});
});

return results;
});

// No channel should have overflowing children
for (const channel of layoutCheck) {
expect(
channel.overflowingChildren,
`Channel ${channel.channelIndex} has children overflowing: ${JSON.stringify(channel.overflowingChildren)}`
).toHaveLength(0);
}
});

test('fader has non-zero height in each channel', async ({ page }) => {
await setupSession(page);

await page.locator('.mixer-btn, button:has-text("Mixer")').first().click();
await expect(page.locator('.mixer-panel-container.expanded')).toBeVisible({ timeout: 5000 });

// Each fader container should have its designed height (120px)
const faderHeights = await page.evaluate(() => {
const faders = document.querySelectorAll('.channel-fader-container');
return Array.from(faders).map(f => Math.round(f.getBoundingClientRect().height));
});

expect(faderHeights.length).toBeGreaterThanOrEqual(1);
for (const height of faderHeights) {
expect(height, 'Fader should have its designed height (not collapsed to 0)').toBeGreaterThanOrEqual(100);
}
});

test('mixer channel elements scroll together with page', async ({ page }) => {
await setupSession(page);

await page.locator('.mixer-btn, button:has-text("Mixer")').first().click();
await expect(page.locator('.mixer-panel-container.expanded')).toBeVisible({ timeout: 5000 });

// Verify we have at least one channel
await expect(page.locator('.mixer-channel').first()).toBeVisible({ timeout: 5000 });

// Get initial positions of channel elements
const initialPositions = await page.evaluate(() => {
const channel = document.querySelector('.mixer-channel');
const mixerPanel = document.querySelector('.mixer-panel');
if (!channel || !mixerPanel) return null;

// Get first track-meter if exists, otherwise use channel name
const meterOrName = document.querySelector('.track-meter') || document.querySelector('.channel-name');
const fader = document.querySelector('.channel-fader-container');

return {
channelTop: channel.getBoundingClientRect().top,
panelTop: mixerPanel.getBoundingClientRect().top,
innerTop: meterOrName ? meterOrName.getBoundingClientRect().top : null,
faderTop: fader ? fader.getBoundingClientRect().top : null,
};
});

expect(initialPositions).not.toBeNull();

// Scroll the page
await page.evaluate(() => window.scrollBy(0, 150));
await page.waitForTimeout(100);

const afterPositions = await page.evaluate(() => {
const channel = document.querySelector('.mixer-channel');
const mixerPanel = document.querySelector('.mixer-panel');
if (!channel || !mixerPanel) return null;

const meterOrName = document.querySelector('.track-meter') || document.querySelector('.channel-name');
const fader = document.querySelector('.channel-fader-container');

return {
channelTop: channel.getBoundingClientRect().top,
panelTop: mixerPanel.getBoundingClientRect().top,
innerTop: meterOrName ? meterOrName.getBoundingClientRect().top : null,
faderTop: fader ? fader.getBoundingClientRect().top : null,
};
});

expect(afterPositions).not.toBeNull();

// All elements should have moved by the same amount (within 2px tolerance)
const panelDelta = afterPositions!.panelTop - initialPositions!.panelTop;
const channelDelta = afterPositions!.channelTop - initialPositions!.channelTop;
expect(Math.abs(channelDelta - panelDelta)).toBeLessThan(2);

if (initialPositions!.innerTop !== null && afterPositions!.innerTop !== null) {
const innerDelta = afterPositions!.innerTop - initialPositions!.innerTop;
expect(Math.abs(innerDelta - panelDelta)).toBeLessThan(2);
}

if (initialPositions!.faderTop !== null && afterPositions!.faderTop !== null) {
const faderDelta = afterPositions!.faderTop - initialPositions!.faderTop;
expect(Math.abs(faderDelta - panelDelta)).toBeLessThan(2);
}
});
});
9 changes: 7 additions & 2 deletions app/e2e/playback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,19 @@ test.describe('Playback stability', () => {

test('should not flicker during playback - step changes are monotonic', async ({ page }) => {

// Track step changes via DOM mutations
// Track step changes via DOM mutations (scoped to step grid, excluding VU meters)
await page.evaluate(() => {
const win = window as Window & {
__stepChanges: Array<{ count: number; time: number }>;
__observer: MutationObserver;
};
win.__stepChanges = [];
const observer = new MutationObserver(() => {
const observer = new MutationObserver((mutations) => {
// Ignore mutations from VU meter elements (high-frequency style updates)
const isRelevant = mutations.some(m =>
!(m.target as Element).closest?.('.track-meter')
);
if (!isRelevant) return;
const playingIndicators = document.querySelectorAll('.playing, [data-playing="true"]');
win.__stepChanges.push({
count: playingIndicators.length,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/e2e/visual.spec.ts-snapshots/sample-picker-chromium-darwin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading