Files
gymflow/tests/04_data_progress.spec.ts

459 lines
25 KiB
TypeScript

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 initAcc = page.getByRole('heading', { name: /Setup Your Account/i });
const dashboard = page.getByText('Free Workout');
await expect(heading.or(initAcc).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(initAcc.or(dashboard)).toBeVisible();
}
if (await initAcc.isVisible()) {
await page.getByRole('button', { name: /Get Started/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<string, string> = {};
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();
});
});