|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Guidance for working in this repo. Focused on the test infrastructure and |
| 4 | +conventions established for it; see `README.md` for app setup. |
| 5 | + |
| 6 | +## Project |
| 7 | + |
| 8 | +- **TDEI Workspaces frontend** — Nuxt 4 / Vue 3 **SPA** (`ssr: false`), |
| 9 | + bootstrap-vue-next UI, maplibre-gl + Leaflet maps. |
| 10 | +- API access is via class-based HTTP clients in `services/` (all extend |
| 11 | + `BaseHttpClient` in [services/http.ts](services/http.ts), which wraps `fetch`). |
| 12 | + Clients are constructed in [services/index.ts](services/index.ts) from |
| 13 | + `import.meta.env.VITE_*` URLs. |
| 14 | +- Types in `types/`; the workspace API shapes mirror the backend OpenAPI spec |
| 15 | + (integer `id`, `title` not `name`, uuid `tdeiProjectGroupId`, etc.). |
| 16 | + |
| 17 | +## Testing |
| 18 | + |
| 19 | +Stack: **Vitest** (unit) + **Playwright** (e2e) + **MSW** (shared API stubs) + |
| 20 | +an **OpenAPI contract validator**. Config: [vitest.config.ts](vitest.config.ts), |
| 21 | +[playwright.config.ts](playwright.config.ts). |
| 22 | + |
| 23 | +### Run |
| 24 | + |
| 25 | +```bash |
| 26 | +./run-tests.sh # eslint (test/ only) + unit + e2e |
| 27 | +./run-tests.sh unit # vitest only |
| 28 | +./run-tests.sh e2e # playwright only |
| 29 | +./run-tests.sh --update-snapshots # extra args pass through to playwright |
| 30 | +``` |
| 31 | + |
| 32 | +The e2e suite is written **to the `@test` outline comments (the spec), not the |
| 33 | +current code**, so a new e2e failure flags a real app/spec divergence. It |
| 34 | +currently passes aside from intentionally-skipped (`test.fixme`) flows. Unit is |
| 35 | +always green. |
| 36 | + |
| 37 | +### Layout |
| 38 | + |
| 39 | +``` |
| 40 | +test/ |
| 41 | + setup.ts # MSW server lifecycle for vitest |
| 42 | + mocks/ |
| 43 | + fixtures.ts # canned API data — SINGLE SOURCE OF TRUTH (spec-shaped) |
| 44 | + handlers.ts # MSW http handlers (vitest) |
| 45 | + server.ts # setupServer(...handlers) |
| 46 | + contract/openapi.json # vendored backend OpenAPI spec |
| 47 | + unit/ # vitest: services/ + util/ (default env happy-dom) |
| 48 | + e2e/ |
| 49 | + fixtures.ts # Playwright test fixture + seed helpers |
| 50 | + contract.ts # recordContract(page) -> OpenAPI validator |
| 51 | + *.spec.ts # one per page; *.aria.yml + *.png snapshots alongside |
| 52 | +``` |
| 53 | + |
| 54 | +### Conventions |
| 55 | + |
| 56 | +- **Unit tests** default to the **happy-dom** environment (several `services/` |
| 57 | + modules construct `DOMParser`/`XMLSerializer` at import time; bare node lacks |
| 58 | + them). A test can opt into the full Nuxt env with `// @vitest-environment nuxt`. |
| 59 | +- **e2e auth/stubs:** import from `./fixtures` — `test`, `expect`, |
| 60 | + `seedAuthenticatedSession(page)` (call FIRST for any authed page; user is |
| 61 | + "Tester"/`USER_ID`), `seedProjectGroupSelection(page, {id, name})`. Import |
| 62 | + canned data + ids (`aWorkspace`, `myWorkspaces`, `projectGroups`, |
| 63 | + `PROJECT_GROUP_ID`, `USER_ID`, `TEST_API_BASE`) from `../mocks/fixtures`. |
| 64 | +- **API stubbing:** in e2e ALL API base URLs point at host `http://api.test/` |
| 65 | + (set in `playwright.config.ts` `webServer.env`). Stub **every** endpoint a page |
| 66 | + hits or the page 500s on the failed fetch. New-API response bodies **must be |
| 67 | + spec-conformant** (the contract validator checks them). |
| 68 | +- **Contract testing:** for a page's "validate API calls match the Swagger spec" |
| 69 | + outline, `const c = recordContract(page)` before navigating, drive the page, |
| 70 | + then `expect(c.violations()).toEqual([])`. Only `api.test` new-API calls are |
| 71 | + checked (TDEI/OSM hosts are out of spec scope). |
| 72 | +- **Snapshots:** use **ARIA** snapshots (`toMatchAriaSnapshot()`), not pixel |
| 73 | + screenshots — they're text-based and cross-platform stable. Don't snapshot |
| 74 | + volatile content (blob URLs, transient toasts). Regenerate with |
| 75 | + `--update-snapshots`. |
| 76 | +- **Lint:** the stylistic config requires **semicolons** and **no trailing |
| 77 | + commas** (set in `nuxt.config.ts` `eslint.config.stylistic`). New code must |
| 78 | + comply. App source has a large pre-existing backlog of other lint errors, so |
| 79 | + `run-tests.sh` lints `test/` only. |
| 80 | + |
| 81 | +### The `@test` comment workflow |
| 82 | + |
| 83 | +Pages carry `@test e2e:` / `@test unit:` outline comments at the top describing |
| 84 | +intended behavior. Tests are generated **to the comment (the spec), not the |
| 85 | +current code** — if the code diverges, the test is left RED to document the bug. |
| 86 | +Don't soften assertions to make buggy code pass. |
| 87 | + |
| 88 | +## Gotchas (learned the hard way) |
| 89 | + |
| 90 | +- **OSM URLs include `/api/0.6/`.** The OSM client base is |
| 91 | + `http://api.test/osm/api/0.6/`. Route globs must be e.g. |
| 92 | + `**/osm/api/0.6/changesets.json**`. Also: glob `*` matches a single path |
| 93 | + segment, so `**/osm/changeset/*/upload` will NOT match |
| 94 | + `osm/api/0.6/changeset/777/upload`. |
| 95 | +- **`DatasetTypeRadio` is a Bootstrap `.btn-check`** (hidden `<input>` + visible |
| 96 | + `<label>`). `.check()` times out — click the label text instead |
| 97 | + (`getByText('GTFS Pathways').click()`). |
| 98 | +- **`app-icon` renders material-icon ligature text** (e.g. "menu_book") into |
| 99 | + element text/accessible names. Match headings/buttons by regex name or stable |
| 100 | + class/href, not exact accessible name. |
| 101 | +- **Playwright `getByRole({ name })` is substring/normalized** unless |
| 102 | + `exact: true`. "Delete this workspace" also matches "...want to delete this |
| 103 | + workspace" — anchor or use `exact: true`. |
| 104 | +- **`selectOption` needs a string/value/index**, not a regex object. |
| 105 | +- **maplibre-gl needs WebGL**, unreliable headless. Pages with a map: stub the |
| 106 | + bbox/data endpoints empty so the map shows its empty state instead of |
| 107 | + initializing; assert DOM, not the GL canvas. The dashboard auto-selects a |
| 108 | + workspace when a project group has workspaces (→ mounts the map), so the |
| 109 | + empty-state tests use empty `workspaces/mine`. |
| 110 | +- **Nuxt dev server compiles routes lazily**, so cold parallel hits can be slow; |
| 111 | + `playwright.config.ts` sets a 10s `expect` timeout to absorb it. Run e2e |
| 112 | + against a production build if you want pre-compiled routes. |
| 113 | +- **Playwright route precedence is most-recently-registered-first** — register |
| 114 | + per-test override routes AFTER (or in a way that wins over) shared stub helpers. |
| 115 | + |
| 116 | +## Permission Structure |
| 117 | + |
| 118 | +Project Group Admin ("POC") |
| 119 | +* Superuser for the whole project group |
| 120 | +* Implied by "poc" role in TDEI |
| 121 | + |
| 122 | +Lead/Owner/Workspace Admin |
| 123 | +* Admin-level access for a workspace |
| 124 | +* Configures workspace settings and quest definitions |
| 125 | +* Assigns users to workspace teams |
| 126 | +* Ability to merge changes from other workspace |
| 127 | +* Exports data to TDEI (with appropriate TDEI core roles) |
| 128 | +* Granted by Workspaces setting. |
| 129 | + |
| 130 | +Contributor/Data Generator |
| 131 | +* Modifies workspace data--all modifications need validation |
| 132 | +* Implied by membership in TDEI project group |
| 133 | + |
| 134 | +Validator |
| 135 | +* Modifies workspace data and approves changes from contributors |
| 136 | +* Granted by Workspaces setting. |
| 137 | + |
| 138 | +Viewer/Member/Everyone Else |
| 139 | +* Read-only access to workspace data |
| 140 | +* With express TDEI sign-up, the need for this access level diminishes greatly |
| 141 | +* Granted by Workspaces setting. |
| 142 | + |
| 143 | +## What Each Role Can Do |
| 144 | + |
| 145 | +Below is the *intended* capability matrix annotated with **what the frontend |
| 146 | +actually enforces** (validated against the code on 2026-06-25). The frontend |
| 147 | +recognizes only three workspace roles — `lead | validator | contributor` |
| 148 | +([types/workspaces.ts](types/workspaces.ts)) — exposed via |
| 149 | +[composables/useWorkspaceRole.ts](composables/useWorkspaceRole.ts), which |
| 150 | +provides `isLead` and `isValidator` only (and **`isValidator` is true for leads |
| 151 | +too**). There is no `isContributor`/`isPoc` helper at the workspace-role layer. |
| 152 | + |
| 153 | +Project Lead |
| 154 | +* Edit Metadata — ✅ enforced (`isLead`, [General.vue](components/settings/panel/General.vue)) |
| 155 | +* Edit Longform Quests — ⚠️ enforced but also requires the workspace be |
| 156 | + published for external apps: `appControlsDisabled = !isLead || !externalAppAccess` |
| 157 | + ([Apps.vue](components/settings/panel/Apps.vue)) |
| 158 | +* Toggle App-Enabled Flag — ✅ enforced (`isLead`, [Apps.vue](components/settings/panel/Apps.vue)) |
| 159 | +* Delete Workspace — ✅ enforced (`isLead`, [Delete.vue](components/settings/panel/Delete.vue)) |
| 160 | +* Define User Teams — ✅ enforced (`isLead`, [teams/index.vue](pages/workspace/[id]/settings/teams/index.vue), [MembersDialog.vue](components/teams/MembersDialog.vue)) |
| 161 | +* Define Groups or Roles — ✅ enforced (`isLead`, [members.vue](pages/workspace/[id]/settings/members.vue)) |
| 162 | +* Export to TDEI — ❌ **not gated by the workspace `lead` role.** Eligibility is |
| 163 | + `pg.roles.includes('poc') || pg.roles.includes('<type>_data_generator')` — |
| 164 | + i.e. **TDEI project-group roles**, not the workspace role |
| 165 | + ([export/tdei.vue](pages/workspace/[id]/export/tdei.vue)). A Lead without one |
| 166 | + of those TDEI roles cannot export; a non-lead with `poc` can. |
| 167 | +* Validate Changeset — ✅ enforced (`isValidator`, [review/Toolbar.vue](components/review/Toolbar.vue)) |
| 168 | +* Move Workspace from Project Group to Project Group — ❌ **not implemented.** |
| 169 | + No `move`/`transfer`/`changeProjectGroup` code exists anywhere in the frontend. |
| 170 | +* Edit POSM Element — ✅ available, but **not role-gated** (see note below) |
| 171 | + |
| 172 | +Validator |
| 173 | +* Export to TDEI — ❌ same as above — gated on TDEI `poc`/`data_generator`, |
| 174 | + never on the `validator` workspace role |
| 175 | +* Validate Changeset — ✅ enforced (`isValidator` covers `validator`) |
| 176 | +* Edit POSM Element — ✅ available, but **not role-gated** (see note below) |
| 177 | + |
| 178 | +Contributor |
| 179 | +* Edit POSM Element — ✅ available, but **not role-gated** (see note below) |
| 180 | + |
| 181 | +Authenticated User With PG/Workspace Association |
| 182 | +* Edit POSM Element — ✅ available, but **not role-gated** (see note below) |
| 183 | + |
| 184 | +**Note on "Edit POSM Element":** the editor page |
| 185 | +([edit.vue](pages/workspace/[id]/edit.vue)) has **zero role gating** — the only |
| 186 | +gate is `auth.global.ts` (authentication, not role), so any authenticated user |
| 187 | +who reaches it loads the editor. The "contributor edits need validation" and |
| 188 | +"viewer is read-only" distinctions are **not enforced in this frontend**; they |
| 189 | +would have to be enforced by the backend / changeset-validation flow. |
| 190 | + |
| 191 | +## Test status |
| 192 | + |
| 193 | +The e2e suite currently passes aside from **6 intentionally-skipped tests**: 6 |
| 194 | +`test.fixme` dashboard flows blocked by maplibre/external-editor rendering. Some |
| 195 | +heavy authed pages (e.g. `settings/members`) can flake under high parallelism |
| 196 | +(the lazy-compile issue above) — run serially (`--workers=1`) for deterministic |
| 197 | +results. |
0 commit comments