Skip to content

Commit 345b939

Browse files
authored
Unit and integration tests (#64)
Tests generated via AI via "@test: XXYYZZ" annotations on top of method defs. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> This PR adds a full testing setup across the app and stabilizes the CI pipeline to run it. Highlights: - Adds Vitest, Playwright, and MSW-based test infrastructure, including shared fixtures, mock handlers, setup, and config files. - Introduces extensive unit coverage for utilities and service clients. - Adds broad end-to-end coverage for core flows: sign-in, home, help, dashboard, workspace creation, export, review, settings, editor hosting, team join, and workspace member/team management. - Includes contract validation against a vendored OpenAPI spec for new API traffic. - Updates app pages and components to support the new tests, including added test-outline comments, typing cleanup, form/UX adjustments, and toast-based error/success feedback in several settings and create/export flows. - Refactors export/import/service layers with stronger typing and safer handling of optional IDs, archive contents, and editor/container setup. - Updates CI to run lint, typecheck, unit tests, and E2E tests, while disabling Sentry telemetry during CI runs and ignoring Playwright artifacts in git. - Adds repo guidance in `CLAUDE.md` and related README/config updates for the new testing workflow. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 3b7131d + 2b29b6a commit 345b939

132 files changed

Lines changed: 7125 additions & 595 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(npm ls *)",
5+
"Bash(node -e \"const e=require\\('./node_modules/vue-router/package.json'\\).exports; console.log\\(JSON.stringify\\(Object.keys\\(e\\)\\)\\)\")",
6+
"Bash(node -e \"console.log\\(require\\('./node_modules/vue-router/package.json'\\).version\\)\")",
7+
"Bash(node -e \"console.log\\(require\\('./node_modules/@sentry/nuxt/package.json'\\).version\\)\")"
8+
]
9+
}
10+
}

.github/workflows/ci.yml

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,56 @@ on:
66
pull_request:
77
branches: [ master ]
88

9+
# Cancel any in-progress run for the same branch/PR when a newer commit lands.
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
914
jobs:
10-
run-lint:
15+
ci:
1116
runs-on: ubuntu-latest
1217

1318
steps:
14-
- name: Checkout code
15-
uses: actions/checkout@v4
19+
- name: Checkout code
20+
uses: actions/checkout@v5
21+
22+
- name: Set up Node.js
23+
uses: actions/setup-node@v5
24+
with:
25+
node-version: '24'
26+
cache: 'npm'
27+
28+
# Runs `nuxt prepare` via the postinstall hook, which generates the .nuxt
29+
# types + eslint typegen that both `lint` and `typecheck` rely on.
30+
- name: Install dependencies
31+
run: npm ci
32+
33+
# The quality steps below use `if: ${{ !cancelled() }}` so every check runs
34+
# and reports its own result even when an earlier one fails.
35+
- name: Lint
36+
if: ${{ !cancelled() }}
37+
run: npm run lint
38+
39+
- name: Type check
40+
if: ${{ !cancelled() }}
41+
run: npm run typecheck
42+
43+
- name: Unit tests
44+
if: ${{ !cancelled() }}
45+
run: npm test
1646

17-
- name: Set up Node.js
18-
uses: actions/setup-node@v4
19-
with:
20-
node-version: '24'
47+
- name: Install Playwright browser
48+
if: ${{ !cancelled() }}
49+
run: npx playwright install --with-deps chromium
2150

22-
- name: Install dependencies
23-
run: npm ci
51+
- name: E2E tests
52+
if: ${{ !cancelled() }}
53+
run: npm run test:e2e
2454

25-
- name: Run lint script
26-
run: npm run lint
55+
- name: Upload Playwright report
56+
if: ${{ !cancelled() }}
57+
uses: actions/upload-artifact@v5
58+
with:
59+
name: playwright-report
60+
path: playwright-report/
61+
retention-days: 7

.github/workflows/tag.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
contents: write
1616
steps:
1717
- name: Checkout code
18-
uses: actions/checkout@v4
18+
uses: actions/checkout@v5
1919
with:
2020
fetch-depth: 1
2121
- name: Get the current branch name

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ logs
2222
.env
2323
.env.*
2424
!.env.example
25+
26+
# Playwright
27+
/test-results/
28+
/playwright-report/
29+
/blob-report/
30+
/playwright/.cache/

.nuxtrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
setups.@nuxt/test-utils="4.0.3"

CLAUDE.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introdu
44

55
### ⚠️ Reminder: you must set the tag of the environment you wish to deploy in this repo, then run the deploy workflow in workspaces-stack to deploy to dev, stage or prod.
66

7+
# TODOs
8+
* Remove "any" typing, currently ignored by linter/tsc
9+
710
## Dev Setup
811

912
```zsh

components/AppLogo.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<template>
2-
<img class="app-logo img-fluid" src="~/assets/img/tdei-logo.png" alt="" />
2+
<img
3+
class="app-logo img-fluid"
4+
src="~/assets/img/tdei-logo.png"
5+
alt=""
6+
>
37
</template>
4-

0 commit comments

Comments
 (0)