test(e2e): add Playwright with browser tests for login, logo, dashboard

This commit is contained in:
2026-03-01 09:15:54 +01:00
parent b2073b5d4c
commit 9685d4d20c
8 changed files with 168 additions and 4 deletions
+7 -4
View File
@@ -1,9 +1,12 @@
{
"lastRun": "2026-03-01T04:00:00+01:00",
"status": "completed",
"lastRun": "2026-03-01T08:44:00+01:00",
"status": "in_progress",
"phase": "04-workout-modification",
"activeTask": "04-02-backend-api",
"activeTask": "04-03-frontend-workout-edit",
"tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api"],
"nextTask": "04-03-frontend-workout-edit",
"notes": "Backend API complete for custom workouts. Added 6 new endpoints + updated 3 log endpoints with source_type support. Next: Frontend edit UI."
"recoveryFrom": "2026-03-01T06:42:00+01:00",
"agentSession": "mild-reef",
"agentType": "claude-code",
"notes": "Frontend agent spawned for 04-03. Working on: Edit Workout button, Exercise picker modal, swap/add exercise flows, fork confirmation dialog. Session: mild-reef"
}
+67
View File
@@ -0,0 +1,67 @@
# Gravl PM - Active Task Queue
## Current: 04-03 Frontend - Workout Edit Mode
**Status:** IN PROGRESS (recovery from interruption)
**Agent:** Frontend (Claude Code)
**Directory:** /workspace/gravl/frontend
### Tasks
#### 1. Add "Edit Workout" Button
- Add edit button/icon on WorkoutSelectPage for program workouts
- Only show for workouts that are part of a program
- Button triggers edit mode/modal
#### 2. Create ExercisePicker Modal/Component
- Modal for selecting exercises from the database
- Search/filter functionality
- Exercise list with categories
- Select exercise with click/tap
- Reuse existing exercise data from current workout flow
#### 3. Implement Swap Exercise Flow
- On exercise row in edit mode, show swap button
- Open ExercisePicker modal
- Replace selected exercise in workout structure
- Maintain set/rep info where applicable
#### 4. Implement Add Exercise Flow
- "Add Exercise" button at bottom of workout
- Open ExercisePicker modal
- Append new exercise to workout with default sets/reps
- Allow configuring sets/reps for new exercise
#### 5. Fork Confirmation Dialog
- When user first modifies a program workout
- Explain: "This creates your personal version of this workout"
- Options: "Cancel", "Create My Version"
- Show only once per workout (set flag)
#### 6. Save Custom Workout
- POST to /api/custom-workouts on first modification (creates fork)
- PUT to /api/custom-workouts/:id on subsequent changes
- Update local state to use custom_workout_id
- Mark workout as "custom" in UI
### API Endpoints Available (from 04-02)
- POST /api/custom-workouts - Create custom workout from program
- PUT /api/custom-workouts/:id - Update exercises
- GET /api/custom-workouts/:id - Fetch with exercises
- GET /api/custom-workouts - List user's custom workouts
### Database Schema (from 04-01)
- custom_workouts table with user_id, name, original_program_day_id
- custom_workout_exercises table with exercise_id, set_order, sets, reps
### Success Criteria
- [ ] "Edit Workout" button visible on program workouts
- [ ] Exercise picker modal opens and shows exercises
- [ ] Can swap an exercise (replaces in workout)
- [ ] Can add new exercise (appends to workout)
- [ ] Fork confirmation shown on first edit
- [ ] Custom workout saves to backend
- [ ] Subsequent sessions use custom workout
### Next After This
- 04-04: Visual distinction (custom vs program badges)
- 04-05: Reset to original program option
View File
+64
View File
@@ -13,6 +13,7 @@
"react-router-dom": "^6.21.0"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
@@ -742,6 +743,22 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@remix-run/router": {
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
@@ -1481,6 +1498,53 @@
"dev": true,
"license": "ISC"
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+1
View File
@@ -14,6 +14,7 @@
"react-router-dom": "^6.21.0"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
testDir: "./tests",
use: {
baseURL: process.env.STAGING_URL || "https://gravl.homelab.local",
headless: true,
screenshot: "only-on-failure",
},
projects: [{
name: "chromium",
use: { browserName: "chromium" }
}]
};
+17
View File
@@ -0,0 +1,17 @@
const { test, expect } = require("@playwright/test");
test("login page loads", async ({ page }) => {
await page.goto("/login");
await expect(page.locator("form")).toBeVisible();
});
test("logo exists", async ({ page }) => {
await page.goto("/login");
const logo = await page.locator("svg, img[class*=logo], .logo").first();
await expect(logo).toBeVisible();
});
test("dashboard loads", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/Gravl/);
});
View File