Planner — Manual Test Protocol
A human-driven, click-through test script for the portal Planner section
(apps/portal/src/app/dashboard/planner). Work top to bottom; each case is
self-contained. Record Result (✅ pass / ❌ fail / ⚠️ partial) and Notes
(what you actually saw, plus any console/network error) in the right-hand
columns.
Scope: the calendar (month/week/day), data loading, the workout chips, and the schedule drawer (create / edit / delete, athletes, workout builder, recurring, the shared-definition 313 flow). Behaviour is described as currently implemented — if something differs, that difference is the finding.
Test environment
| Item | Value |
|---|---|
| App | @enode/portal dev server |
| Start command | NEXT_PUBLIC_API_SERVER=backdev npm run dev (from apps/portal), or the repo npm run dev picker |
| Backend | backdev (https://backdev.enode.ai/api) — serves the V2 workout DTO |
| URL | http://localhost:3000/dashboard/planner (or :3001 if 3000 is taken) |
| Auth | Logged in as a coach with at least one athlete and one workout template |
| Tools | Browser DevTools open — Console (JS errors) + Network (request/response) tabs |
Before you start: have DevTools open the whole time. A case "passes" only if the visible result is correct and the Console shows no errors and the relevant Network call returned 2xx (or an expected 313 — see §7).
Test-data prerequisites
- At least 2 athletes exist (for the athlete picker).
- At least 1 workout template with several exercises exists (for "select a template").
- At least 1 scheduled workout with exercises and athletes already on a known day (to test edit/display). Create one via §6 first if needed.
- At least 1 recurring workout (created via §6 with "Repeats weekly").
- Optional, for §7: a workout whose plan is shared across schedules (so the backend answers 313).
1. View switching & responsive layout
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 1.1 | Default view | Open /dashboard/planner on desktop | Month view renders, scrolled to the week containing today | ||
| 1.2 | Switch to Week | Click WEEK in the segmented control | Week view (Mon–Sun time grid) renders; control highlights WEEK | ||
| 1.3 | Switch to Day | Click DAY | Day view renders (single day column + mini-calendar on desktop) | ||
| 1.4 | Switch back to Month | Click MONTH | Month view renders, re-scrolled to today's week | ||
| 1.5 | Desktop keeps all 3 views | At ≥768px width, inspect the switcher | DAY, WEEK, MONTH all visible and selectable; no auto-switching on resize | ||
| 1.6 | Mobile portrait hides Week | Narrow window <768px, portrait | WEEK option is hidden; only DAY + MONTH selectable | ||
| 1.7 | Rotate to landscape adopts Week | Mobile, rotate portrait→landscape | View auto-switches to Week | ||
| 1.8 | Rotate to portrait adopts Day | Mobile, rotate landscape→portrait | View auto-switches to Day (Week is unavailable in portrait) | ||
| 1.9 | Manual override within orientation | Mobile portrait, on Day, tap MONTH | View stays MONTH until the next orientation change (no snap-back) |
2. Month view
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 2.1 | Grid fills viewport | Observe the month grid | Exactly 6 week rows fill the scroll area (no clipped half-row) | ||
| 2.2 | Weekday header order | Read the column headers | Mon → Sun (Monday-start) | ||
| 2.3 | Today highlighted | Find today's cell | Today's date number sits in an orange filled circle | ||
| 2.4 | Weekend cells muted | Compare Sat/Sun cells to weekdays | Weekend cells have a subtly muted background | ||
| 2.5 | Out-of-month dates dimmed | Scroll so two months show | Days outside the focused month render in a more-muted colour | ||
| 2.6 | Continuous scroll | Scroll up/down several weeks | Calendar scrolls smoothly week-by-week; weeks render far in both directions (no blank gap within ~40 weeks each way) | ||
| 2.7 | Header tracks scroll | Scroll across a month boundary | The header month + year updates to the month occupying most of the viewport | ||
| 2.8 | Resize re-measures | Resize the browser height | Rows resize so 6 weeks still fill the viewport; no overflow/scroll-jump | ||
| 2.9 | Hover affordance | Hover an empty day cell | A faint + icon appears top-left | ||
| 2.10 | Today button (month) | Scroll far away, click Today | Smoothly scrolls back to today's week |
3. Week view
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 3.1 | Time grid layout | Switch to Week | Left hour gutter + 7 day columns (Mon–Sun); ~12 hours fill the viewport | ||
| 3.2 | All-day section | Observe the top strip | A sticky All day row sits above the scrolling hour grid | ||
| 3.3 | Off-hours muting | Look at rows before 07:00 and at/after 22:00 | Those hour rows have a muted background | ||
| 3.4 | Today column marked | Find today in the day header | Today's date number is in an orange circle | ||
| 3.5 | Prev/next week | Click the ‹ / › chevrons | The week shifts back/forward by 7 days; header updates | ||
| 3.6 | Today button (week) | Navigate away, click Today | Returns to the current week (offset 0) | ||
| 3.7 | Hover affordance | Hover an hour cell | A faint + appears centered in the cell | ||
| 3.8 | Timed workout placement | View a day with a timed workout | Its chip is positioned vertically at its time-of-day (local time) |
4. Day view & mini-calendar
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 4.1 | Desktop layout | Switch to Day (desktop) | Persistent mini-calendar side panel on the left + single-day time grid on the right | ||
| 4.2 | Prev/next day | Click ‹ / › | Selected day moves back/forward by one day; grid + header update | ||
| 4.3 | Mini-calendar month nav | Click the mini-calendar's ‹ / › | Mini month changes independently of the selected day | ||
| 4.4 | Pick a date | Click a date in the mini-calendar | Main grid jumps to that day; that date shows as selected (orange) | ||
| 4.5 | Today vs selected styling | Compare today and a different selected day | Today = orange text/ring; selected = orange filled; only one filled at a time | ||
| 4.6 | "Has events" dot | Find a day with workouts (not selected) | A small green dot appears under that date | ||
| 4.7 | Mobile mini-calendar | Narrow to mobile portrait, tap the date header | Mini-calendar opens in a bottom sheet; picking a date closes it |
5. Workout chips (display) — V1→V2 regression focus
These verify the chips read the V2 model (
workoutDefinition.items). A regression here shows up as "0 exercises" on every chip.
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 5.1 | Chip content | Find a scheduled workout chip | Shows name + meta line: Scheduled · N exercises · M athletes | ||
| 5.2 | Exercise count correct | Compare a chip's N exercises to the actual exercise count in that workout | Count matches the workout's real exercise count — not 0 | ||
| 5.3 | Athlete count correct | Compare M athletes to assigned athletes | Count matches assigned participants | ||
| 5.4 | Singular/plural | Find a workout with exactly 1 exercise and 1 athlete | Reads 1 exercise · 1 athlete (singular), not 1 exercises | ||
| 5.5 | Recurring label | Find a recurring workout chip | Meta line begins Recurring (not Scheduled) | ||
| 5.6 | Recurring appears weekly | Note a recurring workout's weekday, scroll several weeks | The chip appears on every matching weekday across weeks | ||
| 5.7 | Ordering within a day | Open a day with all-day + timed + recurring items | Order is: all-day first → by time-of-day → scheduled before recurring → by name | ||
| 5.8 | Network sanity | In DevTools, inspect the GET /workouts/schedules?from=&to= response | Items are nested under workoutDefinition.items; chip count equals that array's length |
6. Schedule drawer — create ("new")
Opened by clicking an empty cell (month) or hour (week/day).
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 6.1 | Open from month cell | Click an empty day cell in Month | Drawer opens titled "Schedule workout"; Date prefilled to that day; All day = on | ||
| 6.2 | Open from hour cell | Click an empty hour cell in Week/Day | Drawer opens; Date = that day; All day = off; Time prefilled to the clicked hour | ||
| 6.3 | Toggle All day | Turn All day off | A Time row appears with a time input | ||
| 6.4 | Date editable | Change the Date field | Value updates; no error | ||
| 6.5 | Repeats weekly | Turn Repeats weekly on | A Repeats on row shows Every {weekday} matching the chosen date | ||
| 6.6 | Athletes empty state | Look at the Athletes section (fresh) | Shows "No athlete selected." + Add athletes button | ||
| 6.7 | Add athletes | Click Add athletes, select 2, confirm | Two athlete chips appear in a horizontal row; Edit athletes… link shows | ||
| 6.8 | Workout empty state | Look at the Workout section (fresh) | Shows "No exercises added yet." + Add exercise + or select a template | ||
| 6.9 | Build from scratch | Click Add exercise, pick 2 exercises, confirm | Two slot cards appear; a Name field shows above the list | ||
| 6.10 | Name required hint | Clear the Name field | Hint "Name your workout to schedule it." shows; Schedule button disabled | ||
| 6.11 | Pick a template | Click or select a template, pick one, confirm | Name/items populate from the template; slot cards reflect its exercises | ||
| 6.12 | Edit a slot | Tap a slot card | The slot editor drawer pushes on top; edits reflect back on close | ||
| 6.13 | Reorder slots | Drag a slot card to a new position | Order persists in the list | ||
| 6.14 | Break a superset | On a superset chain, trigger break link | The linked items split into separate slots | ||
| 6.15 | Save disabled until valid | Observe Schedule before items/name/date are set | Button disabled until: ≥1 item and a non-empty name and a date | ||
| 6.16 | Save (create) | Fill everything, click Schedule | Button shows "Saving…"; on success the drawer closes and the chip appears immediately on the calendar (no full refetch/flash) | ||
| 6.17 | Persisted across reload | Click the Refresh button (or reload page) | The created workout is still there (round-tripped to backend) | ||
| 6.18 | All-day time model | Create an all-day, non-recurring workout, inspect the create_schedule request body | timeMode: "none", date = a UTC midnight timestamp | ||
| 6.19 | Timed time model | Create a timed, non-recurring workout, inspect the body | timeMode: "universal", date encodes the chosen UTC wall-clock time | ||
| 6.20 | Recurring all-day model | Create a recurring all-day workout, inspect the body | state: "recurring", date: null, weekday set to the short day name | ||
| 6.21 | Recurring timed model | Create a recurring timed workout, inspect the body | state: "recurring", date = the 1971 template + time-of-day, weekday set |
7. Schedule drawer — shared-definition conflict (HTTP 313)
Requires a workout whose plan/definition is shared across schedules. The backend answers the first save with 313 instead of 2xx.
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 7.1 | Conflict surfaces | Save a change to a shared-plan workout | Backend returns 313 (visible in Network); instead of an error, the "Shared workout plan" dialog opens | ||
| 7.2 | Dialog options | Read the dialog | Three actions: Only this schedule, Update the shared plan, Cancel | ||
| 7.3 | Fork a copy | Click Only this schedule | Re-sends to the *_definition_copy endpoint; saves; only this schedule changes | ||
| 7.4 | Edit shared plan | (Fresh conflict) click Update the shared plan | Re-sends to the *_definition_update/schedule_and_definition endpoint; saves; the shared plan changes | ||
| 7.5 | Cancel | (Fresh conflict) click Cancel | Dialog closes; no save performed; drawer stays open with edits intact |
8. Schedule drawer — edit ("edit") — V1→V2 regression focus
Opened by clicking an existing workout chip. Verifies V2 item seeding.
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 8.1 | Open from chip | Click a scheduled workout chip | Drawer opens titled with the workout's name | ||
| 8.2 | Existing items show | Look at the Workout section | The workout's existing exercises render as slot cards — not the "No exercises added yet" empty state | ||
| 8.3 | Options seeded | Check Date / All day / Time / Repeats | All match the saved workout (correct date, all-day vs time, recurring on/off) | ||
| 8.4 | Athletes seeded | Check the Athletes section | The workout's assigned athletes appear as chips | ||
| 8.5 | Save enabled immediately | Observe the Save button on open | Enabled right away (no stuck "Loading workout details…" hint, no perma-disabled Save) | ||
| 8.6 | Edit & save (update) | Change name/items/athletes, click Save | "Saving…" then drawer closes; the chip updates on the calendar immediately | ||
| 8.7 | No 404 on update | Inspect the PUT /workouts/update/schedule response | Returns 2xx (or 313 → §7) — not 404 entityNotFound | ||
| 8.8 | Move to another day | Edit a workout's date to a different month/day, save | Chip disappears from the old day and appears on the new one | ||
| 8.9 | Convert scheduled → recurring | Edit a scheduled workout, turn Repeats weekly on, save | It now appears as a recurring chip on its weekday | ||
| 8.10 | Add an athlete in edit | Open edit, add another athlete, save | Athlete count on the chip increments | ||
| 8.11 | Redundant GET note | Inspect Network on edit-open | (Known) a GET /workouts/{id} fires on open — should not break anything; flag if it errors or blocks the UI |
9. Delete
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 9.1 | Delete button (edit only) | Open a workout in edit mode | A Delete button shows (bottom-left); it is absent in create mode | ||
| 9.2 | Confirm dialog | Click Delete | A confirm dialog "Delete schedule?" with Delete / Cancel appears | ||
| 9.3 | Cancel delete | Click Cancel | Dialog closes; workout unchanged | ||
| 9.4 | Confirm delete | Click Delete in the dialog | DELETE /workouts/{id} fires; drawer closes; the chip disappears immediately | ||
| 9.5 | Delete persists | Refresh / reload | The workout stays gone | ||
| 9.6 | Delete recurring | Delete a recurring workout | It disappears from all its weekday occurrences |
10. Data loading, caching & refresh
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 10.1 | Recurring loads once | On first load, watch Network | A single GET /workouts/recurring fires | ||
| 10.2 | Month range fetch | Watch Network while in Month view | GET /workouts/schedules?from=&to= fires for the focused month (±1 neighbouring months) | ||
| 10.3 | No duplicate month fetch | Scroll back into an already-loaded month | No second fetch for that month (cached) | ||
| 10.4 | Week view fetch | Switch to Week | Schedules fetched for the months spanning the visible week's first/last day | ||
| 10.5 | Day view fetch | Switch to Day | Schedules fetched for the selected day's month + the mini-calendar's month | ||
| 10.6 | Reload button | Click the Refresh (circular-arrows) button | All loaded months + recurring refetch; the icon spins while loading | ||
| 10.7 | Reload disabled while loading | Click Refresh, watch the button | Button is disabled (dimmed) until fetches complete | ||
| 10.8 | Error resilience | (If reproducible) force a failing fetch | Console logs the failure; the calendar doesn't crash; a retry via Refresh recovers |
11. Cross-cutting
| # | Use case | Steps | Expected result | Result | Notes |
|---|---|---|---|---|---|
| 11.1 | No console errors | Click through §1–§10 with Console open | No uncaught errors or React warnings | ||
| 11.2 | Drawer close paths | Close the drawer via backdrop / close control | Drawer dismisses cleanly; calendar interactive again | ||
| 11.3 | Rapid open/close | Open and close the drawer repeatedly | No stuck overlay, no duplicated drawers, no leaked scroll-lock | ||
| 11.4 | Timezone placement | Create a workout near local midnight, verify the day it lands on | It lands on the local calendar day (option-b: UTC instant placed on the user's local day) | ||
| 11.5 | i18n wrapping | Scan visible strings | All user-facing copy is real text (no raw keys / undefined) |
Defect log
| # | Case ref | Severity (blocker/major/minor) | What happened | Expected | Console / Network detail |
|---|---|---|---|---|---|
Known/expected behaviours (not defects)
- Roobert font 404s in dev (
/fonts/Roobert-*.ttf) are expected locally and unrelated to the Planner. - A
GET /workouts/{id}on edit-open (case 8.11) is currently redundant V1-era scaffolding; it is slated for removal but should not affect behaviour. - The Planner is an evolving scaffold (inline components, finite ~40-week scroll window); "infinite" scroll and extracted components are future work, not defects.