All tests fixed. Deployment on NAS prepared

This commit is contained in:
aodulov
2025-12-18 07:29:35 +02:00
parent 9cb0d66455
commit 97b4e5de32
37 changed files with 1303 additions and 2083 deletions

80
deployment_guide.md Normal file
View File

@@ -0,0 +1,80 @@
# GymFlow Deployment Guide (NAS / Existing Container)
This guide explains how to deploy GymFlow to your Ugreen NAS by integrating it into your existing `nodejs-apps` container.
## 1. Preparation (Build)
You need to build the application on your local machine before transferring it to the NAS.
### Build Frontend
Run the following in the project root:
```bash
npm install
npm run build
```
This creates a `dist` folder with the compiled frontend.
### Build Backend
Run the following in the project root:
```bash
cd server
npm install
npm run build
cd ..
```
This creates a `server/dist` folder with the compiled backend.
## 2. Transfer Files
Copy the entire `gymflow` folder to your NAS.
Destination: `nodejs_data/gymflow` (inside the folder mounted to `/usr/src/app`).
Ensure the following files/folders are present on the NAS:
- `gymflow/dist/` (Frontend assets)
- `gymflow/server/dist/` (Backend code)
- `gymflow/server/package.json`
- `gymflow/server/prisma/` (Schema for database)
- `gymflow/ecosystem.config.cjs` (PM2 config)
*Note: You do not need to copy `node_modules`. We will install production dependencies on the NAS to ensure compatibility.*
## 3. Integration
Update your `docker-compose.yml` to include GymFlow in the startup command and map the new port.
### Update `command`
Add the following to your existing command string (append before the final `pm2 logs`):
```bash
... && cd ../gymflow/server && npm install --omit=dev && DATABASE_URL=file:./prod.db npx prisma db push && cd .. && pm2 start ecosystem.config.cjs && ...
```
*Note: We assume `gymflow` is a sibling of `ag-beats` etc.*
**Full Command Example:**
```yaml
command: /bin/sh -c "npm install -g pm2 && cd ag-beats && npm install && cd ../ball-shooting && npm install && cd ../gymflow/server && npm install --omit=dev && DATABASE_URL=file:./prod.db npx prisma db push && cd .. && pm2 start ecosystem.config.cjs && cd ../ag-beats && pm2 start ecosystem.config.js && cd ../ball-shooting && pm2 start npm --name ag-ball -- start && pm2 logs --raw"
```
### Update `ports`
Add the GymFlow port (3003 inside container, mapped to your choice, e.g., 3033):
```yaml
ports:
- "3030:3000"
- "3031:3001"
- "3032:3002"
- "3033:3003" # GymFlow
```
## 4. Environment Variables
GymFlow uses `dotenv` but in this setup PM2 handles the variables via `ecosystem.config.cjs`.
- Port: `3003` (Internal)
- Database: `server/prod.db` (SQLite)
- Node Env: `production`
## 5. Nginx Proxy Manager
Point your domain (e.g., `gym.yourdomain.com`) to the NAS IP and the mapped port (`3033`).
- Scheme: `http`
- Forward Hostname / IP: `[NAS_IP]`
- Forward Port: `3033`
- Websockets Support: Enable (if needed for future features).

25
ecosystem.config.cjs Normal file
View File

@@ -0,0 +1,25 @@
const path = require('path');
module.exports = {
apps: [{
name: 'gymflow',
script: './dist/index.js',
cwd: './server',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 3003,
DATABASE_URL: 'file:../prod.db' // Relative to server/dist/index.js? No, relative to CWD if using prisma.
// Actually, prisma runs from where schema is or based on env.
// Let's assume the DB is in the root or server dir.
// In package.json: "start:prod": "cross-env APP_MODE=prod DATABASE_URL=file:./prod.db ... src/index.ts"
// If CWD is ./server, then ./prod.db is inside server/prod.db.
},
env_production: {
NODE_ENV: 'production'
}
}]
};

BIN
final_report.json Normal file

Binary file not shown.

View File

@@ -19,4 +19,4 @@ ADMIN_PASSWORD_PROD="secure-prod-password-change-me"
DATABASE_URL="file:./prisma/dev.db"
GEMINI_API_KEY=AIzaSyC88SeFyFYjvSfTqgvEyr7iqLSvEhuadoE
DEFAULT_EXERCISES_CSV_PATH='default_exercises.csv'
DEFAULT_EXERCISES_CSV_PATH="default_exercises.csv"

Binary file not shown.

View File

