Skip to main content

User drawer — Manual Test Protocol

A human-driven, click-through script for the portal's Users page and user drawer (apps/portal/src/app/dashboard/users/). 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).

Scope: who appears in the roster, which tabs a user gets, the Summary / Goals / Schedule / Activity panels, and the caching + invalidation behaviour behind them. Behaviour is described as currently implemented — if something differs, that difference is the finding.

Test environment​

ItemValue
App@enode/portal dev server
Start commandNEXT_PUBLIC_API_SERVER=backdev npm run dev (from apps/portal)
URLhttp://localhost:3001/dashboard/users (or :3000)
Accounts needed(A) trainable owner · (B) non-trainable coach · (C) a coach or admin without writeTrainingGoal
Data neededAt least one athlete with a training goal and completed sessions this week; one athlete without a training goal
ToolsDevTools open — Console and Network (filter to the API host)

Before you start: DevTools open throughout. A case passes only if the visible result is correct and the Console is clean and the Network traffic matches what the case says.


1. Roster visibility​

#CaseStepsExpectedResultNotes
1.1You appear when trainableSign in as (A)Your own row is in the table with a "You" badge next to the name
1.2Sole memberOn an account where you are the only visible userThe table shows your one row — not the "Add your athletes" empty state
1.3You disappear when not trainableOpen your own drawer → Details → turn Trainable off → Save → reloadYour row is gone from the roster
1.4Non-trainable coachSign in as (B)Your own row is not in the table
1.5Another owner stays hiddenAs (B) or a coach, on an account whose owner is trainableThat owner is not in the table
1.6Mobile cardNarrow the window below mdYour card shows the "You" badge too
1.7CountsCompare the header "N users in scope" and the table's "Showing x of y"Both include your own row
1.8Not bulk-selectableTick the select-all checkboxYour row is not selected and shows a lock glyph instead of a checkbox; the Delete count excludes you
1.9Empty state survivesSign in to an account with no users besides a non-trainable ownerThe "Add your athletes" first-run empty state renders

2. Which tabs a user gets​

