Guidance for working in this repo. Focused on the test infrastructure and
conventions established for it; see README.md for app setup.
- TDEI Workspaces frontend — Nuxt 4 / Vue 3 SPA (
ssr: false), bootstrap-vue-next UI, maplibre-gl + Leaflet maps. - API access is via class-based HTTP clients in
services/(all extendBaseHttpClientin services/http.ts, which wrapsfetch). Clients are constructed in services/index.ts fromimport.meta.env.VITE_*URLs. - Types in
types/; the workspace API shapes mirror the backend OpenAPI spec (integerid,titlenotname, uuidtdeiProjectGroupId, etc.).
Stack: Vitest (unit) + Playwright (e2e) + MSW (shared API stubs) + an OpenAPI contract validator. Config: vitest.config.ts, playwright.config.ts.
./run-tests.sh # eslint (test/ only) + unit + e2e
./run-tests.sh unit # vitest only
./run-tests.sh e2e # playwright only
./run-tests.sh --update-snapshots # extra args pass through to playwrightThe e2e suite is written to the @test outline comments (the spec), not the
current code, so a new e2e failure flags a real app/spec divergence. It
currently passes aside from intentionally-skipped (test.fixme) flows. Unit is
always green.
test/
setup.ts # MSW server lifecycle for vitest
mocks/
fixtures.ts # canned API data — SINGLE SOURCE OF TRUTH (spec-shaped)
handlers.ts # MSW http handlers (vitest)
server.ts # setupServer(...handlers)
contract/openapi.json # vendored backend OpenAPI spec
unit/ # vitest: services/ + util/ (default env happy-dom)
e2e/
fixtures.ts # Playwright test fixture + seed helpers
contract.ts # recordContract(page) -> OpenAPI validator
*.spec.ts # one per page; *.aria.yml + *.png snapshots alongside
- Unit tests default to the happy-dom environment (several
services/modules constructDOMParser/XMLSerializerat import time; bare node lacks them). A test can opt into the full Nuxt env with// @vitest-environment nuxt. - e2e auth/stubs: import from
./fixtures—test,expect,seedAuthenticatedSession(page)(call FIRST for any authed page; user is "Tester"/USER_ID),seedProjectGroupSelection(page, {id, name}). Import canned data + ids (aWorkspace,myWorkspaces,projectGroups,PROJECT_GROUP_ID,USER_ID,TEST_API_BASE) from../mocks/fixtures. - API stubbing: in e2e ALL API base URLs point at host
http://api.test/(set inplaywright.config.tswebServer.env). Stub every endpoint a page hits or the page 500s on the failed fetch. New-API response bodies must be spec-conformant (the contract validator checks them). - Contract testing: for a page's "validate API calls match the Swagger spec"
outline,
const c = recordContract(page)before navigating, drive the page, thenexpect(c.violations()).toEqual([]). Onlyapi.testnew-API calls are checked (TDEI/OSM hosts are out of spec scope). - Snapshots: use ARIA snapshots (
toMatchAriaSnapshot()), not pixel screenshots — they're text-based and cross-platform stable. Don't snapshot volatile content (blob URLs, transient toasts). Regenerate with--update-snapshots. - Lint: the stylistic config requires semicolons and no trailing
commas (set in
nuxt.config.tseslint.config.stylistic). New code must comply. App source has a large pre-existing backlog of other lint errors, sorun-tests.shlintstest/only.
Pages carry @test e2e: / @test unit: outline comments at the top describing
intended behavior. Tests are generated to the comment (the spec), not the
current code — if the code diverges, the test is left RED to document the bug.
Don't soften assertions to make buggy code pass.
- OSM URLs include
/api/0.6/. The OSM client base ishttp://api.test/osm/api/0.6/. Route globs must be e.g.**/osm/api/0.6/changesets.json**. Also: glob*matches a single path segment, so**/osm/changeset/*/uploadwill NOT matchosm/api/0.6/changeset/777/upload. DatasetTypeRadiois a Bootstrap.btn-check(hidden<input>+ visible<label>)..check()times out — click the label text instead (getByText('GTFS Pathways').click()).app-iconrenders material-icon ligature text (e.g. "menu_book") into element text/accessible names. Match headings/buttons by regex name or stable class/href, not exact accessible name.- Playwright
getByRole({ name })is substring/normalized unlessexact: true. "Delete this workspace" also matches "...want to delete this workspace" — anchor or useexact: true. selectOptionneeds a string/value/index, not a regex object.- maplibre-gl needs WebGL, unreliable headless. Pages with a map: stub the
bbox/data endpoints empty so the map shows its empty state instead of
initializing; assert DOM, not the GL canvas. The dashboard auto-selects a
workspace when a project group has workspaces (→ mounts the map), so the
empty-state tests use empty
workspaces/mine. - Nuxt dev server compiles routes lazily, so cold parallel hits can be slow;
playwright.config.tssets a 10sexpecttimeout to absorb it. Run e2e against a production build if you want pre-compiled routes. - Playwright route precedence is most-recently-registered-first — register per-test override routes AFTER (or in a way that wins over) shared stub helpers.
Project Group Admin ("POC")
- Superuser for the whole project group
- Implied by "poc" role in TDEI
Lead/Owner/Workspace Admin
- Admin-level access for a workspace
- Configures workspace settings and quest definitions
- Assigns users to workspace teams
- Ability to merge changes from other workspace
- Exports data to TDEI (with appropriate TDEI core roles)
- Granted by Workspaces setting.
Contributor/Data Generator
- Modifies workspace data--all modifications need validation
- Implied by membership in TDEI project group
Validator
- Modifies workspace data and approves changes from contributors
- Granted by Workspaces setting.
Viewer/Member/Everyone Else
- Read-only access to workspace data
- With express TDEI sign-up, the need for this access level diminishes greatly
- Granted by Workspaces setting.
Below is the intended capability matrix annotated with what the frontend
actually enforces (validated against the code on 2026-06-25). The frontend
recognizes only three workspace roles — lead | validator | contributor
(types/workspaces.ts) — exposed via
composables/useWorkspaceRole.ts, which
provides isLead and isValidator only (and isValidator is true for leads
too). There is no isContributor/isPoc helper at the workspace-role layer.
Project Lead
- Edit Metadata — ✅ enforced (
isLead, General.vue) - Edit Longform Quests —
⚠️ enforced but also requires the workspace be published for external apps:appControlsDisabled = !isLead || !externalAppAccess(Apps.vue) - Toggle App-Enabled Flag — ✅ enforced (
isLead, Apps.vue) - Delete Workspace — ✅ enforced (
isLead, Delete.vue) - Define User Teams — ✅ enforced (
isLead, teams/index.vue, MembersDialog.vue) - Define Groups or Roles — ✅ enforced (
isLead, members.vue) - Export to TDEI — ❌ not gated by the workspace
leadrole. Eligibility ispg.roles.includes('poc') || pg.roles.includes('<type>_data_generator')— i.e. TDEI project-group roles, not the workspace role (export/tdei.vue). A Lead without one of those TDEI roles cannot export; a non-lead withpoccan. - Validate Changeset — ✅ enforced (
isValidator, review/Toolbar.vue) - Move Workspace from Project Group to Project Group — ❌ not implemented.
No
move/transfer/changeProjectGroupcode exists anywhere in the frontend. - Edit POSM Element — ✅ available, but not role-gated (see note below)
Validator
- Export to TDEI — ❌ same as above — gated on TDEI
poc/data_generator, never on thevalidatorworkspace role - Validate Changeset — ✅ enforced (
isValidatorcoversvalidator) - Edit POSM Element — ✅ available, but not role-gated (see note below)
Contributor
- Edit POSM Element — ✅ available, but not role-gated (see note below)
Authenticated User With PG/Workspace Association
- Edit POSM Element — ✅ available, but not role-gated (see note below)
Note on "Edit POSM Element": the editor page
(edit.vue) has zero role gating — the only
gate is auth.global.ts (authentication, not role), so any authenticated user
who reaches it loads the editor. The "contributor edits need validation" and
"viewer is read-only" distinctions are not enforced in this frontend; they
would have to be enforced by the backend / changeset-validation flow.
The e2e suite currently passes aside from 6 intentionally-skipped tests: 6
test.fixme dashboard flows blocked by maplibre/external-editor rendering. Some
heavy authed pages (e.g. settings/members) can flake under high parallelism
(the lazy-compile issue above) — run serially (--workers=1) for deterministic
results.