import { test, expect } from './fixtures'; import { randomUUID } from 'crypto'; // Helper for setup async function loginAndSetup(page: any, createUniqueUser: any) { const user = await createUniqueUser(); await page.goto('/'); await page.getByLabel('Email').fill(user.email); await page.getByLabel('Password').fill(user.password); await page.getByRole('button', { name: 'Login' }).click(); try { const heading = page.getByRole('heading', { name: /Change Password/i }); const dashboard = page.getByText('Free Workout'); await expect(heading.or(dashboard)).toBeVisible({ timeout: 5000 }); if (await heading.isVisible()) { await page.getByLabel('New Password').fill('StrongNewPass123!'); await page.getByRole('button', { name: /Save|Change/i }).click(); await expect(dashboard).toBeVisible(); } } catch (e) { // Login might already be done } return user; } test.describe('IV. Data & Progress', () => { test('4.1. A. Session History - View Past Sessions', async ({ page, createUniqueUser, request }) => { const user = await loginAndSetup(page, createUniqueUser); const exNameSession = 'Hist View Session ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exNameSession, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click(); await page.getByRole('textbox', { name: /Select Exercise/i }).click(); await page.getByText(exNameSession).click(); await page.getByLabel('Weight (kg)').first().fill('50'); await page.getByLabel('Reps').first().fill('10'); await page.getByRole('button', { name: /Log Set/i }).click(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); const exNameSporadic = 'Hist View Sporadic ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exNameSporadic, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: 'Quick Log' }).click(); await page.getByRole('textbox', { name: /Select Exercise/i }).click(); await page.getByText(exNameSporadic).click(); await page.getByLabel(/Reps/i).first().fill('12'); await page.getByRole('button', { name: /Log Set/i }).click(); await page.getByRole('button', { name: 'Quit' }).click(); await page.getByRole('button', { name: 'History' }).click(); await expect(page.getByRole('heading', { name: 'History' })).toBeVisible(); await expect(page.getByText(/50\s*kg\s*x\s*12\s*reps/).or(page.getByText(/x 12 reps/))).toBeVisible(); await expect(page.getByText('No plan').first()).toBeVisible(); await expect(page.getByText('Sets:').first()).toBeVisible(); await expect(page.getByRole('heading', { name: 'Quick Log' })).toBeVisible(); }); test('4.2. A. Session History - View Detailed Session', async ({ page, createUniqueUser, request }) => { const user = await loginAndSetup(page, createUniqueUser); const exName = 'Detail View ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click(); await page.getByRole('textbox', { name: /Select Exercise/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel('Weight (kg)').first().fill('50'); await page.getByLabel('Reps').first().fill('10'); await page.getByRole('button', { name: /Log Set/i }).click(); await page.getByRole('button', { name: 'Finish' }).click(); // Wait for session save to complete const savePromise = page.waitForResponse(resp => resp.url().endsWith('/sessions/active') && resp.request().method() === 'PUT'); await page.getByRole('button', { name: 'Confirm' }).click(); const saveResp = await savePromise; if (!saveResp.ok()) console.log('Save failed:', await saveResp.text()); expect(saveResp.ok()).toBeTruthy(); await page.getByRole('button', { name: 'History' }).click(); await page.getByText('No plan').first().click(); await expect(page.getByRole('heading', { name: /Edit|Session Details/ })).toBeVisible(); await expect(page.getByText('Start')).toBeVisible(); await expect(page.getByText('End')).toBeVisible(); }); test('4.3. A. Session History - Edit Past Session Details', async ({ page, createUniqueUser, request }) => { const user = await loginAndSetup(page, createUniqueUser); const exName = 'Edit Sess ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); // Wait for session save to complete const savePromise = page.waitForResponse(resp => resp.url().endsWith('/sessions/active') && resp.request().method() === 'PUT'); await page.getByRole('button', { name: 'Confirm' }).click(); const saveResp = await savePromise; if (!saveResp.ok()) console.log('Save failed:', await saveResp.text()); expect(saveResp.ok()).toBeTruthy(); await page.getByRole('button', { name: 'History' }).click(); await page.getByText('No plan').first().click(); await page.getByRole('spinbutton').first().fill('75.5'); await page.getByRole('button', { name: 'Save' }).click(); await expect(page.getByText('75.5kg')).toBeVisible(); }); test('4.4. A. Session History - Edit Individual Set in Past Session', async ({ page, createUniqueUser, request }) => { const user = await loginAndSetup(page, createUniqueUser); const exName = 'Edit Set ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: `Log Set` }).click(); await expect(page.getByText('50 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); await page.getByRole('button', { name: 'History' }).click(); await page.getByText('No plan').first().click(); // Click the pencil icon to edit the set await page.getByRole('button', { name: 'Edit' }).first().click(); // Find the input with value 50. It might be a number input. // Also wait for the input to be visible first await expect(page.locator('input[value="50"]')).toBeVisible(); await page.locator('input[value="50"]').fill('55'); await page.getByRole('button', { name: 'Save' }).last().click(); await expect(page.getByText('55 kg x 10 reps')).toBeVisible(); }); test('4.5. A. Session History - Verify Edit Fields per Exercise Type', async ({ page, createUniqueUser, request }) => { // Merged from repro_edit_fields.spec.ts const user = await loginAndSetup(page, createUniqueUser); const types = [ { type: 'PLYOMETRIC', name: 'Plyo Test', expectedFields: ['Reps'] }, { type: 'STRENGTH', name: 'Strength Test', expectedFields: ['Weight (kg)', 'Reps'] }, { type: 'CARDIO', name: 'Cardio Test', expectedFields: ['Time (sec)', 'Distance (m)'] }, { type: 'STATIC', name: 'Static Test', expectedFields: ['Time (sec)', 'Weight (kg)', 'Body Weight'] }, { type: 'BODYWEIGHT', name: 'Bodyweight Test', expectedFields: ['Reps', 'Body Weight', 'Weight (kg)'] }, { type: 'HIGH_JUMP', name: 'High Jump Test', expectedFields: ['Height (cm)'] }, { type: 'LONG_JUMP', name: 'Long Jump Test', expectedFields: ['Distance (m)'] }, ]; const exIds: Record = {}; for (const t of types) { const resp = await request.post('/api/exercises', { data: { name: t.name, type: t.type }, headers: { 'Authorization': `Bearer ${user.token}` } }); expect(resp.ok()).toBeTruthy(); const created = await resp.json(); exIds[t.name] = created.data?.id; } await page.reload(); const now = Date.now(); const setsStub = types.map(t => { const set: any = { exerciseId: exIds[t.name], timestamp: now + 1000, completed: true }; if (t.type === 'STRENGTH' || t.type === 'BODYWEIGHT' || t.type === 'PLYOMETRIC') set.reps = 10; if (t.type === 'STRENGTH' || t.type === 'BODYWEIGHT' || t.type === 'STATIC') set.weight = 50; if (t.type === 'BODYWEIGHT' || t.type === 'STATIC') set.bodyWeightPercentage = 100; if (t.type === 'CARDIO' || t.type === 'STATIC') set.durationSeconds = 60; if (t.type === 'CARDIO' || t.type === 'LONG_JUMP') set.distanceMeters = 100; if (t.type === 'HIGH_JUMP') set.height = 150; return set; }); const sessionResp = await request.post('/api/sessions', { data: { id: randomUUID(), // Required by saveSession service startTime: now, endTime: now + 3600000, type: 'STANDARD', sets: setsStub }, headers: { 'Authorization': `Bearer ${user.token}` } }); if (!sessionResp.ok()) console.log('Session create failed:', await sessionResp.json()); expect(sessionResp.ok()).toBeTruthy(); await page.getByRole('button', { name: 'History' }).first().click(); // Click Session Actions menu button, then Edit from dropdown await page.getByRole('button', { name: 'Session Actions' }).click(); await page.getByRole('button', { name: /Edit/i }).click(); await expect(page.getByText('Edit', { exact: true })).toBeVisible(); for (const t of types) { // Find the set row in the session edit dialog const row = page.locator('.bg-surface-container-low').filter({ hasText: t.name }).first(); await expect(row).toBeVisible(); // Click the Edit button for this specific set to open the set edit modal await row.getByRole('button', { name: /Edit/i }).click(); // Wait for Edit Set modal to open await expect(page.getByRole('heading', { name: 'Edit Set' })).toBeVisible(); // Get the Edit Set dialog to scope our searches const editSetDialog = page.getByRole('dialog').filter({ hasText: 'Edit Set' }); // Verify the expected field labels are present in the modal for (const field of t.expectedFields) { // Use exact matching to avoid ambiguity (e.g., 'Time' vs 'Time (sec)') await expect(editSetDialog.getByText(field, { exact: true })).toBeVisible(); } // Close the set edit modal before moving to the next set await page.getByRole('dialog').filter({ hasText: 'Edit Set' }).getByRole('button', { name: /Close/i }).click(); } }); test('4.6. A. Session History - Delete Past Session', async ({ page, createUniqueUser, request }) => { const user = await loginAndSetup(page, createUniqueUser); const exName = 'Del Sess ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); // Wait for session save to complete const savePromise = page.waitForResponse(resp => resp.url().endsWith('/sessions/active') && resp.request().method() === 'PUT'); await page.getByRole('button', { name: 'Confirm' }).click(); const saveResp = await savePromise; if (!saveResp.ok()) console.log('Save failed:', await saveResp.text()); expect(saveResp.ok()).toBeTruthy(); await page.getByRole('button', { name: 'History' }).click(); // Open session menu and delete await page.getByRole('button', { name: 'Session Actions' }).first().click(); await page.getByRole('button', { name: 'Delete', exact: true }).filter({ hasText: 'Delete' }).first().click(); // Click delete in menu await page.getByRole('button', { name: 'Delete', exact: true }).last().click(); // Click delete in confirmation modal await expect(page.getByText('History is empty')).toBeVisible(); }); test('4.7. A. Session History - Edit Sporadic Set', async ({ page, createUniqueUser, request }) => { const user = await loginAndSetup(page, createUniqueUser); const exName = 'Spor Edit ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: 'Quick Log' }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('12'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 12 reps')).toBeVisible(); await page.getByRole('button', { name: 'Quit' }).click(); await page.getByRole('button', { name: 'History' }).click(); await page.getByRole('button', { name: /Edit/i }).first().click(); // Edit await expect(page.getByRole('heading', { name: `Edit Set` })).toBeVisible(); await page.locator('input[value="12"]').fill('15'); await page.getByRole('button', { name: 'Save' }).click(); await expect(page.getByText(/50\s*kg\s*x\s*15\s*reps/)).toBeVisible(); }); test('4.8. A. Session History - Delete Sporadic Set', async ({ page, createUniqueUser, request }) => { const user = await loginAndSetup(page, createUniqueUser); const exName = 'Spor Del ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: 'Quick Log' }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('12'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 12 reps')).toBeVisible(); await page.getByRole('button', { name: 'Quit' }).click(); await page.getByRole('button', { name: 'History' }).click(); await page.getByRole('button', { name: /Delete/i }).first().click(); // Delete icon // Scope to dialog to avoid finding the icon button behind it await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(); // Confirm delete await expect(page.getByText('50 kg x 12 reps')).not.toBeVisible(); }); test('4.9. A. Session History - Export CSV', async ({ page, createUniqueUser, request }) => { // Merged from history-export.spec.ts const user = await loginAndSetup(page, createUniqueUser); const exName = 'Bench Press Test'; await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: 'Free Workout' }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByText(exName).first().click(); await page.getByLabel(/Weight/i).first().fill('100'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: 'Log Set' }).click(); await expect(page.getByText('100 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); await page.getByRole('button', { name: 'History' }).click(); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Export CSV' }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toContain('gymflow_history'); expect(download.suggestedFilename()).toContain('.csv'); }); test('4.10. B. Performance Statistics - View Volume Chart', async ({ page, createUniqueUser, request }) => { test.setTimeout(120000); const user = await loginAndSetup(page, createUniqueUser); const exName = 'Vol Chart ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); // Session 1 await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); // Session 2 await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('60'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('60 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); await page.getByRole('button', { name: 'Stats' }).click(); await expect(page.getByText('Work Volume')).toBeVisible(); }); test('4.11. B. Performance Statistics - View Set Count Chart', async ({ page, createUniqueUser, request }) => { test.setTimeout(120000); const user = await loginAndSetup(page, createUniqueUser); const exName = 'Set Chart ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); // Session 1 await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); // Session 2 await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('60'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('60 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); await page.getByRole('button', { name: 'Stats' }).click(); await expect(page.getByText('Number of Sets')).toBeVisible(); }); test('4.12. B. Performance Statistics - View Body Weight Chart', async ({ page, createUniqueUser, request }) => { test.setTimeout(120000); const user = await loginAndSetup(page, createUniqueUser); const exName = 'BW Chart ' + randomUUID().slice(0, 4); await request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } }); await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); // Second session to satisfy "Not enough data" check await page.getByRole('button', { name: /Free Workout/i }).click(); await page.getByRole('textbox', { name: /Select/i }).click(); await page.getByRole('button', { name: exName }).click(); await page.getByLabel(/Weight/i).first().fill('50'); await page.getByLabel(/Reps/i).first().fill('10'); await page.getByRole('button', { name: /Log/i }).click(); await expect(page.getByText('50 kg x 10 reps')).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const dateStr = yesterday.toISOString().split('T')[0]; await page.evaluate(async ({ token, dateStr }) => { await fetch('/api/weight', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ weight: 70, dateStr }) }); }, { token: user.token, dateStr }); await page.getByRole('button', { name: 'Profile' }).click(); await page.getByRole('button', { name: 'Weight Tracker' }).click(); await page.getByPlaceholder('Enter weight...').fill('72'); await page.getByRole('button', { name: 'Log', exact: true }).click(); await expect(page.getByText('Weight logged successfully')).toBeVisible(); await page.getByRole('button', { name: 'Stats' }).click(); await expect(page.getByText('Body Weight History')).toBeVisible(); }); });