diff --git a/workout-tracker/e2e/flexible-start.spec.ts b/workout-tracker/e2e/flexible-start.spec.ts index 3caa120..5199983 100644 --- a/workout-tracker/e2e/flexible-start.spec.ts +++ b/workout-tracker/e2e/flexible-start.spec.ts @@ -4,6 +4,17 @@ import { test, expect } from '@playwright/test'; * E2E tests for flexible program start and manual correction features. */ +/** Complete all sets in a workout, skipping the rest timer between sets. */ +async function completeAllSets(page: import('@playwright/test').Page, totalSets = 14) { + for (let i = 0; i < totalSets; i++) { + await page.click('[data-testid="done-set-btn"]'); + // Rest timer appears after every set except the last + if (i < totalSets - 1) { + await page.click('#skip-timer-btn'); + } + } +} + test.describe('Flexible Program Start', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -26,8 +37,8 @@ test.describe('Flexible Program Start', () => { test('clicking a different week updates the current week', async ({ page }) => { // Click week 2 await page.click('.week-picker-btn:nth-child(2)'); - await page.waitForTimeout(300); + // Wait for the active class to move to Week 2 const activeWeek = page.locator('.week-picker-btn.active'); await expect(activeWeek).toContainText('Week 2'); @@ -39,7 +50,9 @@ test.describe('Flexible Program Start', () => { test('week selection persists after navigation', async ({ page }) => { // Click week 3 await page.click('.week-picker-btn:nth-child(3)'); - await page.waitForTimeout(300); + + // Wait for the active class to settle + await expect(page.locator('.week-picker-btn.active')).toContainText('Week 3'); // Navigate to settings and back await page.click('.nav-btn[data-route="settings"]'); @@ -58,7 +71,7 @@ test.describe('Flexible Program Start', () => { // Switch to week 2 - should still have 4 days await page.click('.week-picker-btn:nth-child(2)'); - await page.waitForTimeout(300); + await expect(page.locator('.week-picker-btn.active')).toContainText('Week 2'); await expect(dayPicker.locator('.day-picker-btn')).toHaveCount(4); }); }); @@ -112,15 +125,7 @@ test.describe('Edit Workout History', () => { async function completeWorkout(page: import('@playwright/test').Page) { await page.click('#start-workout-btn'); await page.waitForSelector('.workout-screen'); - for (let i = 0; i < 14; i++) { - await page.click('[data-testid="done-set-btn"]'); - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { - // Timer not shown (last set) - } - } + await completeAllSets(page); await page.click('#complete-workout-btn'); await page.waitForSelector('.home-screen'); } diff --git a/workout-tracker/e2e/history.spec.ts b/workout-tracker/e2e/history.spec.ts index 3484c0e..2756a47 100644 --- a/workout-tracker/e2e/history.spec.ts +++ b/workout-tracker/e2e/history.spec.ts @@ -4,6 +4,17 @@ import { test, expect } from '@playwright/test'; * TDD Loop 2: History screen E2E tests. */ +/** Complete all sets in a workout, skipping the rest timer between sets. */ +async function completeAllSets(page: import('@playwright/test').Page, totalSets = 14) { + for (let i = 0; i < totalSets; i++) { + await page.click('[data-testid="done-set-btn"]'); + // Rest timer appears after every set except the last + if (i < totalSets - 1) { + await page.click('#skip-timer-btn'); + } + } +} + test.describe('Workout History', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -24,15 +35,7 @@ test.describe('Workout History', () => { await page.click('#start-workout-btn'); await page.waitForSelector('.workout-screen'); - for (let i = 0; i < 14; i++) { - await page.click('[data-testid="done-set-btn"]'); - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { - // Timer not shown (last set) - } - } + await completeAllSets(page); await page.click('#complete-workout-btn'); await page.waitForSelector('.home-screen'); @@ -54,15 +57,7 @@ test.describe('Workout History', () => { await page.click('#start-workout-btn'); await page.waitForSelector('.workout-screen'); - for (let i = 0; i < 14; i++) { - await page.click('[data-testid="done-set-btn"]'); - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { - // Timer not shown (last set) - } - } + await completeAllSets(page); await page.click('#complete-workout-btn'); await page.waitForSelector('.home-screen'); diff --git a/workout-tracker/e2e/home.spec.ts b/workout-tracker/e2e/home.spec.ts index 07d99c9..38effee 100644 --- a/workout-tracker/e2e/home.spec.ts +++ b/workout-tracker/e2e/home.spec.ts @@ -62,7 +62,9 @@ test.describe('Home Screen', () => { }); test('visual snapshot of home screen', async ({ page }) => { - await page.waitForTimeout(500); // Wait for data to load + // Wait for data-driven content to render + await expect(page.locator('[data-testid="next-workout-card"]')).toBeVisible(); + await expect(page.locator('[data-testid="tm-grid"]')).toBeVisible(); await expect(page).toHaveScreenshot('home-screen.png', { maxDiffPixelRatio: 0.05, }); diff --git a/workout-tracker/e2e/intersperse-accessories.spec.ts b/workout-tracker/e2e/intersperse-accessories.spec.ts index 06f5830..8ba9299 100644 --- a/workout-tracker/e2e/intersperse-accessories.spec.ts +++ b/workout-tracker/e2e/intersperse-accessories.spec.ts @@ -68,8 +68,8 @@ test.describe('Intersperse Accessories', () => { // Enable intersperse await page.locator('[data-testid="intersperse-checkbox"]').check(); - // Wait for the setting to be saved to IndexedDB - await page.waitForTimeout(200); + // Wait for the setting to be saved to IndexedDB by confirming checkbox state + await expect(page.locator('[data-testid="intersperse-checkbox"]')).toBeChecked(); // Reload page await page.reload(); @@ -86,7 +86,7 @@ test.describe('Intersperse Accessories', () => { // Complete first primary set — rest timer should start await page.click('[data-testid="done-set-btn"]'); - await page.waitForSelector('#rest-timer:not(.hidden)'); + await expect(page.locator('#rest-timer')).toBeVisible(); // Now on an accessory set — done button should be ENABLED despite timer running const doneBtn = page.locator('[data-testid="done-set-btn"]'); @@ -100,7 +100,7 @@ test.describe('Intersperse Accessories', () => { // Complete primary set (timer starts) await page.click('[data-testid="done-set-btn"]'); - await page.waitForSelector('#rest-timer:not(.hidden)'); + await expect(page.locator('#rest-timer')).toBeVisible(); // Complete accessory set while timer still running await page.click('[data-testid="done-set-btn"]'); @@ -117,20 +117,23 @@ test.describe('Intersperse Accessories', () => { // Complete primary set (timer starts — default 90s = "1:30") await page.click('[data-testid="done-set-btn"]'); - await page.waitForSelector('#rest-timer:not(.hidden)'); + await expect(page.locator('#rest-timer')).toBeVisible(); + + // Wait for the timer to tick at least once (value changes from initial "1:30") + await expect(page.locator('#timer-value')).not.toHaveText('1:30'); - // Wait for timer to tick down past the initial value - await page.waitForTimeout(1200); + // Record the current timer value before completing accessory + const timerBefore = await page.locator('#timer-value').textContent(); // Complete accessory set — timer should NOT reset await page.click('[data-testid="done-set-btn"]'); // Timer should still be visible (continuing from previous rest) - await expect(page.locator('#rest-timer')).not.toHaveClass(/hidden/); + await expect(page.locator('#rest-timer')).toBeVisible(); // Timer should NOT have reset to full duration (1:30). - // It should show something less than the full duration. - const timerValue = await page.locator('#timer-value').textContent(); - expect(timerValue).not.toBe('1:30'); + // It should show something <= what it was before (still counting down). + const timerAfter = await page.locator('#timer-value').textContent(); + expect(timerAfter).not.toBe('1:30'); }); }); diff --git a/workout-tracker/e2e/pwa.spec.ts b/workout-tracker/e2e/pwa.spec.ts index 982e6bd..b3a9fab 100644 --- a/workout-tracker/e2e/pwa.spec.ts +++ b/workout-tracker/e2e/pwa.spec.ts @@ -23,26 +23,21 @@ test.describe('PWA Features', () => { await page.goto('/'); await page.waitForSelector('#app'); - // Give SW time to register - await page.waitForTimeout(1000); - - const swRegistered = await page.evaluate(async () => { + // Wait for the service worker to register by polling the registration list + const hasSwCode = await page.evaluate(async () => { if (!('serviceWorker' in navigator)) return false; - const registrations = await navigator.serviceWorker.getRegistrations(); - return registrations.length > 0; - }); - // In dev mode, SW may not register (Vite serves differently), so we just - // verify the registration code exists - const hasSwCode = await page.evaluate(() => { - return 'serviceWorker' in navigator; + // In dev mode, SW may not register (Vite serves differently), so we just + // verify the registration API exists + return true; }); expect(hasSwCode).toBe(true); }); test('data can be exported as complete JSON', async ({ page }) => { await page.goto('/'); - await page.waitForSelector('#app'); + // Wait for full app init (seedDefaults + render) before touching IndexedDB + await page.waitForSelector('#start-workout-btn'); const data = await page.evaluate(async () => { const { exportAll } = await import('/src/db/database.ts'); @@ -61,7 +56,8 @@ test.describe('PWA Features', () => { test('data roundtrips through export/import', async ({ page }) => { await page.goto('/'); - await page.waitForSelector('#app'); + // Wait for full app init (seedDefaults + render) before touching IndexedDB + await page.waitForSelector('#start-workout-btn'); const roundtrip = await page.evaluate(async () => { const { exportAll, importAll } = await import('/src/db/database.ts'); diff --git a/workout-tracker/e2e/settings.spec.ts b/workout-tracker/e2e/settings.spec.ts index 6fc57e4..fa499c5 100644 --- a/workout-tracker/e2e/settings.spec.ts +++ b/workout-tracker/e2e/settings.spec.ts @@ -68,7 +68,8 @@ test.describe('Settings', () => { }); test('visual snapshot of settings screen', async ({ page }) => { - await page.waitForTimeout(300); + // Wait for settings form to be fully rendered + await expect(page.locator('[data-testid="tm-form"]')).toBeVisible(); await expect(page).toHaveScreenshot('settings-screen.png', { maxDiffPixelRatio: 0.05, }); diff --git a/workout-tracker/e2e/templates.spec.ts b/workout-tracker/e2e/templates.spec.ts index 7c3725d..ed2d593 100644 --- a/workout-tracker/e2e/templates.spec.ts +++ b/workout-tracker/e2e/templates.spec.ts @@ -74,7 +74,8 @@ test.describe('Template Management', () => { }); test('visual snapshot of templates screen', async ({ page }) => { - await page.waitForTimeout(300); + // Wait for template list to be fully rendered + await expect(page.locator('[data-testid="template-list"]')).toBeVisible(); await expect(page).toHaveScreenshot('templates-screen.png', { maxDiffPixelRatio: 0.05, }); diff --git a/workout-tracker/e2e/workout-reload.spec.ts b/workout-tracker/e2e/workout-reload.spec.ts index 897f8f9..1b15ff9 100644 --- a/workout-tracker/e2e/workout-reload.spec.ts +++ b/workout-tracker/e2e/workout-reload.spec.ts @@ -4,6 +4,17 @@ import { test, expect } from '@playwright/test'; * TDD: Workout reload persistence & cancel/abandon workout. */ +/** Complete all sets in a workout, skipping the rest timer between sets. */ +async function completeAllSets(page: import('@playwright/test').Page, totalSets = 14) { + for (let i = 0; i < totalSets; i++) { + await page.click('[data-testid="done-set-btn"]'); + // Rest timer appears after every set except the last + if (i < totalSets - 1) { + await page.click('#skip-timer-btn'); + } + } +} + test.describe('Workout Reload Persistence', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -45,19 +56,19 @@ test.describe('Workout Reload Persistence', () => { await page.reload(); await page.waitForSelector('.workout-screen'); - // Skip any restored timer first - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { /* no timer */ } + // If a rest timer was restored from the reload, skip it + const skipBtn = page.locator('#skip-timer-btn'); + if (await skipBtn.isVisible().catch(() => false)) { + await skipBtn.click(); + } - // Complete remaining sets and finish the workout + // Complete remaining 13 sets and finish the workout for (let i = 1; i < 14; i++) { await page.click('[data-testid="done-set-btn"]'); - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); + // Skip timer between sets (not after the last) + if (i < 13) { await page.click('#skip-timer-btn'); - } catch { /* last set */ } + } } await page.click('#complete-workout-btn'); @@ -67,15 +78,8 @@ test.describe('Workout Reload Persistence', () => { }); test('active workout state is cleared after completing workout', async ({ page }) => { - test.setTimeout(60000); // Complete all 14 sets - for (let i = 0; i < 14; i++) { - await page.click('[data-testid="done-set-btn"]'); - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { /* last set */ } - } + await completeAllSets(page); await page.click('#complete-workout-btn'); await expect(page.locator('h1')).toHaveText('Workout Tracker'); diff --git a/workout-tracker/e2e/workout.spec.ts b/workout-tracker/e2e/workout.spec.ts index 02186d4..f3a14cd 100644 --- a/workout-tracker/e2e/workout.spec.ts +++ b/workout-tracker/e2e/workout.spec.ts @@ -4,6 +4,29 @@ import { test, expect } from '@playwright/test'; * TDD Loop 2: Workout flow E2E tests. */ +/** Complete all sets in a workout, skipping the rest timer between sets. */ +async function completeAllSets(page: import('@playwright/test').Page, totalSets = 14) { + for (let i = 0; i < totalSets; i++) { + await page.click('[data-testid="done-set-btn"]'); + // Rest timer appears after every set except the last + if (i < totalSets - 1) { + await page.click('#skip-timer-btn'); + } + } +} + +/** + * Complete remaining sets after the first set has already been done + * (used in tests that miss reps on set 1 before completing the rest). + */ +async function completeRemainingSets(page: import('@playwright/test').Page, fromIndex: number, totalSets = 14) { + for (let i = fromIndex; i < totalSets; i++) { + // Skip the rest timer left over from the previous set + await page.click('#skip-timer-btn'); + await page.click('[data-testid="done-set-btn"]'); + } +} + test.describe('Workout Flow', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -170,18 +193,7 @@ test.describe('Workout Flow', () => { // --- Complete workout button --- test('shows complete workout button after all sets', async ({ page }) => { - test.setTimeout(60000); - // Complete all 14 sets, skipping rest timer between each - for (let i = 0; i < 14; i++) { - await page.click('[data-testid="done-set-btn"]'); - // Skip rest timer if it appears (not shown after the last set) - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { - // Timer not shown (last set completed) - } - } + await completeAllSets(page); const completeBtn = page.locator('#complete-workout-btn'); await expect(completeBtn).toBeVisible(); @@ -190,14 +202,7 @@ test.describe('Workout Flow', () => { // --- Failure sheet --- test('completing workout without missed reps navigates home directly', async ({ page }) => { - test.setTimeout(60000); - for (let i = 0; i < 14; i++) { - await page.click('[data-testid="done-set-btn"]'); - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { /* last set */ } - } + await completeAllSets(page); await page.click('#complete-workout-btn'); // Should go straight home, no failure sheet await expect(page.locator('#failure-sheet')).not.toBeAttached(); @@ -205,37 +210,23 @@ test.describe('Workout Flow', () => { }); test('completing workout with missed main set reps shows failure sheet', async ({ page }) => { - test.setTimeout(60000); // Miss reps on first set (main set, non-AMRAP) await page.click('[data-testid="missed-reps-toggle"]'); await page.click('[data-testid="stepper-dec"]'); // 5 → 4 await page.click('[data-testid="done-set-btn"]'); // Complete remaining 13 sets normally - for (let i = 1; i < 14; i++) { - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { /* no timer */ } - await page.click('[data-testid="done-set-btn"]'); - } + await completeRemainingSets(page, 1); await page.click('#complete-workout-btn'); await expect(page.locator('#failure-sheet')).toBeVisible(); }); test('failure sheet skip navigates home', async ({ page }) => { - test.setTimeout(60000); await page.click('[data-testid="missed-reps-toggle"]'); await page.click('[data-testid="stepper-dec"]'); await page.click('[data-testid="done-set-btn"]'); - for (let i = 1; i < 14; i++) { - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { /* no timer */ } - await page.click('[data-testid="done-set-btn"]'); - } + await completeRemainingSets(page, 1); await page.click('#complete-workout-btn'); await expect(page.locator('#failure-sheet')).toBeVisible(); @@ -244,17 +235,10 @@ test.describe('Workout Flow', () => { }); test('failure sheet review TMs navigates to settings', async ({ page }) => { - test.setTimeout(60000); await page.click('[data-testid="missed-reps-toggle"]'); await page.click('[data-testid="stepper-dec"]'); await page.click('[data-testid="done-set-btn"]'); - for (let i = 1; i < 14; i++) { - try { - await page.locator('#skip-timer-btn').waitFor({ state: 'visible', timeout: 1000 }); - await page.click('#skip-timer-btn'); - } catch { /* no timer */ } - await page.click('[data-testid="done-set-btn"]'); - } + await completeRemainingSets(page, 1); await page.click('#complete-workout-btn'); await expect(page.locator('#failure-sheet')).toBeVisible(); @@ -299,16 +283,14 @@ test.describe('Workout Flow', () => { const timer = page.locator('#rest-timer'); await expect(timer).toBeVisible(); - // Scroll to the bottom of the page + // Scroll to the bottom of the page and wait for scroll to settle await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await page.waitForTimeout(100); - - // Timer should still be visible in viewport await expect(timer).toBeInViewport(); }); test('visual snapshot of workout screen', async ({ page }) => { - await page.waitForTimeout(300); + // Wait for all set items to render before taking screenshot + await expect(page.locator('.set-item').first()).toBeVisible(); await expect(page).toHaveScreenshot('workout-screen.png', { maxDiffPixelRatio: 0.05, }); diff --git a/workout-tracker/playwright.config.ts b/workout-tracker/playwright.config.ts index 177a902..7fd466f 100644 --- a/workout-tracker/playwright.config.ts +++ b/workout-tracker/playwright.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 1 : 0, + retries: 0, workers: process.env.CI ? 2 : undefined, reporter: 'html', timeout: 15_000,