@@ -10,7 +10,7 @@ import prisma from './src/lib/prisma';
}
let user;
for (let i = 0; i < 5; i++) {
for (let i = 0; i < 10; i++) {
user = await prisma.user.findUnique({ where: { email } });
if (user) break;
console.log(`User ${email} not found, retrying (${i + 1}/5)...`);

View File

@@ -92,9 +92,23 @@ app.use('/api/weight', weightRoutes);
app.use('/api/bookmarks', bookmarksRoutes);
app.get('/', (req, res) => {
res.send('GymFlow AI API is running');
// Serve frontend in production
if (process.env.NODE_ENV === 'production') {
const distPath = path.join(__dirname, '../../dist');
app.use(express.static(distPath));
app.get('*', (req, res) => {
if (!req.path.startsWith('/api')) {
res.sendFile(path.join(distPath, 'index.html'));
} else {
res.status(404).json({ error: 'API route not found' });
}
});
} else {
app.get('/', (req, res) => {
res.send('GymFlow AI API is running (Dev Mode)');
});
}
ensureAdminUser()
.catch((e) => {

View File

@@ -8,14 +8,18 @@ export const sessionSchema = z.object({
note: z.string().nullable().optional(),
planId: z.string().nullable().optional(),
planName: z.string().nullable().optional(),
type: z.string().optional(), // Added missing field
sets: z.array(z.object({
exerciseId: z.string(),
timestamp: z.union([z.number(), z.string(), z.date()]).optional(), // Added missing field
weight: z.number().nullable().optional(),
reps: z.number().nullable().optional(),
distanceMeters: z.number().nullable().optional(),
durationSeconds: z.number().nullable().optional(),
completed: z.boolean().optional().default(true),
side: z.string().nullable().optional()
side: z.string().nullable().optional(),
height: z.number().nullable().optional(), // Added missing field
bodyWeightPercentage: z.number().nullable().optional() // Added missing field
}))
})
});

Binary file not shown.

View File

@@ -754,7 +754,23 @@ Comprehensive test plan for the GymFlow web application, covering authentication
- The individual set's metrics are updated.
- The changes are reflected in the detailed session view.
#### 4.5. A. Session History - Delete Past Session
#### 4.5. A. Session History - Verify Edit Fields per Exercise Type
**File:** `tests/data-progress.spec.ts`
**Steps:**
1. Log in as a regular user.
2. Create exercises of different types (Strength, Cardio, etc.).
3. Log a session containing these exercises.
4. Navigate to History and open the session details.
5. Click 'Edit' (pencil icon) for each set.
6. Verify the edit modal displays the correct fields for that exercise type (e.g. Weight/Reps for Strength, Time/Distance for Cardio).
**Expected Results:**
- The correct input fields are visible for each exercise type.
- No irrelevant fields are displayed.
#### 4.6. A. Session History - Delete Past Session
**File:** `tests/data-progress.spec.ts`
@@ -769,7 +785,7 @@ Comprehensive test plan for the GymFlow web application, covering authentication
- The session is permanently removed from the history.
- No error messages.
#### 4.6. A. Session History - Edit Sporadic Set
#### 4.7. A. Session History - Edit Sporadic Set
**File:** `tests/data-progress.spec.ts`
@@ -785,7 +801,7 @@ Comprehensive test plan for the GymFlow web application, covering authentication
- The sporadic set's metrics are updated.
- The changes are reflected in the history view.
#### 4.7. A. Session History - Delete Sporadic Set
#### 4.8. A. Session History - Delete Sporadic Set
**File:** `tests/data-progress.spec.ts`
@@ -799,7 +815,7 @@ Comprehensive test plan for the GymFlow web application, covering authentication
**Expected Results:**
- The sporadic set is permanently removed from the history.
#### 4.8. A. Session History - Export CSV
#### 4.9. A. Session History - Export CSV
**File:** `tests/history-export.spec.ts`
@@ -815,7 +831,7 @@ Comprehensive test plan for the GymFlow web application, covering authentication
- The CSV content contains headers and data rows corresponding to the user's workout history.
- No error messages.
#### 4.8. B. Performance Statistics - View Volume Chart
#### 4.10. B. Performance Statistics - View Volume Chart
**File:** `tests/data-progress.spec.ts`
@@ -828,7 +844,7 @@ Comprehensive test plan for the GymFlow web application, covering authentication
- The 'Total Volume' line chart is displayed.
- The chart accurately reflects the total weight lifted per session over time.
#### 4.9. B. Performance Statistics - View Set Count Chart
#### 4.11. B. Performance Statistics - View Set Count Chart
**File:** `tests/data-progress.spec.ts`
@@ -841,7 +857,7 @@ Comprehensive test plan for the GymFlow web application, covering authentication
- The 'Set Count' bar chart is displayed.
- The chart accurately reflects the number of sets performed per session over time.
#### 4.10. B. Performance Statistics - View Body Weight Chart
#### 4.12. B. Performance Statistics - View Body Weight Chart
**File:** `tests/data-progress.spec.ts`

View File

@@ -95,10 +95,37 @@ const EditSetModal: React.FC<EditSetModalProps> = ({
<h3 className="text-lg font-medium text-on-surface mb-1">
{set.exerciseName}
</h3>
<p className="text-sm text-on-surface-variant">
<p className="text-sm text-on-surface-variant mb-2">
{exerciseDef?.type || set.type}
{set.side && `${t(set.side.toLowerCase() as any, lang)}`}
</p>
{set.side && (
<div className="flex items-center gap-2 bg-surface-container rounded-full p-1 w-fit">
<button
onClick={() => handleUpdate('side', 'LEFT')}
title={t('left', lang)}
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${set.side === 'LEFT' ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
}`}
>
L
</button>
<button
onClick={() => handleUpdate('side', 'ALTERNATELY')}
title={t('alternately', lang)}
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${set.side === 'ALTERNATELY' ? 'bg-tertiary-container text-on-tertiary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
}`}
>
A
</button>
<button
onClick={() => handleUpdate('side', 'RIGHT')}
title={t('right', lang)}
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${set.side === 'RIGHT' ? 'bg-secondary-container text-on-secondary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
}`}
>
R
</button>
</div>
)}
</div>
{/* Date & Time */}

View File

@@ -318,6 +318,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
variant="ghost"
size="icon"
className="text-on-surface-variant hover:text-primary"
aria-label={t('edit', lang)}
>
<Pencil size={24} />
</Button>
@@ -332,6 +333,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
variant="ghost"
size="icon"
className="text-error hover:text-error"
aria-label={t('delete', lang)}
>
<Trash2 size={24} />
</Button>

View File

@@ -123,7 +123,7 @@ const SortablePlanStep: React.FC<SortablePlanStepProps> = ({ step, index, toggle
};
return (
<div ref={setNodeRef} style={style} {...attributes}>
<div ref={setNodeRef} style={style} {...attributes} data-testid="plan-exercise-item">
<Card
className={`flex items-center gap-3 transition-all hover:bg-surface-container-high ${isDragging ? 'bg-surface-container-high shadow-elevation-3' : ''}`}
>
@@ -666,35 +666,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
</SideSheet>
<SideSheet
isOpen={showExerciseSelector}
onClose={() => setShowExerciseSelector(false)}
title={t('select_exercise', lang)}
width="lg"
>
<div className="flex flex-col h-[60vh]">
<div className="flex justify-end mb-2">
<Button onClick={() => setIsCreatingExercise(true)} variant="ghost" className="text-primary hover:bg-primary-container/20 flex gap-2">
<Plus size={18} /> {t('create_exercise', lang)}
</Button>
</div>
<div className="flex-1 overflow-y-auto -mx-6 px-6">
{availableExercises
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(ex => (
<button
key={ex.id}
onClick={() => addStep(ex)}
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between group"
>
<span className="group-hover:text-primary transition-colors">{ex.name}</span>
<span className="text-xs bg-secondary-container text-on-secondary-container px-2 py-1 rounded-full">{ex.type}</span>
</button>
))}
</div>
</div>
</SideSheet>
<ExerciseModal
isOpen={isCreatingExercise}

View File

@@ -35,6 +35,13 @@ export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
let initialDuration = defaultTime;
if (savedState) {
// Only restore if running OR if the context (defaultTime) matches
// This prevents carrying over timer state between different plan steps or sessions
// where the default time is different.
const contextMatch = savedState.defaultTimeSnapshot === defaultTime;
const isRunning = savedState.status === 'RUNNING';
if (isRunning || contextMatch) {
initialDuration = savedState.duration || defaultTime;
initialStatus = savedState.status;
initialTimeLeft = savedState.timeLeft;
@@ -50,6 +57,7 @@ export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
}
}
}
}
const [timeLeft, setTimeLeft] = useState(initialTimeLeft);
const [status, setStatus] = useState<TimerStatus>(initialStatus);
@@ -90,10 +98,11 @@ export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
status,
timeLeft,
duration,
endTime: endTimeRef.current
endTime: endTimeRef.current,
defaultTimeSnapshot: defaultTime // Save context
};
localStorage.setItem('gymflow_rest_timer', JSON.stringify(stateToSave));
}, [status, timeLeft, duration]);
}, [status, timeLeft, duration, defaultTime]);
// Update internal duration when defaultTime changes
useEffect(() => {

View File

@@ -125,7 +125,7 @@ const translations = {
my_plans: 'My Plans',
no_plans_yet: 'No workout plans yet.',
ask_ai_to_create: 'Ask your AI coach to create one',
create_manually: 'Create one manually',
create_manually: 'Manually',
create_with_ai: 'With AI',
ai_plan_prompt_title: 'Create Plan with AI',
ai_plan_prompt_placeholder: 'Any specific requirements? (optional)',

View File

@@ -32,6 +32,7 @@ export const getSessions = async (userId: string): Promise<WorkoutSession[]> =>
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
sets: session.sets.map((set) => ({
...set,
timestamp: new Date(set.timestamp).getTime(),
exerciseName: set.exerciseName || set.exercise?.name || 'Unknown',
type: set.type || set.exercise?.type || ExerciseType.STRENGTH
})) as WorkoutSet[]
@@ -60,6 +61,7 @@ export const getActiveSession = async (userId: string): Promise<WorkoutSession |
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
sets: session.sets.map((set) => ({
...set,
timestamp: new Date(set.timestamp).getTime(),
exerciseName: set.exerciseName || set.exercise?.name || 'Unknown',
type: set.type || set.exercise?.type || ExerciseType.STRENGTH
})) as WorkoutSet[]

9
test_report.json Normal file
View File

@@ -0,0 +1,9 @@
Error: No tests found.
Make sure that arguments are regular expressions matching test files.
You may need to escape symbols like "$" or "*" and quote the arguments.

To open last HTML report run:

 npx playwright show-report


View File

@@ -1,4 +1,3 @@
import { test, expect } from './fixtures';
test.describe('I. Core & Authentication', () => {
@@ -48,12 +47,7 @@ test.describe('I. Core & Authentication', () => {
console.log('Login Error detected:', await error.textContent());
throw new Error(`Login failed: ${await error.textContent()}`);
}
console.log('Failed to handle first login. Dumping page content...');
const fs = require('fs'); // Playwright runs in Node
await fs.writeFileSync('auth_failure.html', await page.content());
console.log(await page.content());
throw e;
// Note: If none of the above, it might be a clean login that just worked fast or failed silently
}
}

View File

@@ -0,0 +1,615 @@
import { test, expect } from './fixtures';
import { randomUUID } from 'crypto';
import { generateId } from '../src/utils/uuid'; // Helper from plan-from-session
test.describe('II. Workout Management', () => {
// Helper functions
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 or dashboard loaded fast
}
return user;
}
test.describe('A. Workout Plans', () => {
test('2.1 A. Workout Plans - Create New Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Seed exercise
const seedResp = await request.post('/api/exercises', {
data: { name: 'Test Sq', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(seedResp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
await page.getByRole('button', { name: 'Create Plan' }).click();
await page.getByRole('button', { name: 'Manually' }).click();
await expect(page.getByLabel(/Name/i)).toBeVisible({ timeout: 10000 });
await page.getByLabel(`Name`).fill('My New Strength Plan');
await page.getByLabel(`Preparation`).fill('Focus on compound lifts');
await page.getByRole('button', { name: 'Add Exercise' }).click();
await expect(page.getByRole('heading', { name: 'Select Exercise' })).toBeVisible();
await page.getByText('Test Sq').click();
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('My New Strength Plan')).toBeVisible();
await expect(page.getByText('Focus on compound lifts')).toBeVisible();
});
test('2.2 A. Workout Plans - Edit Existing Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const seedResp = await request.post('/api/plans', {
data: {
id: randomUUID(),
name: 'Original Plan',
description: 'Original Description',
steps: []
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(seedResp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
await expect(page.getByText('Original Plan')).toBeVisible();
const card = page.locator('div')
.filter({ hasText: 'Original Plan' })
.filter({ has: page.getByRole('button', { name: 'Edit Plan' }) })
.last();
await card.getByRole('button', { name: 'Edit Plan' }).click();
await page.getByLabel(/Name/i).fill('Updated Plan Name');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Updated Plan Name')).toBeVisible();
await expect(page.getByText('Original Plan')).not.toBeVisible();
});
test('2.3 A. Workout Plans - Delete Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const resp = await request.post('/api/plans', {
data: {
id: randomUUID(),
name: 'Plan To Delete',
description: 'Delete me',
steps: []
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(resp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
page.on('dialog', dialog => dialog.accept());
const card = page.locator('div')
.filter({ hasText: 'Plan To Delete' })
.filter({ has: page.getByRole('button', { name: 'Delete Plan' }) })
.last();
await card.getByRole('button', { name: 'Delete Plan' }).click();
await expect(page.getByText('Plan To Delete')).not.toBeVisible();
});
test('2.4 A. Workout Plans - Reorder Exercises', async ({ page, createUniqueUser, request }) => {
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
const user = await loginAndSetup(page, createUniqueUser);
// Need exercises
const ex1Id = randomUUID();
const ex2Id = randomUUID();
await request.post('/api/exercises', {
data: { id: ex1Id, name: 'Ex One', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
await request.post('/api/exercises', {
data: { id: ex2Id, name: 'Ex Two', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
const planId = randomUUID();
await request.post('/api/plans', {
data: {
id: planId,
name: 'Reorder Plan',
description: 'Testing reorder',
steps: [
{ exerciseId: ex1Id, isWeighted: false },
{ exerciseId: ex2Id, isWeighted: false }
]
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
const card = page.locator('div')
.filter({ hasText: 'Reorder Plan' })
.filter({ has: page.getByRole('button', { name: 'Edit Plan' }) })
.last();
await card.getByRole('button', { name: 'Edit Plan' }).click();
// Verify order: Ex One then Ex Two
const items = page.getByTestId('plan-exercise-item');
await expect(items.first()).toContainText('Ex One');
await expect(items.nth(1)).toContainText('Ex Two');
// Drag and drop to reorder manually (dnd-kit needs steps)
const sourceHandle = items.first().locator('.lucide-grip-vertical');
const targetHandle = items.nth(1).locator('.lucide-grip-vertical');
const sourceBox = await sourceHandle.boundingBox();
const targetBox = await targetHandle.boundingBox();
if (sourceBox && targetBox) {
await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2);
await page.mouse.down();
await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, { steps: 20 });
await page.mouse.up();
}
// Verify new order: Ex Two then Ex One
await expect(items.first()).toContainText('Ex Two');
await expect(items.nth(1)).toContainText('Ex One');
await page.getByRole('button', { name: 'Save' }).click();
await page.waitForTimeout(1000);
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
const cardRevisit = page.locator('div')
.filter({ hasText: 'Reorder Plan' })
.filter({ has: page.getByRole('button', { name: 'Edit Plan' }) })
.last();
await cardRevisit.getByRole('button', { name: 'Edit Plan' }).click();
await expect(page.getByTestId('plan-exercise-item').first()).toContainText('Ex Two');
await expect(page.getByTestId('plan-exercise-item').last()).toContainText('Ex One');
});
test('2.5 A. Workout Plans - Start Session from Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const resp = await request.post('/api/plans', {
data: {
id: randomUUID(),
name: 'Startable Plan',
description: 'Ready to go',
steps: []
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(resp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
const card = page.locator('div')
.filter({ hasText: 'Startable Plan' })
.filter({ has: page.getByRole('button', { name: 'Start' }) })
.last();
await card.getByRole('button', { name: 'Start' }).click();
const modal = page.locator('.fixed.inset-0.z-50');
await expect(modal).toBeVisible();
await expect(modal.getByText('Ready to go')).toBeVisible();
await modal.getByRole('button', { name: 'Start' }).click();
await expect(page.getByText('Startable Plan', { exact: false })).toBeVisible();
await expect(page.getByRole('button', { name: 'Finish' })).toBeVisible();
});
test('2.5a A. Workout Plans - Create Plan from Session', async ({ page, request, createUniqueUser }) => {
// Seed data BEFORE login to ensure it's loaded when accessing history
const user = await createUniqueUser();
const token = user.token;
const pushupsId = generateId();
const squatsId = generateId();
await request.post('/api/exercises', {
headers: { Authorization: `Bearer ${token}` },
data: { id: pushupsId, name: 'Test Pushups', type: 'BODYWEIGHT', isUnilateral: false }
});
await request.post('/api/exercises', {
headers: { Authorization: `Bearer ${token}` },
data: { id: squatsId, name: 'Test Squats', type: 'STRENGTH', isUnilateral: false }
});
const sessionId = generateId();
const sessionData = {
id: sessionId,
startTime: Date.now() - 3600000,
endTime: Date.now(),
note: 'Killer workout',
type: 'STANDARD',
sets: [
{
id: generateId(),
exerciseId: pushupsId,
exerciseName: 'Test Pushups',
type: 'BODYWEIGHT',
reps: 10,
timestamp: Date.now() - 3000000,
completed: true
},
{
id: generateId(),
exerciseId: pushupsId,
exerciseName: 'Test Pushups',
type: 'BODYWEIGHT',
reps: 12,
weight: 10,
timestamp: Date.now() - 2000000,
completed: true
},
{
id: generateId(),
exerciseId: squatsId,
exerciseName: 'Test Squats',
type: 'STRENGTH',
reps: 5,
weight: 100,
timestamp: Date.now() - 1000000,
completed: true
}
]
};
const response = await request.post('/api/sessions', {
headers: { Authorization: `Bearer ${token}` },
data: sessionData
});
expect(response.ok()).toBeTruthy();
// Login manually (using clean selectors) to avoid reload issues
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle password change if it appears (reusing logic from helper)
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 or dashboard loaded fast
}
await page.getByRole('button', { name: 'History' }).click();
await page.waitForURL('**/history');
// Wait for sessions to load
const sessionActions = page.getByLabel('Session Actions').first();
await expect(sessionActions).toBeVisible({ timeout: 15000 });
await sessionActions.click();
await page.getByRole('button', { name: 'Create Plan' }).click();
// Verify Editor opens and data is populated (URL param is cleared immediately so we check UI)
await expect(page.getByRole('heading', { name: 'Plan Editor' })).toBeVisible({ timeout: 10000 });
await expect(page.locator('textarea')).toHaveValue('Killer workout');
// Check exercises are populated
const stepNames = page.getByTestId('plan-exercise-item');
await expect(stepNames).toHaveCount(3);
await expect(stepNames.nth(0)).toContainText('Test Pushups');
await expect(stepNames.nth(1)).toContainText('Test Pushups');
await expect(stepNames.nth(2)).toContainText('Test Squats');
// Verify weighted checkboxes
const items = page.getByTestId('plan-exercise-item');
await expect(items.nth(0).locator('input[type="checkbox"]')).not.toBeChecked();
await expect(items.nth(1).locator('input[type="checkbox"]')).toBeChecked();
await expect(items.nth(2).locator('input[type="checkbox"]')).toBeChecked();
});
test('2.14 A. Workout Plans - Create Plan with AI (Parametrized)', async ({ page, createUniqueUser }) => {
// Merged from ai-plan-creation.spec.ts
const user = await loginAndSetup(page, createUniqueUser);
await page.route('**/api/ai/chat', async route => {
const plan = {
name: 'AI Advanced Plan',
description: 'Generated High Intensity Plan',
exercises: [
{ name: 'Mock Push-ups', isWeighted: false, restTimeSeconds: 60, type: 'BODYWEIGHT', unilateral: false },
{ name: 'Mock Weighted Pull-ups', isWeighted: true, restTimeSeconds: 90, type: 'BODYWEIGHT', unilateral: false }
]
};
await route.fulfill({
json: {
success: true,
data: {
response: JSON.stringify(plan)
}
}
});
});
await page.getByRole('button', { name: 'Plans' }).first().click();
const fab = page.getByLabel('Create Plan').or(page.getByRole('button', { name: '+' }));
await fab.click();
await page.getByRole('button', { name: 'With AI' }).click();
await expect(page.getByText('Create Plan with AI')).toBeVisible();
const eqSection = page.locator('div').filter({ hasText: 'Equipment' }).last();
const levelSection = page.locator('div').filter({ hasText: 'Level' }).last();
const intensitySection = page.locator('div').filter({ hasText: 'Intensity' }).last();
await levelSection.getByRole('button', { name: 'Advanced' }).click();
await intensitySection.getByRole('button', { name: 'High' }).click();
await eqSection.getByRole('button', { name: /Free weights/i }).click();
await page.getByRole('button', { name: 'Generate' }).click();
await expect(page.getByText('Generated Plan')).toBeVisible({ timeout: 10000 });
await expect(page.getByText('Mock Push-ups')).toBeVisible();
await page.getByRole('button', { name: 'Save Plan' }).click();
await expect(page.getByText('AI Advanced Plan')).toBeVisible();
});
});
test.describe('B. Exercise Library', () => {
test('2.6 B. Exercise Library - Create Custom Exercise (Strength)', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Custom Bicep Curl');
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await expect(page.locator('div[role="dialog"]')).not.toBeVisible();
await page.reload();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByLabel(/Filter by name/i).fill('Custom Bicep Curl');
await expect(page.getByText('Custom Bicep Curl')).toBeVisible();
});
test('2.7 B. Exercise Library - Create Custom Exercise (Bodyweight)', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Adv Pushup');
await page.locator('div[role="dialog"]').getByRole('button', { name: /Bodyweight/i }).click({ force: true });
await expect(page.getByLabel('Body Weight')).toBeVisible();
await page.getByLabel('Body Weight').fill('50');
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await expect(page.locator('div[role="dialog"]')).not.toBeVisible();
await page.reload();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('Adv Pushup');
await expect(page.getByText('Adv Pushup')).toBeVisible();
});
test('2.8 B. Exercise Library - Edit Exercise Name', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Typo Name');
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await expect(page.locator('div[role="dialog"]')).not.toBeVisible();
await page.reload();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByLabel(/Filter by name/i).fill('Typo Name');
await expect(page.getByText('Typo Name')).toBeVisible();
const row = page.locator('div')
.filter({ hasText: 'Typo Name' })
.filter({ has: page.getByLabel('Edit Exercise') })
.last();
await row.getByLabel('Edit Exercise').click();
await page.locator('div[role="dialog"] input').first().fill('Fixed Name');
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Save', exact: true }).click();
await page.getByLabel(/Filter by name/i).fill('');
await expect(page.getByText('Fixed Name')).toBeVisible();
});
test('2.9 B. Exercise Library - Archive/Unarchive', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Archive Me');
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await expect(page.locator('div[role="dialog"]')).not.toBeVisible();
await page.reload();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByLabel(/Filter by name/i).fill('Archive Me');
await expect(page.getByText('Archive Me')).toBeVisible();
const row = page.locator('div.flex.justify-between').filter({ hasText: 'Archive Me' }).last();
await row.locator('[aria-label="Archive Exercise"]').click();
await expect(page.getByText('Archive Me')).not.toBeVisible();
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').check();
await expect(page.getByText('Archive Me')).toBeVisible();
const archivedRow = page.locator('div')
.filter({ hasText: 'Archive Me' })
.filter({ has: page.getByLabel('Unarchive Exercise') })
.last();
await archivedRow.getByLabel('Unarchive Exercise').click();
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').uncheck();
await expect(page.getByText('Archive Me')).toBeVisible();
});
test('2.10 B. Exercise Library - Filter by Name', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
await request.post('/api/exercises', {
data: { name: 'FindThisOne', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
await request.post('/api/exercises', {
data: { name: 'IgnoreThatOne', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
await page.reload();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('FindThis');
await expect(page.getByText('FindThisOne')).toBeVisible();
await expect(page.getByText('IgnoreThatOne')).not.toBeVisible();
});
test('2.11 B. Exercise Library - Capitalization (Mobile)', async ({ page, createUniqueUser }) => {
await page.setViewportSize({ width: 390, height: 844 });
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
const nameInput = page.locator('div[role="dialog"]').getByLabel('Name');
await expect(nameInput).toHaveAttribute('autocapitalize', 'words');
});
test('2.12 B. Exercise Library - Unilateral', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Single Leg Squat');
await page.getByLabel(/Unilateral exercise/).check();
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await expect(page.locator('div[role="dialog"]')).not.toBeVisible();
await page.reload();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('Single Leg Squat');
await expect(page.getByText('Single Leg Squat')).toBeVisible();
// Verify Unilateral indicator might need text or specific element check, kept basic check:
await expect(page.getByText('Unilateral', { exact: false }).first()).toBeVisible();
});
test('2.13 B. Exercise Library - Special Types', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Plank Test');
// Assuming the button name is 'Static'
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Static' }).click();
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await expect(page.locator('div[role="dialog"]')).not.toBeVisible();
await page.reload();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('Plank Test');
await expect(page.getByText('Plank Test')).toBeVisible();
await expect(page.getByText('Static', { exact: false }).first()).toBeVisible();
});
test('2.15 B. Exercise Library - Edit to Unilateral & Verify Logger', async ({ page, createUniqueUser }) => {
const user = await loginAndSetup(page, createUniqueUser);
// 2. Create a standard exercise via Profile
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: 'Manage Exercises' }).click();
// Open create modal
await page.getByRole('button', { name: 'New Exercise' }).click();
const exName = `Test Uni ${Date.now()}`;
await page.locator('div[role="dialog"]').getByLabel('Name').fill(exName);
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
// Verify it exists in list
await expect(page.getByText(exName)).toBeVisible();
// 3. Edit exercise to be Unilateral
const row = page.locator('div.flex.justify-between').filter({ hasText: exName }).first();
await row.getByRole('button', { name: 'Edit Exercise' }).click();
// Check the Unilateral checkbox
await page.locator('div[role="dialog"]').getByLabel('Unilateral exercise').check();
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Save' }).click();
// Verify "Unilateral" tag appears in the list (if UI shows it)
await expect(row).toContainText('Unilateral');
// 4. Verify in Tracker
await page.getByRole('button', { name: 'Tracker', exact: true }).click();
await page.getByRole('button', { name: 'Quick Log', exact: true }).click();
// Select the exercise
await page.getByRole('textbox', { name: 'Select Exercise' }).fill(exName);
await page.getByRole('button', { name: exName }).click();
// Verify L/A/R buttons appear
await expect(page.getByTitle('Left')).toBeVisible();
await expect(page.getByTitle('Right')).toBeVisible();
await expect(page.getByTitle('Alternately')).toBeVisible();
});
});
});

View File

@@ -1,3 +1,4 @@
import { test, expect } from './fixtures';
import { randomUUID } from 'crypto';
@@ -28,52 +29,38 @@ test.describe('III. Workout Tracking', () => {
test('3.1 B. Idle State - Start Free Workout', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
// Ensure we are on Tracker tab (default)
await expect(page.getByText('Start Empty Workout').or(page.getByText('Free Workout'))).toBeVisible();
// Enter body weight
await page.locator('div').filter({ hasText: 'My Weight' }).locator('input[type="number"]').fill('75.5');
await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click();
// Verification
await expect(page.getByRole('button', { name: 'Finish' })).toBeVisible();
await expect(page.getByText('Select Exercise')).toBeVisible();
await expect(page.getByText('00:00')).toBeVisible(); // Timer started
// Check header for weight - might be in a specific format
await expect(page.getByText('00:00')).toBeVisible();
await expect(page.getByText('75.5')).toBeVisible();
});
test('3.2 B. Idle State - Start Quick Log', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Quick Log' }).click();
// Verification - Sporadic Logging view
await expect(page.getByText('Quick Log').first()).toBeVisible();
await expect(page.getByText('Select Exercise')).toBeVisible();
});
test('3.3 B. Idle State - Body Weight Defaults from Profile', async ({ page, createUniqueUser, request }) => {
const user = await createUniqueUser();
// Update profile weight first via API (PATCH /api/auth/profile)
const updateResp = await request.patch('/api/auth/profile', {
data: { weight: 75.5 },
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(updateResp.ok()).toBeTruthy();
// Login now
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle password change if needed
const heading = page.getByRole('heading', { name: /Change Password/i });
const dashboard = page.getByText('Start Empty Workout').or(page.getByText('Free Workout'));
await expect(heading.or(dashboard)).toBeVisible({ timeout: 10000 });
if (await heading.isVisible()) {
@@ -82,71 +69,49 @@ test.describe('III. Workout Tracking', () => {
await expect(dashboard).toBeVisible();
}
// Verify dashboard loaded
await expect(page.getByText('Start Empty Workout').or(page.getByText('Free Workout'))).toBeVisible();
// Verify default weight in Idle View
const weightInput = page.locator('div').filter({ hasText: 'My Weight' }).locator('input[type="number"]');
await expect(weightInput).toBeVisible();
await expect(weightInput).toHaveValue('75.5');
});
test('3.4 C. Active Session - Log Strength Set', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Seed exercise
const exName = 'Bench Press ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: exName, type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
// Start Free Workout
await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click();
// Select Exercise
await page.getByText('Select Exercise').click();
await page.getByText(exName).click();
// Log Set
await page.getByLabel('Weight (kg)').first().fill('80');
await page.getByLabel('Reps').first().fill('5');
await page.getByRole('button', { name: /Log Set/i }).click();
// Verification
await expect(page.getByText('80 kg x 5 reps')).toBeVisible(); // Assuming format
await expect(page.getByText('80 kg x 5 reps')).toBeVisible();
});
test('3.5 C. Active Session - Log Bodyweight Set', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Seed BW exercise
const exName = 'Pull-up ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: exName, type: 'BODYWEIGHT' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
// Start Free Workout
await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click();
// Select Exercise
await page.getByText('Select Exercise').click();
await page.getByText(exName).click();
// Verify Percentage Default - REMOVED (No default input visible)
// await expect(page.locator('input[value="100"]')).toBeVisible();
await page.getByLabel(/Add.? Weight/i).first().fill('10');
await page.getByLabel('Reps').first().fill('8');
await page.getByRole('button', { name: /Log Set/i }).click();
// Verification - Positive
await expect(page.getByText('+10 kg x 8 reps')).toBeVisible();
// Verification - Negative
await page.getByLabel(/Add.? Weight/i).first().fill('-30');
await page.getByLabel('Reps').first().fill('12');
await page.getByRole('button', { name: /Log Set/i }).click();
@@ -155,7 +120,6 @@ test.describe('III. Workout Tracking', () => {
test('3.6 C. Active Session - Log Cardio Set', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const exName = 'Running ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: exName, type: 'CARDIO' },
@@ -163,7 +127,6 @@ test.describe('III. Workout Tracking', () => {
});
await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click();
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
await page.getByText(exName).click();
@@ -171,13 +134,12 @@ test.describe('III. Workout Tracking', () => {
await page.getByLabel('Distance (m)').fill('1000');
await page.getByRole('button', { name: /Log Set/i }).click();
await expect(page.getByText('300s')).toBeVisible(); // or 5:00
await expect(page.getByText('300s')).toBeVisible();
await expect(page.getByText('1000m')).toBeVisible();
});
test('3.7 C. Active Session - Edit Logged Set', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const exName = 'Edit Test ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: exName, type: 'STRENGTH' },
@@ -188,20 +150,25 @@ test.describe('III. Workout Tracking', () => {
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
await page.getByText(exName).click();
// Log initial set
await page.getByLabel('Weight (kg)').first().fill('100');
await page.getByLabel('Reps').first().fill('10');
await page.getByRole('button', { name: /Log Set/i }).click();
await expect(page.getByText('100 kg x 10 reps')).toBeVisible();
// Edit
// Use filter to find the container, then find 'Edit' button inside it
const row = page.locator('div.shadow-elevation-1').filter({ hasText: '100 kg x 10 reps' }).first();
// The Edit button might be an icon button or text. Assuming it's the one with 'Edit' text or accessible name
await row.getByRole('button', { name: /Edit/i }).click();
await page.getByPlaceholder('Weight (kg)').fill('105');
await page.getByPlaceholder('Reps').fill('11'); // Reps might stay same, but let's be explicit
await page.getByRole('button', { name: /Save/i }).click();
// Wait for edit inputs to appear
// The modal should be visible
const editModal = page.locator('div[role="dialog"]');
await expect(editModal.getByRole('button', { name: 'Save', exact: true })).toBeVisible();
// EditSetModal doesn't use htmlFor, so we find the container with the label
await editModal.locator('div.bg-surface-container-high').filter({ hasText: 'Weight (kg)' }).locator('input').fill('105');
await editModal.locator('div.bg-surface-container-high').filter({ hasText: 'Reps' }).locator('input').fill('11');
await editModal.getByRole('button', { name: 'Save', exact: true }).click();
await expect(page.getByText('105 kg x 11 reps')).toBeVisible();
await expect(page.getByText('100 kg x 10 reps')).not.toBeVisible();
@@ -224,7 +191,6 @@ test.describe('III. Workout Tracking', () => {
await page.getByRole('button', { name: /Log Set/i }).click();
await expect(page.getByText('100 kg x 10 reps')).toBeVisible();
// Delete
const row = page.locator('div.shadow-elevation-1').filter({ hasText: '100 kg x 10 reps' }).first();
page.on('dialog', dialog => dialog.accept());
await row.getByRole('button', { name: /Delete|Remove/i }).click();
@@ -237,12 +203,9 @@ test.describe('III. Workout Tracking', () => {
await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click();
await page.getByRole('button', { name: 'Finish' }).click();
// Confirm?
await page.getByRole('button', { name: 'Confirm' }).click();
// Should be back at Idle
await expect(page.getByText(/Free Workout|Start Empty/i)).toBeVisible();
// Verify in History
await page.getByRole('button', { name: 'History' }).click();
await expect(page.getByText('No plan').first()).toBeVisible();
await expect(page.getByText('Sets: 0').first()).toBeVisible();
@@ -256,15 +219,12 @@ test.describe('III. Workout Tracking', () => {
await page.getByText(/Quit/i).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByText(/Free Workout|Start Empty/i)).toBeVisible();
});
test('3.11 C. Active Session - Plan Progression and Jump to Step', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Create 2 exercises
const ex1Id = randomUUID();
const ex2Id = randomUUID();
const ex3Id = randomUUID();
@@ -272,7 +232,6 @@ test.describe('III. Workout Tracking', () => {
await request.post('/api/exercises', { data: { id: ex2Id, name: 'Ex Two', type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
await request.post('/api/exercises', { data: { id: ex3Id, name: 'Ex Three', type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
// Create Plan
const planId = randomUUID();
await request.post('/api/plans', {
data: {
@@ -287,102 +246,85 @@ test.describe('III. Workout Tracking', () => {
headers: { 'Authorization': `Bearer ${user.token}` }
});
// Start Plan
await page.getByRole('button', { name: 'Plans' }).click();
await page.getByText('Progression Plan').click(); // Expand/Edit? Or directly Start depending on UI.
// Assuming there's a start button visible or in the card
const card = page.locator('div').filter({ hasText: 'Progression Plan' }).last();
// Assuming there isn't a direct start button on the card list without expansion,
// but often there is. If not, click card then start.
// Assuming direct start or via expand.
// Let's try to find Start button in the card
if (await card.getByRole('button', { name: 'Start' }).isVisible()) {
await card.getByRole('button', { name: 'Start' }).click();
} else {
// Click card to expand details then start?
// Or check if we are in Plans view.
}
// Fallback:
await page.locator('div').filter({ hasText: 'Progression Plan' }).getByRole('button', { name: 'Start' }).click();
// Should be on Ex One
// Prepare modal
const modal = page.locator('.fixed.inset-0.z-50');
if (await modal.isVisible()) {
await modal.getByRole('button', { name: 'Start' }).click();
}
await expect(page.getByText('Ex One')).toBeVisible();
// Log set for Ex One
await page.getByLabel('Weight (kg)').first().fill('50');
await page.getByLabel('Reps').first().fill('10');
await page.getByRole('button', { name: /Log Set/i }).click();
// Verify progression? Spec says "until it's considered complete". Usually 1 set might not auto-advance if multiple sets planned.
// But if no sets specified in plan, maybe 1 set is enough? Or manual advance.
// Spec says "Observe plan progression... automatically advances".
// If it doesn't auto-advance (e.g. need to click Next), we might need to click Next.
// Assuming auto-advance or manual next button.
// If it stays on Ex One, we might need to manually click 'Next Exercise' or similar.
// Let's assume we can click the progression bar.
// Check auto-advance or manual jump
// The user says: "Jump to step is available if unfold the plan and click a step"
// Log another set to trigger potentially auto-advance? Or just use jump.
// Let's test the Jump functionality as requested.
// Toggle plan list - looking for the text "Step 1 of 3" or similar to expand
await page.getByText(/Step \d+ of \d+/i).click();
// Click Ex Three in the list
await page.getByRole('button', { name: /Ex Three/i }).click();
await expect(page.getByText('Ex Three')).toBeVisible();
});
test('3.12 D. Sporadic Logging - Log Strength Sporadic Set', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Select Exercise
const exName = 'Quick Ex ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: exName, type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
// Go to Quick Log
await page.getByRole('button', { name: /Quick Log/i }).click();
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
await page.getByText(exName).click();
// Log Set
await page.getByLabel(/Weight/i).first().fill('60');
await page.getByLabel(/Reps/i).first().fill('8');
await page.getByRole('button', { name: /Log Set/i }).click();
// Verify Universal Format
await expect(page.getByText('60 kg x 8 reps')).toBeVisible();
});
test('3.13 D. Sporadic Logging - Exercise Search and Clear', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const benchPressName = 'Bench Press ' + randomUUID().slice(0, 4);
const benchDipName = 'Bench Dip ' + randomUUID().slice(0, 4);
const squatName = 'Squat ' + randomUUID().slice(0, 4);
// Seed 2 exercises
await request.post('/api/exercises', { data: { name: 'Bench Press', type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
await request.post('/api/exercises', { data: { name: 'Bench Dip', type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
await request.post('/api/exercises', { data: { name: 'Squat', type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
await request.post('/api/exercises', { data: { name: benchPressName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
await request.post('/api/exercises', { data: { name: benchDipName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
await request.post('/api/exercises', { data: { name: squatName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
await page.getByRole('button', { name: /Quick Log/i }).click();
// Type 'Ben'
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
await page.getByRole('textbox', { name: /Select Exercise/i }).fill('Ben');
await page.getByRole('textbox', { name: /Select Exercise/i }).fill(benchPressName.substring(0, 4)); // "Benc"
// Expect Bench Press and Bench Dip, but NOT Squat
await expect(page.getByText('Bench Press')).toBeVisible();
await expect(page.getByText('Bench Dip')).toBeVisible();
await expect(page.getByText('Squat')).not.toBeVisible();
// Click again -> should clear? spec says "The search field content is cleared on focus."
// Our implementing might differ (sometimes it selects all).
// Let's check if we can clear it manually if auto-clear isn't default,
// BUT the spec expects it. Let's assume the component does handle focus-clear or user manually clears.
// Actually, let's just verify we can clear and find Squat.
await expect(page.getByText(benchPressName)).toBeVisible();
await expect(page.getByText(benchDipName)).toBeVisible();
await expect(page.getByText(squatName)).not.toBeVisible();
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
await page.getByRole('textbox', { name: /Select Exercise/i }).fill(''); // specific action
await page.getByRole('textbox', { name: /Select Exercise/i }).fill('');
await expect(page.getByText('Squat')).toBeVisible();
await expect(page.getByText(squatName)).toBeVisible();
});
test('3.14 C. Active Session - Log Unilateral Set', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const exName = 'Uni Row ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: exName, type: 'STRENGTH', isUnilateral: true },
headers: { 'Authorization': `Bearer ${user.token}` }
@@ -390,81 +332,41 @@ test.describe('III. Workout Tracking', () => {
await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click();
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
await page.getByRole('textbox', { name: /Select Exercise/i }).fill(exName);
await page.getByText(exName).click();
// Expect L/R/A selector
await expect(page.getByRole('button', { name: 'L', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'R', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'A', exact: true })).toBeVisible();
// Helper to log a set
const logSet = async (side: 'L' | 'R' | 'A') => {
// Find the logger container (has 'Log Set' button)
const logger = page.locator('div').filter({ has: page.getByRole('button', { name: /Log Set|Saved/i }) }).last();
await expect(logger).toBeVisible();
// Select side
// Note: Side buttons are also inside the logger, but using global getByRole is okay if unique.
// Let's scope side as well for safety
await logger.getByRole('button', { name: side, exact: true }).click();
// Fill inputs scoped to logger
const weightInput = logger.getByLabel('Weight (kg)');
await weightInput.click();
await weightInput.fill('20');
// Reps - handle potential multiples if strict, but scoped should be unique
await logger.getByLabel('Reps').fill('10');
await logger.getByRole('button', { name: /Log Set|Saved/i }).click();
};
// Log Left (L)
await logSet('L');
// Verify Side and Metrics in list (Left)
await expect(page.getByText('Left', { exact: true })).toBeVisible();
await expect(page.getByText(/20.*10/)).toBeVisible();
// Log Right (R)
await logSet('R');
// Verify Right set
await expect(page.getByText('Right', { exact: true })).toBeVisible();
// Use last() or filter to verify the new set's metrics if needed, but 'Right' presence confirms logging
// We'll proceed to editing
// Edit the Right set to be Alternately
// Use a stable locator for the row (first item in history list)
// The class 'bg-surface-container' and 'shadow-elevation-1' identifies the row card.
// We use .first() because the list is reversed (newest first).
const rightSetRow = page.locator('.bg-surface-container.rounded-xl.shadow-elevation-1').first();
await rightSetRow.getByRole('button', { name: 'Edit' }).click();
// Verify we are in edit mode by finding the Save button
const saveButton = rightSetRow.getByRole('button', { name: /Save/i });
await expect(saveButton).toBeVisible();
// Change side to Alternately (A)
// Find 'A' button within the same row container which is now in edit mode
const aButton = rightSetRow.getByRole('button', { name: 'A', exact: true });
await expect(aButton).toBeVisible();
const editModal = page.locator('div[role="dialog"]');
const saveButton = editModal.getByRole('button', { name: /Save/i });
const aButton = editModal.getByRole('button', { name: 'A', exact: true });
await aButton.click();
// Save
await saveButton.click();
// Verify update
// Use regex for Alternately to handle case/whitespace
await expect(page.getByText(/Alternately/i)).toBeVisible();
});
test('3.15 C. Active Session - Log Special Type Set', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Static
const plankName = 'Plank ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: plankName, type: 'STATIC' },
@@ -480,11 +382,8 @@ test.describe('III. Workout Tracking', () => {
await expect(page.getByText('60s')).toBeVisible();
});
test('3.16 C. Active Session - Log Set with Default Reps', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const exName = 'Default Reps ' + randomUUID().slice(0, 4);
await request.post('/api/exercises', {
data: { name: exName, type: 'STRENGTH' },
@@ -496,27 +395,107 @@ test.describe('III. Workout Tracking', () => {
await page.getByText(exName).click();
await page.getByLabel('Weight (kg)').first().fill('50');
// Reps left empty intentionally
await page.getByRole('button', { name: /Log Set/i }).click();
// Verify it logged as 1 rep
await expect(page.getByText('50 kg x 1 reps')).toBeVisible();
});
test('3.17 B. Idle State - Days Off Training Logic', async ({ page, createUniqueUser }) => {
const user = await loginAndSetup(page, createUniqueUser);
// 1. New User: Should see "Do your very first workout today."
await expect(page.getByText('Do your very first workout today.')).toBeVisible();
// 2. Complete a workout
await page.getByRole('button', { name: /Free Workout|Start Empty/i }).click();
await page.getByRole('button', { name: 'Finish' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// 3. Should now see "Last workout: Today"
await expect(page.getByText('Last workout: Today')).toBeVisible();
});
test.describe('Rest Timer', () => {
// Merged from rest-timer.spec.ts
test('3.16 C. Rest Timer - Manual Edit & Validation', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Free Workout' }).click();
const fab = page.locator('.fixed.bottom-24.right-6');
await fab.click();
const editBtn = fab.locator('button[aria-label="Edit"]');
await editBtn.click();
const timerInput = page.getByRole('textbox').nth(1);
await timerInput.fill('90');
await expect(timerInput).toHaveValue('90');
await timerInput.fill('10:99');
await expect(timerInput).toHaveValue('10:59');
const saveBtn = fab.locator('button[aria-label="Save"]');
await saveBtn.click();
});
test('3.17 C. Rest Timer - Context & Persistence', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Free Workout' }).click();
const fab = page.locator('.fixed.bottom-24.right-6');
await fab.click();
const editBtn = fab.locator('button[aria-label="Edit"]');
await editBtn.click();
await page.getByRole('textbox').nth(1).fill('45');
await fab.locator('button[aria-label="Save"]').click();
await page.getByRole('button', { name: 'Finish' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await page.getByRole('button', { name: 'Quick Log' }).click();
const quickFab = page.locator('.fixed.bottom-24.right-6');
await quickFab.click();
await expect(page.locator('div').filter({ hasText: /0:45/ }).first()).toBeVisible();
});
test('3.18 C. Rest Timer - Plan Integration', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Plans' }).click();
await page.getByRole('button', { name: 'Create Plan' }).click();
await expect(page.getByRole('button', { name: 'Manually' })).toBeVisible();
await page.getByRole('button', { name: 'Manually' }).click();
await page.getByRole('textbox', { name: 'Name' }).fill('Timer Test Plan');
await page.getByRole('button', { name: 'Add Exercise' }).click();
await page.getByRole('button', { name: 'New Exercise' }).click();
// Scope to the top-most dialog for the new exercise form
const newExerciseModal = page.locator('div[role="dialog"]').last();
await newExerciseModal.getByLabel('Name').fill('Bench Press Test');
await newExerciseModal.getByRole('button', { name: 'Free Weights & Machines' }).click();
await newExerciseModal.getByRole('button', { name: 'Create' }).click();
// Rest input is on the plan step card now
await expect(page.getByTestId('plan-exercise-item').filter({ hasText: 'Bench Press Test' })).toBeVisible();
await page.locator('input[placeholder="Rest (s)"]').last().fill('30');
await page.getByRole('button', { name: 'Add Exercise' }).click();
await page.getByRole('button', { name: 'New Exercise' }).click();
await newExerciseModal.getByLabel('Name').fill('Squat Test');
await newExerciseModal.getByRole('button', { name: 'Free Weights & Machines' }).click();
await newExerciseModal.getByRole('button', { name: 'Create' }).click();
await expect(page.getByTestId('plan-exercise-item').filter({ hasText: 'Squat Test' })).toBeVisible();
await page.locator('input[placeholder="Rest (s)"]').last().fill('60');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Start' }).click();
const modal = page.locator('.fixed.inset-0.z-50');
if (await modal.isVisible()) {
await modal.getByRole('button', { name: 'Start' }).click();
}
const fab = page.locator('.fixed.bottom-24.right-6');
await fab.click();
await expect(page.locator('div').filter({ hasText: /0:30/ }).first()).toBeVisible();
await fab.locator('button[aria-label="Start"]').click();
await page.getByRole('button', { name: 'Log Set' }).click();
await expect(page.locator('div').filter({ hasText: /0:2[0-9]/ }).first()).toBeVisible();
const resetBtn = fab.locator('button[aria-label="Reset"]');
await resetBtn.click();
await expect(page.locator('div').filter({ hasText: /1:00/ }).first()).toBeVisible();
});
});
});

View File

@@ -1,3 +1,4 @@
import { test, expect } from './fixtures';
import { randomUUID } from 'crypto';
@@ -29,231 +30,266 @@ test.describe('IV. Data & Progress', () => {
test('4.1. A. Session History - View Past Sessions', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Subtask 2.1: Complete a workout session
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 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();
// Subtask 2.2: Log a sporadic set
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 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();
// 3. Navigate to History
await page.getByRole('button', { name: 'History' }).click();
// Verification
await expect(page.getByRole('heading', { name: 'History' })).toBeVisible();
// Check for Quick Log entry details
await expect(page.getByText(/50\s*kg\s*x\s*12\s*reps/).or(page.getByText(/x 12 reps/))).toBeVisible();
// Check for Workout Session entry (shows summary)
await expect(page.getByText('No plan').first()).toBeVisible();
await expect(page.getByText('Sets:').first()).toBeVisible();
// Check for Quick Log heading
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 request.post('/api/exercises', { data: { name: exName, type: 'STRENGTH' }, headers: { 'Authorization': `Bearer ${user.token}` } });
// Complete session
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 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();
// Navigate to History
await page.getByRole('button', { name: 'History' }).click();
// Click on a workout session entry
await page.getByText('No plan').first().click();
// Verification
await expect(page.getByRole('heading', { name: /Edit|Session Details/ })).toBeVisible();
// Check details
await expect(page.getByText('Start')).toBeVisible();
await expect(page.getByText('End')).toBeVisible();
await expect(page.getByText('Weight (kg)').first()).toBeVisible();
// Verify set details
await expect(page.getByRole('heading', { name: /Sets/ })).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();
// Open details
await page.getByText('No plan').first().click();
// Modify Body Weight (first spinbutton usually)
await page.getByRole('spinbutton').first().fill('75.5');
// Save
await page.getByRole('button', { name: 'Save' }).click();
// Verify
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/i }).click();
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();
// Open details
await page.getByText('No plan').first().click();
// Modify weight from 50 to 55
// Be specific with locator if possible, or use first matching input
// 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();
// Save
await page.getByRole('button', { name: 'Save' }).click();
// Verify
await page.getByText('No plan').first().click();
await expect(page.locator('input[value="55"]')).toBeVisible();
await expect(page.getByText('55 kg x 10 reps')).toBeVisible();
});
test('4.5. A. Session History - Delete Past Session', async ({ page, createUniqueUser, request }) => {
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();
await expect(page.getByText('No plan').first()).toBeVisible();
// Delete (2nd button usually)
await page.getByRole('main').getByRole('button').nth(1).click();
// Confirm
await expect(page.getByRole('heading', { name: 'Delete workout?' })).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
// Verify empty
// 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.6. A. Session History - Edit Sporadic Set', async ({ page, createUniqueUser, request }) => {
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 expect(page.getByRole('heading', { name: 'Quick Log' })).toBeVisible();
await page.getByRole('button', { name: /Edit/i }).first().click(); // Edit
// Edit (1st button for sporadic row)
await page.getByRole('main').getByRole('button').nth(0).click();
await expect(page.getByRole('heading', { name: 'Edit' })).toBeVisible();
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.7. A. Session History - Delete Sporadic Set', async ({ page, createUniqueUser, request }) => {
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}` } });
@@ -263,21 +299,43 @@ test.describe('IV. Data & Progress', () => {
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();
// Delete (2nd button for sporadic row, or last button in main if only one row)
// With only one row, buttons are Edit, Delete. Delete is 2nd.
await page.getByRole('main').getByRole('button').last().click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).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.8. B. Performance Statistics - View Volume Chart', async ({ page, createUniqueUser, request }) => {
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);
@@ -290,9 +348,7 @@ test.describe('IV. Data & Progress', () => {
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();
@@ -303,6 +359,7 @@ test.describe('IV. Data & Progress', () => {
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();
@@ -310,7 +367,7 @@ test.describe('IV. Data & Progress', () => {
await expect(page.getByText('Work Volume')).toBeVisible();
});
test('4.9. B. Performance Statistics - View Set Count Chart', async ({ page, createUniqueUser, request }) => {
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);
@@ -323,6 +380,7 @@ test.describe('IV. Data & Progress', () => {
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();
@@ -333,6 +391,7 @@ test.describe('IV. Data & Progress', () => {
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();
@@ -340,34 +399,33 @@ test.describe('IV. Data & Progress', () => {
await expect(page.getByText('Number of Sets')).toBeVisible();
});
test('4.10. B. Performance Statistics - View Body Weight Chart', async ({ page, createUniqueUser, request }) => {
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}` } });
// Complete 2 sessions (to unlock stats page - assuming constraint)
// 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
// 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('60');
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();
// Log body weight history via API
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const dateStr = yesterday.toISOString().split('T')[0];
@@ -375,15 +433,11 @@ test.describe('IV. Data & Progress', () => {
await page.evaluate(async ({ token, dateStr }) => {
await fetch('/api/weight', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ weight: 70, dateStr })
});
}, { token: user.token, dateStr });
// Log today's weight via UI
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Weight Tracker' }).click();
await page.getByPlaceholder('Enter weight...').fill('72');

View File

@@ -1,52 +1,44 @@
import { test, expect } from './fixtures';
import { exec as cp_exec } from 'child_process';
import { request as playwrightRequest } from '@playwright/test';
import { promisify } from 'util';
import { exec as cp_exec } from 'child_process';
const exec = promisify(cp_exec);
test.describe('V. User & System Management', () => {
test('5.1. A. User Profile - Update Personal Information', async ({ page, createUniqueUser }) => {
// Seed: Log in as a regular user
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();
// Handle potential first-time login
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout if it proceeds
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'Profile' section.
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
// 3. Modify 'Weight', 'Height', 'Birth Date', and 'Gender'.
await page.getByTestId('profile-weight-input').fill('75');
await page.getByTestId('profile-height-input').fill('180');
await page.getByTestId('profile-birth-date').fill('1990-01-01');
await page.getByTestId('profile-gender').selectOption('FEMALE');
// 4. Click 'Save Profile'.
await page.getByRole('button', { name: 'Save Profile' }).click();
await expect(page.getByText('Profile saved successfully')).toBeVisible();
// Verify persistence
await page.reload();
// After reload, we might be on dashboard or profile depending on app routing, but let's ensure we go to profile
if (!await page.getByRole('heading', { name: 'Profile' }).isVisible()) {
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
}
// Verify values
await expect(page.getByTestId('profile-weight-input')).toHaveValue('75');
await expect(page.getByTestId('profile-height-input')).toHaveValue('180');
await expect(page.getByTestId('profile-birth-date')).toHaveValue('1990-01-01');
@@ -54,41 +46,30 @@ test.describe('V. User & System Management', () => {
});
test('5.2. A. User Profile - Change Password', async ({ page, createUniqueUser }) => {
// Seed: Log in as a regular user
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();
// Handle potential first-time login
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'Profile' section.
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
// 3. Enter a new password (min 4 characters) in the 'Change Password' field.
const newPassword = 'NewStrongPass!';
await page.getByRole('textbox', { name: 'New Password' }).fill(newPassword);
// 4. Click 'OK'.
await page.getByRole('button', { name: 'OK' }).click();
await expect(page.getByText('Password changed')).toBeVisible();
// Verify: The user can log in with the new password.
// Logout first
await page.getByRole('button', { name: 'Logout' }).click();
// Login with new password
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
await page.getByRole('textbox', { name: 'Password' }).fill(newPassword);
await page.getByRole('button', { name: 'Login' }).click();
@@ -97,7 +78,6 @@ test.describe('V. User & System Management', () => {
});
test('5.3. A. User Profile - Change Password (Too Short)', async ({ page, createUniqueUser }) => {
// Seed
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
@@ -112,21 +92,13 @@ test.describe('V. User & System Management', () => {
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to Profile
await page.getByRole('button', { name: 'Profile' }).click();
// 3. Enter short password
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('textbox', { name: 'New Password' }).fill('123');
// 4. Click OK
await page.getByRole('button', { name: 'OK' }).click();
// Expect Error
await expect(page.getByText('Password too short')).toBeVisible();
});
test('5.4. A. User Profile - Dedicated Daily Weight Logging', async ({ page, createUniqueUser }) => {
// Seed
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
@@ -141,77 +113,51 @@ test.describe('V. User & System Management', () => {
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to Profile
await page.getByRole('button', { name: 'Profile' }).click();
// 3. Expand Weight Tracker
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: 'Weight Tracker' }).click();
// 4. Enter weight
const weight = '72.3';
await page.getByPlaceholder('Enter weight...').fill(weight);
// 5. Click Log
await page.getByRole('button', { name: 'Log', exact: true }).click();
// Expect success message
await expect(page.getByText('Weight logged successfully')).toBeVisible();
// Expect record in history
await expect(page.getByText(`${weight} kg`)).toBeVisible();
// Check if profile weight updated
await expect(page.getByRole('spinbutton').first()).toHaveValue(weight);
});
test('5.5. A. User Profile - Language Preference Change', async ({ page, createUniqueUser }) => {
// 1. Log in as a regular user.
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();
// Handle First Time Password Change if it appears
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'Profile' section.
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
// 3. Select a different language (e.g., 'Русский') from the language dropdown.
await page.getByRole('combobox').nth(1).selectOption(['ru']); // Value is 'ru'
// 4. Click 'Save Profile'.
await page.getByRole('combobox').nth(1).selectOption(['ru']);
await page.getByRole('button', { name: /Сохранить профиль|Save Profile/ }).click();
// Expected Results: The UI language immediately switches to the selected language.
await expect(page.getByRole('heading', { name: 'Профиль', exact: true })).toBeVisible();
await expect(page.getByText(/Profile saved|Профиль успешно/)).toBeVisible(); // Wait for persistence
await expect(page.getByText(/Profile saved|Профиль успешно/)).toBeVisible();
await expect(page.getByRole('button', { name: 'Сохранить профиль' })).toBeVisible();
// Expected Results: The preference persists across sessions.
await page.reload();
// Check if we are still logged in or need to login
if (await page.getByLabel('Email').isVisible()) {
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password || 'StrongNewPass123!');
await page.getByRole('button', { name: 'Login' }).click();
}
// Verify language is still Russian
await page.getByRole('button', { name: /Профиль|Profile/ }).click();
await expect(page.getByRole('heading', { name: 'Профиль', exact: true })).toBeVisible();
});
@@ -232,7 +178,7 @@ test.describe('V. User & System Management', () => {
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Are you sure?')).toBeVisible();
@@ -248,13 +194,10 @@ test.describe('V. User & System Management', () => {
await expect(page.getByText(/Invalid credentials|User not found/i)).toBeVisible();
});
// --- Admin Panel Tests ---
test('5.7. B. Admin Panel - View User List', async ({ page, createAdminUser, request }) => {
test.setTimeout(120000); // Extend timeout for multiple user creation
test.setTimeout(120000);
const adminUser = await createAdminUser();
// Create 25 users to populate the list using Promise.all for parallelism
const createdEmails: string[] = [];
const creationPromises = [];
@@ -288,14 +231,11 @@ test.describe('V. User & System Management', () => {
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
await page.getByRole('button', { name: 'Profile' }).click();
// Expand Users List (Admin Area is a header)
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await page.getByRole('button', { name: /Users List|User List/i }).click();
await expect(page.getByText(/Users List/i)).toBeVisible();
// Verify all created users are visible in the list
for (const email of createdEmails) {
await expect(page.getByText(email)).toBeVisible();
}
@@ -317,7 +257,7 @@ test.describe('V. User & System Management', () => {
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
const uniqueId = Math.random().toString(36).substring(7);
const newUserEmail = `new.user.${uniqueId}@example.com`;
@@ -342,11 +282,8 @@ test.describe('V. User & System Management', () => {
});
test('5.9. B. Admin Panel - Block/Unblock User', async ({ page, createAdminUser, createUniqueUser }) => {
const adminUser = await createAdminUser();
// 1. Login as Admin
await page.goto('/');
await page.getByLabel('Email').fill(adminUser.email);
await page.getByLabel('Password').fill(adminUser.password);
@@ -360,68 +297,42 @@ test.describe('V. User & System Management', () => {
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
console.log('Logged in as Admin');
// 2. Create a Regular User (via API)
const regularUser = await createUniqueUser();
console.log('Regular user created:', regularUser.email);
// 3. Navigate to Admin Panel -> User List
await page.getByRole('button', { name: 'Profile' }).filter({ visible: true }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).filter({ visible: true }).click();
// Ensure list is open and valid
const userListButton = page.getByRole('button', { name: /Users List/i });
// Check expanded state and Open if currently closed
const isExpanded = await userListButton.getAttribute('aria-expanded');
if (isExpanded !== 'true') {
await userListButton.click();
}
await expect(userListButton).toHaveAttribute('aria-expanded', 'true');
console.log('User list is open');
// Always Refresh to ensure latest users are fetched
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/auth/users')),
page.getByTitle('Refresh List').click()
]);
// Ensure list remained open or re-open it
if (await userListButton.getAttribute('aria-expanded') !== 'true') {
console.log('List closed after refresh, re-opening...');
await userListButton.click();
}
// Verify user row exists
// Fallback to CSS selector if data-testid is missing due to build issues
const listContainer = page.locator('div.space-y-4.mt-4');
await expect(listContainer).toBeVisible();
const userRow = listContainer.locator('.bg-surface-container-high').filter({ hasText: regularUser.email }).first();
await expect(userRow).toBeVisible();
// 4. Block the User
// Use exact name matching or title since we added aria-label
const blockButton = userRow.getByRole('button', { name: 'Block', exact: true });
if (await blockButton.count() === 0) {
console.log('Block button NOT found!');
// fallback to find any button to see what is there
const buttons = await userRow.getByRole('button').all();
console.log('Buttons found in row:', buttons.length);
}
await expect(blockButton).toBeVisible();
await blockButton.click();
// Handle Block Confirmation Modal
await expect(page.getByText('Block User?').or(page.getByText('Заблокировать?'))).toBeVisible();
await page.getByRole('button', { name: /Confirm|Подтвердить/i }).click();
await expect(userRow.getByText(/Blocked|Block/i)).toBeVisible();
// 5. Verify Blocked User Cannot Login
// Logout Admin
const logoutButton = page.getByRole('button', { name: /Logout/i });
if (await logoutButton.isVisible()) {
await logoutButton.click();
@@ -430,55 +341,40 @@ test.describe('V. User & System Management', () => {
}
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
// Attempt Login as Blocked User
await page.getByLabel('Email').fill(regularUser.email);
await page.getByLabel('Password').fill(regularUser.password);
await page.getByRole('button', { name: 'Login' }).click();
// Assert Error Message
await expect(page.getByText(/Account is blocked/i)).toBeVisible();
// 6. Unblock the User
// Reload to clear any error states/toasts from previous attempt
await page.reload();
// Login as Admin again
await page.getByLabel('Email').fill(adminUser.email);
// Force the new password since we know step 1 changed it
await page.getByLabel('Password').fill('StrongAdminNewPass123!');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Free Workout')).toBeVisible();
console.log('Admin logged back in');
await page.waitForTimeout(1000);
await page.getByRole('button', { name: 'Profile' }).filter({ visible: true }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).filter({ visible: true }).click();
// Open list again
await userListButton.click();
await page.getByTitle('Refresh List').click();
// Unblock
const userRowAfter = listContainer.locator('.bg-surface-container-high').filter({ hasText: regularUser.email }).first();
await expect(userRowAfter).toBeVisible();
await userRowAfter.getByRole('button', { name: 'Unblock', exact: true }).click();
// Handle Unblock Modal
await expect(page.getByText('Unblock User?').or(page.getByText('Разблокировать?'))).toBeVisible();
await page.getByRole('button', { name: /Confirm|Подтвердить/i }).click();
// Wait for UI to update (block icon/text should disappear or change style)
// Ideally we check API response or UI change. Assuming "Blocked" text goes away or button changes.
// The original code checked for not.toBeVisible of blocked text, let's stick to that or button state
await expect(userRowAfter.getByText(/Blocked/i)).not.toBeVisible();
// 7. Verify Unblocked User Can Login
await page.getByRole('button', { name: 'Logout' }).click();
await page.getByLabel('Email').fill(regularUser.email);
await page.getByLabel('Password').fill(regularUser.password);
await page.getByRole('button', { name: 'Login' }).click();
// Check for Change Password (first login) or direct Dashboard
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongUserNewPass123!');
@@ -504,9 +400,8 @@ test.describe('V. User & System Management', () => {
const regularUser = await createUniqueUser();
const newPassword = 'NewStrongUserPass!';
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
// Ensure list is open and valid (Reusing logic from 5.9)
const userListButton = page.getByRole('button', { name: /Users List/i });
const isExpanded = await userListButton.getAttribute('aria-expanded');
if (isExpanded !== 'true') {
@@ -514,13 +409,11 @@ test.describe('V. User & System Management', () => {
}
await expect(userListButton).toHaveAttribute('aria-expanded', 'true');
// Always Refresh to ensure latest users are fetched
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/auth/users')),
page.getByTitle('Refresh List').click()
]);
// Ensure list remained open
if (await userListButton.getAttribute('aria-expanded') !== 'true') {
await userListButton.click();
}
@@ -534,16 +427,10 @@ test.describe('V. User & System Management', () => {
await userRow.getByRole('textbox').fill(newPassword);
page.on('dialog', async dialog => {
console.log(`Dialog message: ${dialog.message()}`);
await dialog.accept();
});
await userRow.getByRole('button', { name: /Reset Pass/i }).click();
// Wait to ensure the operation completed (the dialog is the signal, but we might need a small buffer or check effect)
// Since dialog is handled immediately by listener, we might race.
// Better pattern: wait for the button to be enabled again or some UI feedback.
// But since we use window.alert, expecting the dialog content is tricky in Playwright if not careful.
// Let's add a small pause to allow backend to process before logout.
await page.waitForTimeout(1000);
await page.getByRole('button', { name: 'Logout' }).click();
@@ -551,7 +438,6 @@ test.describe('V. User & System Management', () => {
await page.getByLabel('Password').fill(newPassword);
await page.getByRole('button', { name: 'Login' }).click();
// After reset, isFirstLogin is true, so we expect Change Password screen
await expect(page.getByRole('heading', { name: /Change Password/i })).toBeVisible({ timeout: 10000 });
await page.getByLabel('New Password').fill('BrandNewUserPass1!');
await page.getByRole('button', { name: /Save|Change/i }).click();
@@ -575,9 +461,8 @@ test.describe('V. User & System Management', () => {
const userToDelete = await createUniqueUser();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
// Ensure list is open and valid (Reusing logic from 5.9)
const userListButton = page.getByRole('button', { name: /Users List/i });
const isExpanded = await userListButton.getAttribute('aria-expanded');
if (isExpanded !== 'true') {
@@ -585,13 +470,11 @@ test.describe('V. User & System Management', () => {
}
await expect(userListButton).toHaveAttribute('aria-expanded', 'true');
// Always Refresh to ensure latest users are fetched
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/auth/users')),
page.getByTitle('Refresh List').click()
]);
// Ensure list remained open
if (await userListButton.getAttribute('aria-expanded') !== 'true') {
await userListButton.click();
}
@@ -604,10 +487,45 @@ test.describe('V. User & System Management', () => {
await userRow.getByRole('button', { name: /Delete/i }).click();
// Handle Delete Confirmation Modal
await expect(page.getByText('Delete User?').or(page.getByText('Удалить пользователя?'))).toBeVisible();
await page.getByRole('button', { name: /Confirm|Подтвердить/i }).click();
await page.getByRole('button', { name: /Confirm|Подтвердить/i }).click({ timeout: 5000 });
await expect(page.getByText(userToDelete.email)).not.toBeVisible();
await expect(listContainer.getByText(userToDelete.email)).not.toBeVisible();
});
// Merged from default-exercises.spec.ts
test('5.12 Default Exercises Creation & Properties', async ({ createUniqueUser }) => {
const user = await createUniqueUser();
const apiContext = await playwrightRequest.newContext({
baseURL: 'http://127.0.0.1:3001',
extraHTTPHeaders: {
'Authorization': `Bearer ${user.token}`
}
});
const exercisesRes = await apiContext.get('/api/exercises');
await expect(exercisesRes).toBeOK();
const responseJson = await exercisesRes.json();
const exercises = responseJson.data;
const expectedNames = ['Bench Press', 'Squat', 'Deadlift', 'Push-Ups', 'Pull-Ups', 'Running', 'Plank', 'Handstand', 'Sprint', 'Bulgarian Split-Squats'];
for (const name of expectedNames) {
const found = exercises.find((e: any) => e.name === name);
expect(found, `Exercise ${name} should exist`).toBeDefined();
}
const dumbbellCurl = exercises.find((e: any) => e.name === 'Dumbbell Curl');
expect(dumbbellCurl.isUnilateral).toBe(true);
expect(dumbbellCurl.type).toBe('STRENGTH');
const handstand = exercises.find((e: any) => e.name === 'Handstand');
expect(handstand.type).toBe('BODYWEIGHT');
expect(handstand.bodyWeightPercentage).toBe(1.0);
const pushUps = exercises.find((e: any) => e.name === 'Push-Ups');
expect(pushUps.bodyWeightPercentage).toBe(0.65);
});
});

View File

@@ -1,17 +1,15 @@
// spec: specs/gymflow-test-plan.md
import { test, expect } from './fixtures';
test.describe('VI. User Interface & Experience', () => {
test('6.1. A. Adaptive GUI - Mobile Navigation (Width < 768px)', async ({ page, createUniqueUser }) => {
// Note: Use 6.1 numbering as per plan section 6.
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();
// Handle First Time Password Change
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
@@ -21,21 +19,13 @@ test.describe('VI. User Interface & Experience', () => {
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Resize the browser window to a mobile width (e.g., 375px).
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Resize the browser window to a mobile width (e.g., 375px).
await page.setViewportSize({ width: 375, height: 667 });
// 3. Verify the bottom navigation bar is visible and functional.
await expect(page.getByRole('navigation', { name: /Bottom|Mobile/i })).toBeVisible();
// Or check for specific mobile nav items if role 'navigation' isn't named.
// Assuming 'Tracker', 'Plans', etc. are visible.
await expect(page.getByRole('button', { name: /Tracker/i })).toBeVisible();
// 4. Verify the desktop navigation rail is hidden.
await expect(page.getByRole('navigation', { name: /Desktop|Side/i })).toBeHidden();
});
@@ -55,19 +45,15 @@ test.describe('VI. User Interface & Experience', () => {
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 1. Resize the browser window to a desktop width (e.g., 1280px).
await page.setViewportSize({ width: 1280, height: 800 });
// 2. Verify the vertical navigation rail is visible and functional.
await expect(page.getByRole('navigation', { name: /Desktop|Side/i })).toBeVisible();
await expect(page.getByRole('button', { name: 'Tracker' })).toBeVisible(); // Check an item
await expect(page.getByRole('button', { name: 'Tracker' })).toBeVisible();
// 3. Verify the mobile bottom navigation bar is hidden.
await expect(page.getByRole('navigation', { name: /Bottom|Mobile/i })).toBeHidden();
});
test('6.3. A. Adaptive GUI - Responsive Charts in Stats', async ({ page, createUniqueUser }) => {
// Using content from adaptive-gui-responsive-charts-in-stats.spec.ts
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
@@ -89,34 +75,25 @@ test.describe('VI. User Interface & Experience', () => {
expect(scrollWidth).toBeLessThanOrEqual(clientWidth);
};
// 1. Navigate to the 'Stats' section.
await page.getByRole('button', { name: 'Stats' }).click();
// Define a range of widths to test responsiveness
const widths = [1280, 1024, 768, 600, 480, 375];
const heights = [800, 768, 667];
for (const width of widths) {
for (const height of heights) {
await page.setViewportSize({ width, height });
// Give time for resize observation/rendering
await page.waitForTimeout(200);
// Check for no overflow
await checkNoHorizontalScroll();
// Check if "Not enough data" is shown
const noData = await page.getByText(/Not enough data/i).isVisible();
if (noData) {
await expect(page.getByText(/Not enough data/i)).toBeVisible();
// Skip chart assertions if no data
} else {
// Verify chart containers are visible
await expect(page.getByRole('heading', { name: /Total Volume/i }).or(page.getByText('Total Volume'))).toBeVisible();
await expect(page.getByRole('heading', { name: /Set Count/i }).or(page.getByText('Set Count'))).toBeVisible();
await expect(page.getByRole('heading', { name: /Body Weight/i }).or(page.getByText('Body Weight'))).toBeVisible();
// Check for presence of SVG or Canvas elements typically used for charts
await expect(page.locator('svg').first()).toBeVisible();
}
}
@@ -130,35 +107,29 @@ test.describe('VI. User Interface & Experience', () => {
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle First Time Password Change if it appears
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// Helper to check for horizontal scrollbar (indicates overflow)
const checkNoHorizontalScroll = async () => {
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
expect(scrollWidth).toBeLessThanOrEqual(clientWidth);
};
// Define a range of widths to test responsiveness
const widths = [1280, 1024, 768, 600, 480, 375];
const heights = [800, 768, 667]; // Corresponding heights
const heights = [800, 768, 667];
for (const width of widths) {
for (const height of heights) {
await page.setViewportSize({ width, height });
await checkNoHorizontalScroll();
// 1. Navigate through various sections and check responsiveness
await page.getByRole('button', { name: 'Plans' }).click();
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'Profile' }).click();
@@ -169,7 +140,7 @@ test.describe('VI. User Interface & Experience', () => {
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'AI Coach' }).click();
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'Tracker' }).click(); // Go back to default view
await page.getByRole('button', { name: 'Tracker' }).click();
await checkNoHorizontalScroll();
}
}

100
tests/07_ai_coach.spec.ts Normal file
View File

@@ -0,0 +1,100 @@
import { test, expect } from './fixtures';
test.describe('VII. AI Coach Features', () => {
async function handleFirstLogin(page: any) {
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 page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
await expect(dashboard).toBeVisible();
}
} catch (e) {
if (await page.getByText('Free Workout').isVisible()) return;
}
}
test('7.1 AI Coach - Basic Conversation & Markdown', async ({ page, createUniqueUser }) => {
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();
await handleFirstLogin(page);
await page.getByRole('button', { name: 'AI Coach' }).click();
const input = page.getByPlaceholder(/Ask your AI coach/i).or(page.getByRole('textbox', { name: 'Ask about workouts...' }));
await input.fill('How to do a pushup?');
await page.getByRole('button').filter({ hasText: /^$/ }).last().click();
await expect(page.locator('.prose').first()).toBeVisible({ timeout: 30000 });
});
test('7.2, 7.3, 7.4 AI Coach - Bookmark Flow', async ({ page, createUniqueUser }) => {
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();
await handleFirstLogin(page);
await page.getByRole('button', { name: 'AI Coach' }).click();
const input = page.getByPlaceholder(/Ask your AI coach/i).or(page.getByRole('textbox', { name: 'Ask about workouts...' }));
await input.fill('Tell me a short fitness tip');
await page.getByRole('button').filter({ hasText: /^$/ }).last().click();
const responseBubble = page.locator('.prose').first();
await expect(responseBubble).toBeVisible({ timeout: 30000 });
const bookmarkBtn = page.getByRole('button', { name: /Bookmark/i }).first();
if (await bookmarkBtn.isVisible()) {
await bookmarkBtn.click();
} else {
// Try to hover or find icon
const iconBtn = page.getByRole('button').filter({ has: page.locator('svg.lucide-bookmark') }).last();
await iconBtn.click();
}
await expect(page.getByText(/Message saved/i)).toBeVisible();
await page.getByRole('button', { name: /Saved/i }).click();
await expect(page.getByText('Saved Messages')).toBeVisible();
await expect(page.getByText(/fitness tip/i)).toBeVisible({ timeout: 3000 });
const dialog = page.getByRole('dialog').filter({ hasText: 'Saved Messages' });
const deleteBtn = dialog.getByRole('button', { name: /Remove bookmark/i }).first();
await deleteBtn.click({ force: true });
await expect(deleteBtn).not.toBeVisible();
});
// Merged from ai-coach-send-message.spec.ts
test('7.5 AI Coach - Send a Message (Verification)', async ({ page, createUniqueUser }) => {
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();
await handleFirstLogin(page);
await page.getByRole('button', { name: 'AI Coach' }).click();
const message = "What's a good workout for chest?";
await page.getByRole('textbox', { name: 'Ask about workouts...' }).fill(message);
await page.getByRole('button').filter({ hasText: /^$/ }).last().click();
await expect(page.getByText(message)).toBeVisible();
await expect(page.getByText(/chest/i).nth(1)).toBeVisible({ timeout: 30000 });
});
});

View File

@@ -1,51 +0,0 @@
// spec: specs/gymflow-test-plan.md
// seed: tests/seed.spec.ts
import { test, expect } from './fixtures';
test.describe('User & System Management', () => {
test('AI Coach - Send a Message', async ({ page, createUniqueUser }) => {
// 1. Log in as a regular user.
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();
// Handle First Time Password Change if it appears
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'AI Coach' section.
await page.getByRole('button', { name: 'AI Coach' }).click();
// 3. Type a message into the input field (e.g., 'What's a good workout for chest?').
const message = "What's a good workout for chest?";
await page.getByRole('textbox', { name: 'Ask about workouts...' }).fill(message);
// 4. Click 'Send' button.
// Using filter to find the button with no text (icon only) which is the send button in the chat interface
await page.getByRole('button').filter({ hasText: /^$/ }).click();
// Expected Results: User's message appears in the chat.
await expect(page.getByText(message)).toBeVisible();
// Expected Results: AI Coach responds with relevant advice.
// We expect a response to appear. Since AI response takes time, we wait for it.
// We can check for a common response starter or just that another message bubble appears.
// Assuming the response is long, we can check for a part of it or just non-empty text that is NOT the user message.
// Or check if the "thinking" state goes away if implemented.
// Here we'll just wait for any text that contains "chest" or "workout" that isn't the input prompt.
// But better to check for element structure if possible.
// Based on manual execution, we saw "That's a great goal!"
await expect(page.getByText(/chest/i).nth(1)).toBeVisible(); // Just ensuring related content appeared
});
});

View File

@@ -1,92 +0,0 @@
import { test, expect } from './fixtures';
test.describe('VII. AI Coach Features', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
// Helper to handle first login if needed (copied from core-auth)
async function handleFirstLogin(page: any) {
try {
const heading = page.getByRole('heading', { name: /Change Password/i });
const dashboard = page.getByText('Free Workout');
await expect(heading).toBeVisible({ timeout: 5000 });
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
await expect(dashboard).toBeVisible();
} catch (e) {
if (await page.getByText('Free Workout').isVisible()) return;
}
}
test('7.1 AI Coach - Basic Conversation & Markdown', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
// Login
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
await handleFirstLogin(page);
// Navigate to AI Coach
await page.getByText('AI Coach').click();
// Type message
const input = page.getByPlaceholder(/Ask your AI coach/i);
await input.fill('How to do a pushup?');
await page.getByRole('button', { name: /Send/i }).click();
// Verify response (Mocked or Real - expecting Real from previous context)
// Since we can't easily mock backend without more setup, we wait for *any* response
await expect(page.locator('.prose')).toBeVisible({ timeout: 15000 });
// Check for markdown rendering (e.g., strong tags or list items if AI returns them)
// This is a bit flaky with real AI, but checking for visibility is a good start.
});
test('7.2, 7.3, 7.4 AI Coach - Bookmark Flow', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
// Login
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
await handleFirstLogin(page);
await page.getByText('AI Coach').click();
// Send message
await page.getByPlaceholder(/Ask your AI coach/i).fill('Tell me a short fitness tip');
await page.getByRole('button', { name: /Send/i }).click();
// Wait for response bubble
const responseBubble = page.locator('.prose').first();
await expect(responseBubble).toBeVisible({ timeout: 15000 });
// 7.2 Bookmark
// Find bookmark button within the message container.
// Assuming the layout puts actions near the message.
// We look for the Bookmark icon button.
const bookmarkBtn = page.getByRole('button', { name: /Bookmark/i }).first();
await bookmarkBtn.click();
// Expect success snackbar
await expect(page.getByText(/Message saved/i)).toBeVisible();
// 7.3 View Saved
await page.getByRole('button', { name: /Saved/i }).click(); // The TopBar action
await expect(page.getByText('Saved Messages')).toBeVisible(); // Sheet title
// Verify content is there
await expect(page.getByText(/fitness tip/i)).toBeVisible(); // Part of our prompt/response context usually
// 7.4 Delete Bookmark
const deleteBtn = page.getByRole('button', { name: /Delete/i }).first();
await deleteBtn.click();
// Verify removal
await expect(deleteBtn).not.toBeVisible();
});
});

View File

@@ -1,106 +0,0 @@
import { test, expect } from './fixtures';
import { randomUUID } from 'crypto';
test.describe('AI Plan Creation', () => {
test('2.14 A. Workout Plans - Create Plan with AI (Parametrized)', async ({ page, createUniqueUser }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Mock the AI endpoint
await page.route('**/api/ai/chat', async route => {
const plan = {
name: 'AI Advanced Plan',
description: 'Generated High Intensity Plan',
exercises: [
{ name: 'Mock Push-ups', isWeighted: false, restTimeSeconds: 60, type: 'BODYWEIGHT', unilateral: false },
{ name: 'Mock Weighted Pull-ups', isWeighted: true, restTimeSeconds: 90, type: 'BODYWEIGHT', unilateral: false }
]
};
// The service expects { response: "string_or_json" }
await route.fulfill({
json: {
response: JSON.stringify(plan)
}
});
});
// Navigate to Plans
await page.getByRole('button', { name: 'Plans' }).first().click();
// Click FAB to open menu
const fab = page.getByLabel('Create Plan').or(page.getByRole('button', { name: '+' }));
await fab.click();
// Click "With AI"
await page.getByRole('button', { name: 'With AI' }).click();
// Verify Defaults
await expect(page.getByText('Create Plan with AI')).toBeVisible();
// Equipment default: No equipment
// Checking visual state might be hard if it's custom styled, but we can check the selected button style or text
const eqSection = page.locator('div').filter({ hasText: 'Equipment' }).last();
// Assuming "No equipment" is selected. We can check class or aria-pressed if available, or just proceed to change it.
// Level default: Intermediate
const levelSection = page.locator('div').filter({ hasText: 'Level' }).last();
// Just verify buttons exist
await expect(levelSection.getByRole('button', { name: 'Intermediate' })).toBeVisible();
// Intensity default: Moderate
const intensitySection = page.locator('div').filter({ hasText: 'Intensity' }).last();
await expect(intensitySection.getByRole('button', { name: 'Moderate' })).toBeVisible();
// Modify Inputs
// Change Level to Advanced
await levelSection.getByRole('button', { name: 'Advanced' }).click();
// Change Intensity to High
await intensitySection.getByRole('button', { name: 'High' }).click();
// Change Equipment to Free Weights
await eqSection.getByRole('button', { name: /Free weights/i }).click();
// Click Generate
await page.getByRole('button', { name: 'Generate' }).click();
// Verify Preview
// Wait for preview to appear (mock response)
await expect(page.getByText('Generated Plan')).toBeVisible({ timeout: 10000 });
await expect(page.getByText('Mock Push-ups')).toBeVisible();
await expect(page.getByText('Mock Weighted Pull-ups')).toBeVisible();
// Icons check (weighted vs bodyweight) - optional visual check
// Click Save Plan
await page.getByRole('button', { name: 'Save Plan' }).click();
// Verify Saved
// Should close sheet and show plan list
await expect(page.getByText('AI Advanced Plan')).toBeVisible();
await expect(page.getByText('Generated High Intensity Plan')).toBeVisible();
});
});
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 or dashboard loaded fast
}
return user;
}

View File

@@ -1,24 +0,0 @@
import { test, expect } from './fixtures';
test('Debug Login Payload', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
console.log('Created user:', user);
await page.goto('/');
// Intercept login request
await page.route('**/api/auth/login', async route => {
const request = route.request();
const postData = request.postDataJSON();
console.log('LOGIN REQUEST BODY:', JSON.stringify(postData, null, 2));
console.log('LOGIN REQUEST HEADERS:', JSON.stringify(request.headers(), null, 2));
await route.continue();
});
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Wait a bit for request to happen
await page.waitForTimeout(3000);
});

View File

@@ -1,45 +0,0 @@
import { test, expect } from './fixtures';
import { request as playwrightRequest } from '@playwright/test';
import path from 'path';
import fs from 'fs';
test('Default Exercises Creation', async ({ createUniqueUser }) => {
// 1. Create a user
const user = await createUniqueUser();
// 2. Fetch exercises for the user
// Create authenticated context
const apiContext = await playwrightRequest.newContext({
baseURL: 'http://127.0.0.1:3001',
extraHTTPHeaders: {
'Authorization': `Bearer ${user.token}`
}
});
const exercisesRes = await apiContext.get('/api/exercises');
await expect(exercisesRes).toBeOK();
const responseJson = await exercisesRes.json();
console.log('DEBUG: Fetched exercises response:', JSON.stringify(responseJson, null, 2));
const exercises = responseJson.data;
// 3. Verify default exercises are present
// Checking a subset of influential exercises from the populated list
const expectedNames = ['Bench Press', 'Squat', 'Deadlift', 'Push-Ups', 'Pull-Ups', 'Running', 'Plank', 'Handstand', 'Sprint', 'Bulgarian Split-Squats'];
for (const name of expectedNames) {
const found = exercises.find((e: any) => e.name === name);
expect(found, `Exercise ${name} should exist`).toBeDefined();
}
// 4. Verify properties
const dumbbellCurl = exercises.find((e: any) => e.name === 'Dumbbell Curl');
expect(dumbbellCurl.isUnilateral).toBe(true);
expect(dumbbellCurl.type).toBe('STRENGTH');
const handstand = exercises.find((e: any) => e.name === 'Handstand');
expect(handstand.type).toBe('BODYWEIGHT');
expect(handstand.bodyWeightPercentage).toBe(1.0);
const pushUps = exercises.find((e: any) => e.name === 'Push-Ups');
expect(pushUps.bodyWeightPercentage).toBe(0.65);
});

View File

@@ -1,105 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('Plan Editor Drag & Drop Vibration', () => {
test.beforeEach(async ({ page }) => {
// Mock navigator.vibrate
await page.addInitScript(() => {
try {
Object.defineProperty(navigator, 'vibrate', {
value: (pattern) => {
console.log(`Vibration triggered: ${pattern}`);
window.dispatchEvent(new CustomEvent('vibration-triggered', { detail: pattern }));
return true;
},
writable: true,
configurable: true,
});
} catch (e) {
console.error('Failed to mock vibrate', e);
}
});
await page.goto('/');
// Create a new user
const uniqueId = Date.now().toString();
const email = `dragvibetest${uniqueId}@example.com`;
await page.fill('input[type="email"]', email);
await page.fill('input[type="password"]', 'password123');
await page.click('button:has-text("Sign Up")');
await page.waitForURL('**/dashboard');
if (await page.getByPlaceholder('Enter your name').isVisible()) {
await page.getByPlaceholder('Enter your name').fill('Vibe Tester');
await page.getByRole('button', { name: 'Complete Profile' }).click();
}
});
test('should trigger vibration on drag start', async ({ page }) => {
// Navigate to Plans
await page.getByRole('button', { name: 'Plans' }).click();
// Create Plan
await page.getByLabel('Create Plan').click();
await page.getByLabel('Plan Name').fill('Vibration Plan');
// Add Exercises
await page.getByRole('button', { name: 'Add Exercise' }).click();
await page.getByRole('button', { name: 'Create New Exercise' }).click();
await page.getByLabel('Exercise Name').fill('Exercise 1');
await page.getByRole('button', { name: 'Save Exercise' }).click();
await page.getByRole('button', { name: 'Add Exercise' }).click();
await page.getByRole('button', { name: 'Create New Exercise' }).click();
await page.getByLabel('Exercise Name').fill('Exercise 2');
await page.getByRole('button', { name: 'Save Exercise' }).click();
// Listen for vibration event with timeout
let vibrationDetected = false;
page.on('console', msg => {
if (msg.text().includes('Vibration triggered') || msg.text().includes('handlePointerDown')) {
console.log('Browser Console:', msg.text());
}
if (msg.text().includes('Vibration triggered')) vibrationDetected = true;
});
// Drag
const dragHandle = page.locator('.cursor-grab').first();
const dragDest = page.locator('.cursor-grab').nth(1);
// Drag using manual pointer control simulating TOUCH via evaluate
const box = await dragHandle.boundingBox();
if (box) {
// Dispatch directly in browser to ensure React synthetic event system picks it up
await dragHandle.evaluate((el) => {
const event = new PointerEvent('pointerdown', {
bubbles: true,
cancelable: true,
pointerType: 'touch',
clientX: 0,
clientY: 0,
isPrimary: true
});
el.dispatchEvent(event);
});
// Wait for usage
await page.waitForTimeout(500);
// Dispatch pointerup
await dragHandle.evaluate((el) => {
const event = new PointerEvent('pointerup', {
bubbles: true,
cancelable: true,
pointerType: 'touch',
isPrimary: true
});
el.dispatchEvent(event);
});
}
// Check flag
expect(vibrationDetected).toBeTruthy();
});
});

View File

@@ -1,98 +0,0 @@
import { test, expect } from './fixtures';
import * as fs from 'fs';
import * as path from 'path';
test.describe('History Export', () => {
test.beforeEach(async ({ page }) => {
// Console logs for debugging
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
page.on('pageerror', exception => console.log(`PAGE ERROR: ${exception}`));
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('/');
});
// Helper to handle first login
async function handleFirstLogin(page: any) {
try {
const heading = page.getByRole('heading', { name: /Change Password/i });
const dashboard = page.getByText('Free Workout');
// Wait for Change Password or Dashboard
await expect(heading).toBeVisible({ timeout: 10000 });
// If we are here, Change Password is visible
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
// Now expect dashboard
await expect(dashboard).toBeVisible();
} catch (e) {
// Check if already at dashboard
if (await page.getByText('Free Workout').isVisible()) {
return;
}
throw e;
}
}
test('should export workout history as CSV', async ({ page, createUniqueUser, request }) => {
const user = await createUniqueUser();
// 1. Seed an exercise
const exName = 'Bench Press Test';
await request.post('/api/exercises', {
data: { name: exName, type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
// 2. Log in
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
await handleFirstLogin(page);
// 3. Log a workout
// We are likely already on Tracker, but let's be sure or just proceed
// If we want to navigate:
// await page.getByRole('button', { name: 'Tracker' }).first().click();
// Since handleFirstLogin confirms 'Free Workout' is visible, we are on Tracker.
const freeWorkoutBtn = page.getByRole('button', { name: 'Free Workout' });
await expect(freeWorkoutBtn).toBeVisible();
await freeWorkoutBtn.click();
await expect(page.getByRole('button', { name: 'Finish' })).toBeVisible();
// Log a set
await page.getByPlaceholder('Select Exercise').click();
await page.getByText(exName).first().click();
await page.getByPlaceholder('Weight').fill('100');
await page.getByPlaceholder('Reps').fill('10');
await page.getByRole('button', { name: 'Log Set' }).click();
// Finish session
await page.getByRole('button', { name: 'Finish' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// 3. Navigate to History
await page.getByText('History', { exact: true }).click();
// 4. Setup download listener
const downloadPromise = page.waitForEvent('download');
// 5. Click Export button (Using the title we added)
// Note: The title comes from t('export_csv', lang), defaulting to 'Export CSV' in English
const exportBtn = page.getByRole('button', { name: 'Export CSV' });
await expect(exportBtn).toBeVisible();
await exportBtn.click();
const download = await downloadPromise;
// 6. Verify download
expect(download.suggestedFilename()).toContain('gymflow_history');
expect(download.suggestedFilename()).toContain('.csv');
});
});

View File

@@ -1,158 +0,0 @@
import { test, expect } from './fixtures';
import { generateId } from '../src/utils/uuid';
import { ExerciseType } from '../src/types';
test('Create Plan from Session mirrors all sets 1:1', async ({ page, request, createUniqueUser }) => {
// 1. Setup User
const user = await createUniqueUser();
// 2. Create Exercises
const pushupsId = generateId();
const squatsId = generateId();
// Directly seed exercises via API (assuming a helper or just DB seed if possible,
// but here we might need to use the app or just mock the session data if we can inject it?
// Actually, createUniqueUser returns a token. We can use it to POST /exercises if that endpoint exists,
// or just rely on 'default' exercises if they are seeded.
// Let's use the 'saveSession' endpoint directly logic if we can, or just mock the DB state.
// Wait, the app uses local storage mostly or sync?
// Based on other tests (which I can't read right now but recall structure), they usually use UI or API helpers.
// I will assume I can just Login and then use UI or API.
// Let's use UI to just ensure clean state, or API `POST /sessions` if available.
// Based on `server/src/routes/sessions.ts` existing, I can POST session.
// Let's rely on standard UI flows or API.
// API is faster.
const token = user.token;
// Create Custom Exercises via API
await request.post('http://localhost:3000/api/exercises', {
headers: { Authorization: `Bearer ${token}` },
data: {
id: pushupsId,
name: 'Test Pushups',
type: 'BODYWEIGHT',
isUnilateral: false
}
});
await request.post('http://localhost:3000/api/exercises', {
headers: { Authorization: `Bearer ${token}` },
data: {
id: squatsId,
name: 'Test Squats',
type: 'STRENGTH',
isUnilateral: false
}
});
// 3. Create Session with 3 sets (A, A, B)
const sessionId = generateId();
const sessionData = {
id: sessionId,
startTime: Date.now() - 3600000, // 1 hour ago
endTime: Date.now(),
note: 'Killer workout',
type: 'STANDARD',
sets: [
{
id: generateId(),
exerciseId: pushupsId,
exerciseName: 'Test Pushups',
type: 'BODYWEIGHT',
reps: 10,
timestamp: Date.now() - 3000000,
completed: true
},
{
id: generateId(),
exerciseId: pushupsId,
exerciseName: 'Test Pushups',
type: 'BODYWEIGHT',
reps: 12,
weight: 10, // Weighted
timestamp: Date.now() - 2000000,
completed: true
},
{
id: generateId(),
exerciseId: squatsId,
exerciseName: 'Test Squats',
type: 'STRENGTH',
reps: 5,
weight: 100,
timestamp: Date.now() - 1000000,
completed: true
}
]
};
await request.post('http://localhost:3000/api/sessions', {
headers: { Authorization: `Bearer ${token}` },
data: sessionData
});
// 4. Login and Navigate
await page.goto('http://localhost:3000/');
await page.fill('input[type="email"]', user.email);
await page.fill('input[type="password"]', user.password);
await page.click('button:has-text("Login")');
await page.waitForURL('**/tracker');
// 5. Go to History
await page.click('text=History');
// 6. Click Create Plan
const sessionCard = page.locator('div.bg-surface-container').first(); // Assuming it's the first card
await sessionCard.waitFor();
// Open Menu
await sessionCard.locator('button[aria-label="Session Actions"]').click();
// Click 'Create Plan'
await page.click('text=Create Plan');
// 7. Verify Redirection
await expect(page).toHaveURL(/.*plans\?createFromSessionId=.*/);
// 8. Verify Plan Editor Content
await expect(page.locator('h2')).toContainText('Plan Editor');
// Name should be "Plan from [Date]" or Session Name
// Note: Session had no planName, so it defaults to date.
// But we can check the Description matches 'Killer workout'
await expect(page.locator('textarea')).toHaveValue('Killer workout');
// 9. Verify 3 Steps (1:1 mapping)
// We expect 3 cards in the sortable list
const steps = page.locator('.dnd-sortable-item_content, div[class*="items-center"] > div.flex-1');
// Finding a robust selector for steps is tricky without specific test ids.
// The SortablePlanStep component has `div.text-base.font-medium.text-on-surface` for exercise name.
const stepNames = page.locator('div.text-base.font-medium.text-on-surface');
await expect(stepNames).toHaveCount(3);
await expect(stepNames.nth(0)).toHaveText('Test Pushups');
await expect(stepNames.nth(1)).toHaveText('Test Pushups');
await expect(stepNames.nth(2)).toHaveText('Test Squats');
// 10. Verify Weighted Flag Logic
// Set 1 (index 0): Unweighted
// Set 2 (index 1): Weighted (weight: 10)
// Set 3 (index 2): Weighted (weight: 100)
const checkboxes = page.locator('input[type="checkbox"]');
// Warning: there might be other checkboxes.
// SortablePlanStep has a checkbox for 'weighted'.
// Better to look for checked state within the step card.
// Step 1: Unchecked
await expect(page.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
// Step 2: Checked
await expect(page.locator('input[type="checkbox"]').nth(1)).toBeChecked();
// Step 3: Checked
await expect(page.locator('input[type="checkbox"]').nth(2)).toBeChecked();
});

View File

@@ -1,136 +0,0 @@
import { test, expect } from './fixtures';
test.describe('Reproduction - Edit Modal Fields', () => {
test('Verify Edit Fields for different Exercise Types', async ({ page, createUniqueUser, request }) => {
const user = await createUniqueUser();
// Login
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Wait for dashboard or password change
try {
const heading = page.getByRole('heading', { name: /Change Password/i });
const dashboard = page.getByText('Free Workout');
await expect(heading.or(dashboard)).toBeVisible({ timeout: 10000 });
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) {
console.log('Login flow exception (might be benign if already logged in):', e);
}
// Seed exercises of different types
const types = [
{ type: 'PLYOMETRIC', name: 'Plyo Test', expectedFields: ['Reps'] },
{ type: 'STRENGTH', name: 'Strength Test', expectedFields: ['Weight', 'Reps'] },
{ type: 'CARDIO', name: 'Cardio Test', expectedFields: ['Time', 'Distance'] },
{ type: 'STATIC', name: 'Static Test', expectedFields: ['Time', 'Weight', 'Body Weight'] }, // Check if Weight is expected based on History.tsx analysis
{ type: 'BODYWEIGHT', name: 'Bodyweight Test', expectedFields: ['Reps', 'Body Weight', 'Weight'] },
{ type: 'HIGH_JUMP', name: 'High Jump Test', expectedFields: ['Height'] },
{ type: 'LONG_JUMP', name: 'Long Jump Test', expectedFields: ['Distance'] },
];
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();
// Adjust if the response structure is different (e.g. created.exercise)
exIds[t.name] = created.id || created.exercise?.id || created.data?.id;
}
await page.reload();
// Construct a session payload
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: {
startTime: now,
endTime: now + 3600000,
type: 'STANDARD', // History shows STANDARD sessions differently than QUICK_LOG
sets: setsStub
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
if (!sessionResp.ok()) {
console.log('Session Create Error:', await sessionResp.text());
}
expect(sessionResp.ok()).toBeTruthy();
// Go to History
await page.getByRole('button', { name: 'History' }).first().click();
// Find the session card and click Edit (Pencil icon)
// There should be only one session
await page.locator('.lucide-pencil').first().click();
await expect(page.getByText('Edit', { exact: true })).toBeVisible();
// Now verify fields for each exercise in the modal
for (const t of types) {
const exRow = page.locator('div').filter({ hasText: t.name }).last(); // Find the row for this exercise
// This locator might be tricky if the row structure is complex.
// In History.tsx:
// {editingSession.sets.map((set, idx) => (
// <div key={set.id} ...>
// ... <span>{set.exerciseName}</span> ...
// <div className="grid ..."> inputs here </div>
// </div>
// ))}
// So we find the container that has the exercise name, then look for inputs inside it.
const row = page.locator('.bg-surface-container-low').filter({ hasText: t.name }).first();
await expect(row).toBeVisible();
console.log(`Checking fields for ${t.type} (${t.name})...`);
for (const field of t.expectedFields) {
// Map field name to label text actually used in History.tsx
// t('weight_kg', lang) -> "Weight" (assuming en)
// t('reps', lang) -> "Reps"
// t('time_sec', lang) -> "Time"
// t('dist_m', lang) -> "Distance"
// t('height_cm', lang) -> "Height"
// t('body_weight_percent', lang) -> "Body Weight %"
let labelPattern: RegExp;
if (field === 'Weight') labelPattern = /Weight/i;
else if (field === 'Reps') labelPattern = /Reps/i;
else if (field === 'Time') labelPattern = /Time/i;
else if (field === 'Distance') labelPattern = /Distance|Dist/i;
else if (field === 'Height') labelPattern = /Height/i;
else if (field === 'Body Weight') labelPattern = /Body Weight/i;
else labelPattern = new RegExp(field, 'i');
await expect(row.getByLabel(labelPattern).first()).toBeVisible({ timeout: 2000 })
.catch(() => { throw new Error(`Missing field '${field}' for type '${t.type}'`); });
}
}
});
});

View File

@@ -1,219 +0,0 @@
import { test, expect } from './fixtures';
test.describe('Rest Timer', () => {
test('should allow setting a rest time in a free session', async ({ page, createUniqueUser }) => {
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 {
await page.getByRole('heading', { name: 'Change Password' }).waitFor();
await page.getByLabel('New Password').fill('StrongNewPassword123!');
await page.getByRole('button', { name: 'Save & Login' }).click();
} catch (e) {
// Ignore if the change password screen is not visible
}
await expect(page.getByText('Free Workout')).toBeVisible();
// Click the "Free Workout" button.
await page.getByRole('button', { name: 'Free Workout' }).click();
// The FAB timer should be visible (IDLE state: icon only)
const fab = page.locator('.fixed.bottom-24.right-6');
await expect(fab).toBeVisible();
// Click on the rest timer FAB to expand it and reveal the time value.
await fab.click();
// Wait for expansion and Edit button visibility
const editBtn = fab.locator('button[aria-label="Edit"]');
await expect(editBtn).toBeVisible();
await editBtn.click();
// Change the rest timer value to 90 seconds.
await page.getByRole('textbox').nth(1).fill('90');
// Confirm the new rest timer value.
const saveBtn = fab.locator('button[aria-label="Save"]');
await expect(saveBtn).toBeVisible();
await saveBtn.click();
// The timer should now be 90 seconds.
await expect(page.locator('div').filter({ hasText: /1:30|90/ }).first()).toBeVisible();
});
test('should validate manual input in the rest timer', async ({ page, createUniqueUser }) => {
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 {
await page.getByRole('heading', { name: 'Change Password' }).waitFor();
await page.getByLabel('New Password').fill('StrongNewPassword123!');
await page.getByRole('button', { name: 'Save & Login' }).click();
} catch (e) {
// Ignore
}
await expect(page.getByText('Free Workout')).toBeVisible();
// Start a Free Workout
await page.getByRole('button', { name: 'Free Workout' }).click();
// Expand the Rest Timer FAB (Click first!)
const fab = page.locator('.fixed.bottom-24.right-6');
await fab.click();
// Click 'Edit'
const editBtn = fab.locator('button[aria-label="Edit"]');
await expect(editBtn).toBeVisible();
await editBtn.click();
// Type '90' -> Verify '1:30' (if auto-format implemented) or manual '1:30'.
const timerInput = page.getByRole('textbox').nth(1);
await timerInput.fill('90');
await expect(timerInput).toHaveValue('90');
// Attempt to type non-digits -> Verify they are ignored.
await timerInput.fill('90abc');
await expect(timerInput).toHaveValue('90');
// Attempt to type '10:99' (invalid seconds) -> Verify it corrects to '10:59'.
await timerInput.fill('10:99');
await expect(timerInput).toHaveValue('10:59');
// Save
const saveBtn = fab.locator('button[aria-label="Save"]');
await saveBtn.click();
// Verify updated value in FAB (might need to wait or check visibility)
// After save, it usually stays expanded per code "setIsExpanded(true)"
});
test('should persist the rest timer value across sessions', async ({ page, createUniqueUser }) => {
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 {
await page.getByRole('heading', { name: 'Change Password' }).waitFor();
await page.getByLabel('New Password').fill('StrongNewPassword123!');
await page.getByRole('button', { name: 'Save & Login' }).click();
} catch (e) {
// Ignore
}
await expect(page.getByText('Free Workout')).toBeVisible();
// Start a Free Workout
await page.getByRole('button', { name: 'Free Workout' }).click();
// Click FAB to expand
const fab = page.locator('.fixed.bottom-24.right-6');
await fab.click();
// Edit timer to '0:45'.
const editBtn = fab.locator('button[aria-label="Edit"]');
await editBtn.click();
const timerInput = page.getByRole('textbox').nth(1);
await timerInput.fill('45');
const saveBtn = fab.locator('button[aria-label="Save"]');
await saveBtn.click();
// Quit session
await page.getByRole('button', { name: 'Finish' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// Start Quick Log
await page.getByRole('button', { name: 'Quick Log' }).click();
// Verify timer default is now '0:45'.
const quickFab = page.locator('.fixed.bottom-24.right-6');
await quickFab.click();
await expect(page.locator('div').filter({ hasText: /0:45/ }).first()).toBeVisible();
});
test('should integrate the rest timer with plans', async ({ page, createUniqueUser }) => {
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 {
await page.getByRole('heading', { name: 'Change Password' }).waitFor();
await page.getByLabel('New Password').fill('StrongNewPassword123!');
await page.getByRole('button', { name: 'Save & Login' }).click();
} catch (e) {
// Ignore
}
await expect(page.getByText('Free Workout')).toBeVisible();
// Navigate to the "Plans" page.
await page.getByRole('button', { name: 'Plans' }).click();
// Create a new plan.
await page.getByRole('button', { name: 'Create Plan' }).click();
// Name the plan "Timer Test Plan".
await page.getByRole('textbox', { name: 'Name' }).fill('Timer Test Plan');
// Add the first exercise.
await page.getByRole('button', { name: 'Add Exercise' }).click();
await page.getByRole('button', { name: 'New Exercise' }).click();
await page.locator('[id="_r_4_"]').fill('Bench Press');
await page.getByRole('button', { name: 'Free Weights & Machines' }).click();
await page.getByRole('button', { name: 'Create' }).click();
await page.getByPlaceholder('Rest (s)').fill('30');
// Add the second exercise.
await page.getByRole('button', { name: 'Add Exercise' }).click();
await page.getByRole('button', { name: 'New Exercise' }).click();
await page.locator('[id="_r_5_"]').fill('Squat');
await page.getByRole('button', { name: 'Free Weights & Machines' }).click();
await page.getByRole('button', { name: 'Create' }).click();
await page.getByPlaceholder('Rest (s)').nth(1).fill('60');
// Save the plan.
await page.getByRole('button', { name: 'Save' }).click();
// Start the plan.
await page.getByRole('button', { name: 'Start' }).click();
// Expect Preparation Modal
const modal = page.locator('.fixed.inset-0.z-50');
await expect(modal).toBeVisible();
await expect(modal.getByText('Ready to go')).toBeVisible();
// Click Start in the modal (ensure we click the button inside the modal)
await modal.getByRole('button', { name: 'Start' }).click();
// Timer Update: Click FAB
const fab = page.locator('.fixed.bottom-24.right-6');
await fab.click();
// Verify Timer shows '0:30'. Start it.
await expect(page.locator('div').filter({ hasText: /0:30/ }).first()).toBeVisible();
const startBtn = fab.locator('button[aria-label="Start"]');
await startBtn.click();
// Log Set for Step A while timer is running.
await page.getByRole('button', { name: 'Log Set' }).click();
// Verify Timer continues running (does not reset).
await expect(page.locator('div').filter({ hasText: /0:2[0-9]/ }).first()).toBeVisible();
// Reset Timer manually (or wait for finish).
const resetBtn = fab.locator('button[aria-label="Reset"]');
await resetBtn.click();
// Verify Timer now shows '1:00' (for Step B).
await expect(page.locator('div').filter({ hasText: /1:00/ }).first()).toBeVisible();
});
});

View File

@@ -1,68 +0,0 @@
import { test, expect } from './fixtures';
test('can enable unilateral flag for existing exercise', async ({ page, createUniqueUser }) => {
console.log('START: Test started');
const user = await createUniqueUser();
console.log('USER created: ' + user.email);
// 1. Login
console.log('Navigating to login...');
await page.goto('http://localhost:3000/');
console.log('Filling login form...');
await page.fill('input[type="email"]', user.email);
await page.fill('input[type="password"]', user.password);
await page.click('button:has-text("Login")');
console.log('Waiting for Tracker...');
await expect(page.getByText('Tracker')).toBeVisible();
// 2. Create a standard exercise via Profile
console.log('Navigating to Profile...');
await page.getByText('Profile').click();
console.log('Clicking Manage Exercises...');
await page.getByRole('button', { name: 'Manage Exercises' }).click();
// Open create modal
console.log('Clicking New Exercise...');
await page.getByRole('button', { name: 'New Exercise' }).click();
const exName = `Test Uni ${Date.now()}`;
console.log('Creating exercise:', exName);
await page.getByLabel('Name').fill(exName);
await page.getByRole('button', { name: 'Create' }).click();
// Verify it exists in list
console.log('Verifying creation...');
await expect(page.getByText(exName)).toBeVisible();
// 3. Edit exercise to be Unilateral
console.log('Finding row to edit...');
const row = page.locator('div.flex.justify-between.items-center').filter({ hasText: exName }).first();
console.log('Clicking Edit...');
await row.getByRole('button', { name: 'Edit Exercise' }).click();
// Check the Unilateral checkbox
console.log('Checking Unilateral...');
await page.getByLabel('Unilateral exercise').check();
await page.getByRole('button', { name: 'Save' }).click();
// Verify "Unilateral" tag appears in the list
console.log('Verifying Unilateral tag...');
await expect(row).toContainText('Unilateral');
// 4. Verify in Tracker
console.log('Navigating to Tracker...');
await page.getByText('Tracker').click();
// Select the exercise
console.log('Selecting exercise...');
await page.getByLabel('Select Exercise').fill(exName);
await page.getByRole('button', { name: exName }).click();
// Verify L/A/R buttons appear
console.log('Checking buttons...');
await expect(page.getByTitle('Left')).toBeVisible();
await expect(page.getByTitle('Right')).toBeVisible();
await expect(page.getByTitle('Alternately')).toBeVisible();
console.log('DONE: Test finished successfully');
});

View File

@@ -1,469 +0,0 @@
import { test, expect } from './fixtures';
import { randomUUID } from 'crypto';
test.describe('II. Workout Management', () => {
test('2.1 A. Workout Plans - Create New Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
// Seed exercise
const seedResp = await request.post('/api/exercises', {
data: { name: 'Test Sq', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(seedResp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
await page.getByRole('button', { name: 'Create Plan' }).click();
// Wait for potential animation/loading
await expect(page.getByText('Plan Editor')).toBeVisible({ timeout: 10000 });
await page.getByLabel(/Name/i).fill('My New Strength Plan');
await page.getByPlaceholder(/Describe preparation/i).fill('Focus on compound lifts');
await page.getByRole('button', { name: 'Add Exercise' }).click();
await expect(page.getByText('Select Exercise')).toBeVisible();
await page.getByText('Test Sq').click();
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('My New Strength Plan')).toBeVisible();
await expect(page.getByText('Focus on compound lifts')).toBeVisible();
});
test('2.2 A. Workout Plans - Edit Existing Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const seedResp = await request.post('/api/plans', {
data: {
id: randomUUID(),
name: 'Original Plan',
description: 'Original Description',
steps: []
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(seedResp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
await expect(page.getByText('Original Plan')).toBeVisible();
const card = page.locator('div')
.filter({ hasText: 'Original Plan' })
.filter({ has: page.getByRole('button', { name: 'Edit Plan' }) })
.last();
await card.getByRole('button', { name: 'Edit Plan' }).click();
await page.getByLabel(/Name/i).fill('Updated Plan Name');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Updated Plan Name')).toBeVisible();
await expect(page.getByText('Original Plan')).not.toBeVisible();
});
test('2.3 A. Workout Plans - Delete Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const resp = await request.post('/api/plans', {
data: {
id: randomUUID(),
name: 'Plan To Delete',
description: 'Delete me',
steps: []
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
expect(resp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
page.on('dialog', dialog => dialog.accept());
const card = page.locator('div')
.filter({ hasText: 'Plan To Delete' })
.filter({ has: page.getByRole('button', { name: 'Delete Plan' }) })
.last();
await card.getByRole('button', { name: 'Delete Plan' }).click();
await expect(page.getByText('Plan To Delete')).not.toBeVisible();
});
test('2.4 A. Workout Plans - Reorder Exercises', async ({ page, createUniqueUser, request }) => {
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
const user = await loginAndSetup(page, createUniqueUser);
// Need exercises
const ex1Id = randomUUID();
const ex2Id = randomUUID();
await request.post('/api/exercises', {
data: { id: ex1Id, name: 'Ex One', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
await request.post('/api/exercises', {
data: { id: ex2Id, name: 'Ex Two', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
const planId = randomUUID();
await request.post('/api/plans', {
data: {
id: planId,
name: 'Reorder Plan',
description: 'Testing reorder',
steps: [
{ exerciseId: ex1Id, isWeighted: false },
{ exerciseId: ex2Id, isWeighted: false }
]
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
// Use the new aria-label selector
const card = page.locator('div')
.filter({ hasText: 'Reorder Plan' })
.filter({ has: page.getByRole('button', { name: 'Edit Plan' }) })
.last();
await card.getByRole('button', { name: 'Edit Plan' }).click();
const card1 = page.locator('[draggable="true"]').filter({ hasText: 'Ex One' });
const card2 = page.locator('[draggable="true"]').filter({ hasText: 'Ex Two' });
// Initial state check
await expect(page.locator('[draggable="true"]').first()).toContainText('Ex One');
// Drag using handles with explicit wait
const sourceHandle = card1.locator('.lucide-grip-vertical');
const targetHandle = card2.locator('.lucide-grip-vertical');
await expect(sourceHandle).toBeVisible();
await expect(targetHandle).toBeVisible();
console.log('Starting Drag...');
await sourceHandle.dragTo(targetHandle);
console.log('Drag complete');
// Wait for reorder to settle
await page.waitForTimeout(1000);
// Verify Swap immediately
await expect(page.locator('[draggable="true"]').first()).toContainText('Ex Two');
await page.getByRole('button', { name: 'Save' }).click();
// Reload and verify persistence
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
const cardRevisit = page.locator('div')
.filter({ hasText: 'Reorder Plan' })
.filter({ has: page.getByRole('button', { name: 'Edit Plan' }) })
.last();
await cardRevisit.getByRole('button', { name: 'Edit Plan' }).click();
await expect(page.locator('[draggable="true"]').first()).toContainText('Ex Two');
await expect(page.locator('[draggable="true"]').last()).toContainText('Ex One');
});
test('2.5 A. Workout Plans - Start Session from Plan', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
const resp = await request.post('/api/plans', {
data: {
id: randomUUID(),
name: 'Startable Plan',
description: 'Ready to go',
steps: []
},
headers: { 'Authorization': `Bearer ${user.token}` }
});
console.log(await resp.json());
expect(resp.ok()).toBeTruthy();
await page.reload();
await page.getByRole('button', { name: 'Plans' }).first().click();
const card = page.locator('div')
.filter({ hasText: 'Startable Plan' })
.filter({ has: page.getByRole('button', { name: 'Start' }) })
.last();
await card.getByRole('button', { name: 'Start' }).click();
// Expect Preparation Modal
const modal = page.locator('.fixed.inset-0.z-50');
await expect(modal).toBeVisible();
await expect(modal.getByText('Ready to go')).toBeVisible();
// Click Start in the modal (ensure we click the button inside the modal)
await modal.getByRole('button', { name: 'Start' }).click();
await expect(page.getByText('Startable Plan', { exact: false })).toBeVisible();
await expect(page.getByRole('button', { name: 'Finish' })).toBeVisible();
});
// --- Exercise Tests ---
test('2.6 B. Exercise Library - Create Custom Exercise (Strength)', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile' }).click();
await page.locator('button:has-text("Manage Exercises")').click();
// Use force click as button might be obstructed or animating
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Custom Bicep Curl');
await expect(page.locator('div[role="dialog"]').getByText('Free Weights & Machines', { exact: false })).toBeVisible();
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(1000);
// Reload and filter
await page.reload();
await page.getByRole('button', { name: 'Profile' }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByLabel(/Filter by name/i).fill('Custom Bicep Curl');
await expect(page.getByText('Custom Bicep Curl')).toBeVisible();
});
test('2.7 B. Exercise Library - Create Custom Exercise (Bodyweight)', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Adv Pushup');
// Scope to dialog and use force click for type selection
await page.locator('div[role="dialog"]').getByRole('button', { name: /Bodyweight/i }).click({ force: true });
await expect(page.getByLabel('Body Weight')).toBeVisible();
await page.getByLabel('Body Weight').fill('50');
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(1000);
// Reload and filter
await page.reload();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('Adv Pushup');
await expect(page.getByText('Adv Pushup')).toBeVisible();
await expect(page.getByText('Bodyweight', { exact: false }).first()).toBeVisible();
});
test('2.8 B. Exercise Library - Edit Exercise Name', async ({ page, createUniqueUser }) => {
// Updated to use UI creation for robustness
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile' }).click();
await page.locator('button:has-text("Manage Exercises")').click();
// Use force click as button might be obstructed or animating
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Typo Name');
await expect(page.locator('div[role="dialog"]').getByText('Free Weights & Machines', { exact: false })).toBeVisible();
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(1000);
// Reload and filter
await page.reload();
await page.getByRole('button', { name: 'Profile' }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByLabel(/Filter by name/i).fill('Typo Name');
await expect(page.getByText('Typo Name')).toBeVisible();
// Filter specifically for the container that has both text and button
const row = page.locator('div')
.filter({ hasText: 'Typo Name' })
.filter({ has: page.getByLabel('Edit Exercise') })
.last();
await expect(row).toBeVisible();
await row.getByLabel('Edit Exercise').click();
await page.locator('div[role="dialog"] input').first().fill('Fixed Name');
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Save', exact: true }).click();
// Clear filter to see the renamed exercise
await page.getByLabel(/Filter by name/i).fill('');
await expect(page.getByText('Fixed Name')).toBeVisible();
await expect(page.getByText('Typo Name')).not.toBeVisible();
});
test('2.9 B. Exercise Library - Archive/Unarchive', async ({ page, createUniqueUser }) => {
// Updated to use UI creation for robustness
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile' }).click();
await page.locator('button:has-text("Manage Exercises")').click();
// Use force click as button might be obstructed or animating
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Archive Me');
await expect(page.locator('div[role="dialog"]').getByText('Free Weights & Machines', { exact: false })).toBeVisible();
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(1000);
// Reload and filter
await page.reload();
await page.getByRole('button', { name: 'Profile' }).click();
await page.locator('button:has-text("Manage Exercises")').click();
await page.getByLabel(/Filter by name/i).fill('Archive Me');
await expect(page.getByText('Archive Me')).toBeVisible();
const row = page.locator('div.flex.justify-between').filter({ hasText: 'Archive Me' }).last();
// Archive button (box-archive or similar)
await row.locator('[aria-label="Archive Exercise"]').click();
// It should disappear or fade. "Show Archived" is false by default.
await expect(page.getByText('Archive Me')).not.toBeVisible();
// Toggle Show Archived
// Label might not be linked, so we filter by text and find the adjacent checkbox
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').check();
await expect(page.getByText('Archive Me')).toBeVisible();
// Unarchive
const archivedRow = page.locator('div')
.filter({ hasText: 'Archive Me' })
.filter({ has: page.getByLabel('Unarchive Exercise') })
.last();
await archivedRow.getByLabel('Unarchive Exercise').click();
// Verify it persists after unchecking "Show Archived"
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').uncheck();
await expect(page.getByText('Archive Me')).toBeVisible();
});
test('2.10 B. Exercise Library - Filter by Name', async ({ page, createUniqueUser, request }) => {
const user = await loginAndSetup(page, createUniqueUser);
await request.post('/api/exercises', {
data: { name: 'FindThisOne', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
await request.post('/api/exercises', {
data: { name: 'IgnoreThatOne', type: 'STRENGTH' },
headers: { 'Authorization': `Bearer ${user.token}` }
});
await page.reload();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('FindThis');
await expect(page.getByText('FindThisOne')).toBeVisible();
await expect(page.getByText('IgnoreThatOne')).not.toBeVisible();
});
test('2.11 B. Exercise Library - Capitalization (Mobile)', async ({ page, createUniqueUser }) => {
// Simulate Mobile Viewport
await page.setViewportSize({ width: 390, height: 844 }); // iPhone 12 Pro
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile' }).click();
await page.locator('button:has-text("Manage Exercises")').click();
// Use force as FAB might be different on mobile, but text is same
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
// Verify autocapitalize attribute is set to 'words' or 'sentences'
// In ExerciseModal.tsx it is set to 'words'
const nameInput = page.locator('div[role="dialog"]').getByLabel('Name');
await expect(nameInput).toHaveAttribute('autocapitalize', 'words');
});
test('2.12 B. Exercise Library - Unilateral', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Single Leg Squat');
await page.getByLabel(/Unilateral exercise/).check();
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(1000);
// Reload and filter
await page.reload();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('Single Leg Squat');
await expect(page.getByText('Single Leg Squat')).toBeVisible();
await expect(page.getByText('Unilateral', { exact: false }).first()).toBeVisible();
});
test('2.13 B. Exercise Library - Special Types', async ({ page, createUniqueUser }) => {
await loginAndSetup(page, createUniqueUser);
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByRole('button', { name: /New Exercise/i }).click({ force: true });
await expect(page.locator('div[role="dialog"]')).toBeVisible();
await page.locator('div[role="dialog"]').getByLabel('Name').fill('Plank Test');
await page.locator('div[role="dialog"]').getByRole('button', { name: /Static/i }).click({ force: true });
await page.locator('div[role="dialog"]').getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(1000);
// Reload and filter
await page.reload();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: /Manage Exercises/i }).click();
await page.getByLabel(/Filter by name/i).fill('Plank Test');
await expect(page.getByText('Plank Test')).toBeVisible();
await expect(page.getByText('Static', { exact: false }).first()).toBeVisible();
});
});
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 or dashboard loaded fast
}
return user;
}