#CaseStepsExpectedResultNotes
2.1AthleteOpen an athleteTabs: Summary · Goals · Details · Schedule · Activity. Opens on Summary
2.2Trainable non-athleteOpen a trainable coach (or yourself as (A))Same five tabs plus Assignments. Opens on Summary
2.3Non-trainable coachOpen a non-trainable coachDetails + Assignments only — no training tabs
2.4Non-trainable facilityOpen a non-trainable facility adminDetails + Assignments only
2.5Toggle mid-editOn a trainable user, Details → turn Trainable off (don't save)The tab strip does not change while the drawer is open
2.6Toggle takes effectSave that change, reopen the drawerThe training tabs are now gone
2.7Create modeToolbar → Create userNo tab strip at all — the form only

3. Opening cost and caching​

Watch the Network tab filtered to the API host for each of these.

#CaseStepsExpectedResultNotes
3.1One profile readHard-reload, open an athleteExactly one GET /users/{id} — not three
3.2Open costSame openOne POST /history/v2/week, one POST /workout_sessions/search, workouts month + recurring (often cached)
3.3Reopen is freeClose and reopen the same athlete within ~15 sNo new stats or session-search calls; the panels render immediately
3.4Tabs are freeClick through all five tabsNo new requests; no loading flash on first click of each
3.5Lazy DOMOpen a drawer and stay on DetailsRequests still fire, but the muscle maps / Lottie icons are not built until you open those tabs
3.6Focus revalidationLeave the drawer open, switch to another app for >15 s, come backStats + activity quietly refetch; the Details form and any staged Goals edits are untouched
3.7No profile clobberType into a Details field, switch away and backYour typing is not overwritten

4. Summary tab​

#CaseStepsExpectedResultNotes
4.1Week captionOpen Summary"This week 4.8 – 10.8" (day.month) at the top, matching Schedule and Activity
4.2No debug controlsLook at the top of the panelNo "Mock data" checkbox, no percentage slider, no "Log data model" button
4.3Cards render—Weekly-goal banner, metric grid, volume completion ring, muscle map, muscle-usage bars
4.4Muscle-usage rows—One row per muscle, category-grouped, with icon, "done / target sets" and a %
4.5CollapseWith >5 muscle rowsA fade + chevron collapses the list; the chevron expands and collapses it
4.6No dataOpen an athlete with no sessions this weekA muted "No summary data for this athlete yet." — not an error
4.7Load failureThrottle to Offline in DevTools, then open an athlete never opened this sessionAn inline error with Try again + Report — not a bare sentence or a permanent skeleton
4.8RetryRestore the network, press Try againThe cards load

5. Goals tab — no goal yet​

#CaseStepsExpectedResultNotes
5.1Setup CTAOpen the athlete without a training goal → GoalsA frosted "Set a training goal" card with a Choose a goal button, floating over a faded, static ghost of the populated panel that fades out toward the bottom. No live map, goal card or rows
5.1aGhost isn't loadingSame viewThe ghost does not pulse — a pulsing backdrop reads as "loading"
5.2First pick writes straight awayPress Choose a goal → pick oneNo confirm dialog; the picker rows disable while it writes; the tab then shows map + goal + rows
5.3No permissionSign in as (C) and open that athlete's GoalsThe same empty state without the button

6. Goals tab — with a goal​

#CaseStepsExpectedResultNotes
6.1Layout orderOpen Goals on an athlete with a goalTop to bottom: Target distribution (map + legend) → Training goal → Muscle goals (rows)
6.2Map header—"Target distribution" with the weekly Total: N sets on the right, and a one-line description below
6.3Legend ranges—Legend reads e.g. "High 14+ sets · Medium 7–13 sets · Low 0–6 sets" — real numbers, no gaps between bands
6.4Ranges are per athleteCompare an athlete training ~20 sets/muscle with one training ~6The thresholds differ — they scale to each athlete's busiest muscle
6.5Map clickLeft-click a muscle on the mapIts target sets +1; the row below updates; the map re-tints
6.6Map right-clickRight-click a muscleIts target sets −1 (floor 0)
6.7Hover tooltipHover a muscleA floating tooltip with the muscle icon, name, target sets and the click hint — anchored on the muscle, not the corner
6.8Row hoverHover a muscle rowThat muscle outlines on the map above
6.9Sets inputType into a row's set fieldClamped to 0–30; the total and map update
6.10Zone sliderDrag a row's quality splitThe three percentages update and sum to 100
6.11SaveChange a target → SaveThe drawer persists; reopen shows the new value
6.12DiscardChange a target → close the drawer without saving → reopenThe old values are back

7. Goals tab — the goal ↔ targets relationship​

#CaseStepsExpectedResultNotes
7.1No drift, no hintOpen an athlete whose targets match their goalNo "Targets differ…" line and no reset action
7.2Drift appearsChange one muscle's target sets"Targets differ from this goal's defaults." + Reset to goal defaults appears immediately
7.3Zone drift countsInstead change only a zone sliderThe same line appears
7.4Reset worksPress Reset to goal defaultsTargets snap back and the line disappears in the same interaction
7.5Reset is localReset → close without saving → reopenThe athlete's saved targets are unchanged
7.6Change confirmsWith a goal already set, pick a different goalA confirm dialog appears; its copy says targets are adjusted, and does not claim they will be lost
7.7Change appliesConfirm itTargets update, the drawer stays open, the goal card shows the new goal
7.8Summary followsImmediately switch to the Summary tabThe per-muscle targets reflect the new goal. It must not sit on skeletons — this was a real bug
7.9CancelPick a different goal → CancelNothing changes
7.10First-goal checkOn an athlete you just gave their first goal, look for the drift lineIf it appears immediately, the backend adjusted rather than copied the template — note it, it's worth knowing

8. Schedule and Activity tabs​

#CaseStepsExpectedResultNotes
8.1Week captionOpen ScheduleSame "This week d.m – d.m" caption as Summary and Activity
8.2Workouts—This athlete's workouts, split "Earlier this week" (dimmed) / "Upcoming", with weekday headings
8.3EmptyAn athlete with no workoutsDumbbell icon in a platinum tile + "No workouts scheduled…" + a Schedule a workout button, on a frosted card over a static ghost of workout cards
8.4Schedule onePress itThe schedule drawer opens over the top with this athlete preselected
8.5Activity listOpen ActivitySession cards for this week, newest first
8.6Right athletePick an athlete whose name is a substring of another'sOnly their sessions appear (the search is by name, filtered by id)
8.7Empty weekAn athlete with no sessionsClock icon in a platinum tile + "No completed sessions this week." on a frosted card over a static ghost of session cards
8.7aConsistent heightCompare 5.1, 8.3 and 8.7 side by side, and against a picker (open the coach picker with no coaches)All four message cards sit at the same height and use the same icon tile
8.8Inspect ProfilePress "Inspect Profile →" on a cardThe performance sub-drawer pushes on top; back returns to Activity
8.9No debug buttonBoth tabsNo "Log data model" button anywhere

9. Write side-effects and invalidation​

#CaseStepsExpectedResultNotes
9.1Save then reopenEdit muscle targets → Save → reopen immediatelySummary shows the new targets, not cached old ones
9.2Fetch-before-editOpen a drawer with the network throttledSave is disabled until the profile read lands
9.3Save failureGo offline, press SaveAn error surface with Try again — the drawer stays open, edits intact
9.4ArchiveArchive a userThe Status pill flips, the row dims, a toast confirms
9.5DeleteDelete a userConfirm dialog → row disappears → no stale data if a user with that id reappears
9.6Roster freshensSave a name changeThe table row behind the drawer shows the new name without a manual reload
9.7Save your own row as the ownerOpen your own drawer (a trainable owner is in their own roster), edit the name and drag muscle targets → SaveSave is present and works — self-editing runs on writeSelf, not a role tier
9.8No self-archive, no self-deleteOn that same drawer, look at the header actionsNeither Archive nor Delete is offered — both stay tier-only, and owner is no tier
9.9Your access survives the saveAfter 9.7, check GET /users/{you}accessToUserIDs is still null — the drawer omits the field when there is no Assignments tab, so an owner's all-access is never narrowed to []
9.10A coach saving themselvesAs a coach with writeUserTrainer, open your own drawer and saveUnchanged — the tier grant still covers it, with or without writeSelf

10. Unsaved-changes guard​

The drawer never closes on a backdrop tap, but the header X, Escape and hardware Back all can — each must ask before discarding staged edits. Note what counts: a training-goal change is written immediately, so it is not unsaved; an avatar pick is staged until Save, so it is.

#CaseStepsExpectedResultNotes
10.1Clean closeOpen a user, change nothing, press XCloses immediately, no dialog
10.2Keep editingChange the name, press X → Keep editingDialog closes, drawer stays open, the edit is intact
10.3DiscardChange the name, press X → DiscardDrawer closes; reopen shows the original value
10.4EscapeChange the name, press EscapeThe dialog appears — not a silent close
10.5Escape dismisses the dialogWith the dialog open, press EscapeThe dialog closes and stays closed (both it and the drawer listen on the document — without the guard it reopens instantly)
10.6BackChange the name, press browser/hardware BackThe dialog appears
10.7Save doesn't promptChange the name → SaveThe drawer closes with no dialog
10.8Delete / archive don't promptDelete a user; archive a userBoth close with no discard dialog
10.9Goals edits countGoals tab → drag a set count → press XThe dialog appears
10.10Training goal does not countGoals tab → change the training goal → confirm → press X without touching anything elseNo dialog. That write already persisted
10.11Avatar countsPick a photo → press XThe dialog appears (the picture is staged, not uploaded)
10.12Assignments countAssignments tab → add or remove an assignee → press XThe dialog appears
10.13Right-click can't cost you the pageWith staged muscle-target edits, right-click the muscle map: on a muscle, on the plain silhouette, and in the gap between the two figuresNo browser context menu in any of the three spots — its Back entry sits one misclick from discarding the edits
10.13Sub-views pop firstOpen the coach picker → press EscapeThe picker pops, the drawer stays open, no discard dialog
10.14Guard resets per openEdit → Discard → reopen the same user → press XCloses immediately — the previous open's dirty flag must not leak
10.15Roster row editorRoster upload → open a row → change a field → closeThe dialog appears; the wizard's own "Discard this import?" still works separately
10.16Migration editorMigration → open a person → change a field → closeThe dialog appears

11. Performance​

#CaseStepsExpectedResultNotes
11.1No re-render stormReact DevTools Profiler recording → type in a Details field with all tabs previously openedThe hidden Summary / Goals / Schedule / Activity panels show zero renders
11.2Typing stays smoothType quickly into a Details field on an athlete with many sessionsNo input lag

Known gaps​

These are expected as of this protocol — record them only if the behaviour differs from what's described here.

  • A trainable owner on a brand-new account never sees the onboarding empty state, because their own row makes the roster non-empty. The header's Upload roster / Create user actions still work.
  • The activity search filters by athlete NAME, then re-filters by id client-side, because the endpoint has no id filter. An athlete with a blank name loads nothing rather than searching the whole account.
  • Past workouts say nothing about completion. Sessions carry no workout id, so the Schedule tab only dims past days.