Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(npm ls *)",
"Bash(node -e \"const e=require\\('./node_modules/vue-router/package.json'\\).exports; console.log\\(JSON.stringify\\(Object.keys\\(e\\)\\)\\)\")",
"Bash(node -e \"console.log\\(require\\('./node_modules/vue-router/package.json'\\).version\\)\")",
"Bash(node -e \"console.log\\(require\\('./node_modules/@sentry/nuxt/package.json'\\).version\\)\")"
]
}
}
57 changes: 46 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,56 @@ on:
pull_request:
branches: [ master ]

# Cancel any in-progress run for the same branch/PR when a newer commit lands.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
run-lint:
ci:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: '24'
cache: 'npm'

# Runs `nuxt prepare` via the postinstall hook, which generates the .nuxt
# types + eslint typegen that both `lint` and `typecheck` rely on.
- name: Install dependencies
run: npm ci

# The quality steps below use `if: ${{ !cancelled() }}` so every check runs
# and reports its own result even when an earlier one fails.
- name: Lint
if: ${{ !cancelled() }}
run: npm run lint

- name: Type check
if: ${{ !cancelled() }}
run: npm run typecheck

- name: Unit tests
if: ${{ !cancelled() }}
run: npm test

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

- name: Install dependencies
run: npm ci
- name: E2E tests
if: ${{ !cancelled() }}
run: npm run test:e2e

- name: Run lint script
run: npm run lint
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v5
with:
name: playwright-report
path: playwright-report/
retention-days: 7
Comment on lines +15 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Declare minimal GITHUB_TOKEN permissions explicitly.

This workflow only needs read access to contents plus artifact upload, but without a permissions: block it inherits the repository default token scope. Locking that down materially reduces the impact of any compromised test step.

Suggested hardening
+permissions:
+  contents: read
+
 jobs:
   ci:
     runs-on: ubuntu-latest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
# Runs `nuxt prepare` via the postinstall hook, which generates the .nuxt
# types + eslint typegen that both `lint` and `typecheck` rely on.
- name: Install dependencies
run: npm ci
- name: Run lint script
run: npm run lint
# The quality steps below use `if: ${{ !cancelled() }}` so every check runs
# and reports its own result even when an earlier one fails.
- name: Lint
if: ${{ !cancelled() }}
run: npm run lint
- name: Type check
if: ${{ !cancelled() }}
run: npm run typecheck
- name: Unit tests
if: ${{ !cancelled() }}
run: npm test
- name: Install Playwright browser
if: ${{ !cancelled() }}
run: npx playwright install --with-deps chromium
- name: E2E tests
if: ${{ !cancelled() }}
run: npm run test:e2e
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
# Runs `nuxt prepare` via the postinstall hook, which generates the .nuxt
# types + eslint typegen that both `lint` and `typecheck` rely on.
- name: Install dependencies
run: npm ci
# The quality steps below use `if: ${{ !cancelled() }}` so every check runs
# and reports its own result even when an earlier one fails.
- name: Lint
if: ${{ !cancelled() }}
run: npm run lint
- name: Type check
if: ${{ !cancelled() }}
run: npm run typecheck
- name: Unit tests
if: ${{ !cancelled() }}
run: npm test
- name: Install Playwright browser
if: ${{ !cancelled() }}
run: npx playwright install --with-deps chromium
- name: E2E tests
if: ${{ !cancelled() }}
run: npm run test:e2e
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 14-15: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 10-57: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[error] 15-15: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 52-52: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 10 - 56, The CI workflow currently
relies on the default GITHUB_TOKEN scope; add an explicit top-level permissions
block to the ci job/workflow so only the minimal access needed is granted,
keeping checkout and artifact upload working while reducing token privileges.
Update the ci workflow definition near the existing actions/checkout,
actions/setup-node, and actions/upload-artifact steps to declare read-only
contents access plus any artifact-related permission required by the upload
step, and keep the rest of the job unchanged.

Source: Linters/SAST tools

2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Get the current branch name
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ logs
.env
.env.*
!.env.example

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
1 change: 1 addition & 0 deletions .nuxtrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
setups.@nuxt/test-utils="4.0.3"
197 changes: 197 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# CLAUDE.md

Guidance for working in this repo. Focused on the test infrastructure and
conventions established for it; see `README.md` for app setup.

## Project

- **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 extend
`BaseHttpClient` in [services/http.ts](services/http.ts), which wraps `fetch`).
Clients are constructed in [services/index.ts](services/index.ts) from
`import.meta.env.VITE_*` URLs.
- Types in `types/`; the workspace API shapes mirror the backend OpenAPI spec
(integer `id`, `title` not `name`, uuid `tdeiProjectGroupId`, etc.).

## Testing

Stack: **Vitest** (unit) + **Playwright** (e2e) + **MSW** (shared API stubs) +
an **OpenAPI contract validator**. Config: [vitest.config.ts](vitest.config.ts),
[playwright.config.ts](playwright.config.ts).

### Run

```bash
./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 playwright
```

The 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.

### Layout

```
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
```

### Conventions

- **Unit tests** default to the **happy-dom** environment (several `services/`
modules construct `DOMParser`/`XMLSerializer` at 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 in `playwright.config.ts` `webServer.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,
then `expect(c.violations()).toEqual([])`. Only `api.test` new-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.ts` `eslint.config.stylistic`). New code must
comply. App source has a large pre-existing backlog of other lint errors, so
`run-tests.sh` lints `test/` only.

### The `@test` comment workflow

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.

## Gotchas (learned the hard way)

- **OSM URLs include `/api/0.6/`.** The OSM client base is
`http://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/*/upload` will NOT match
`osm/api/0.6/changeset/777/upload`.
- **`DatasetTypeRadio` is a Bootstrap `.btn-check`** (hidden `<input>` + visible
`<label>`). `.check()` times out — click the label text instead
(`getByText('GTFS Pathways').click()`).
- **`app-icon` renders 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** unless
`exact: true`. "Delete this workspace" also matches "...want to delete this
workspace" — anchor or use `exact: true`.
- **`selectOption` needs 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.ts` sets a 10s `expect` timeout 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.

## Permission Structure

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.

## What Each Role Can Do

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](types/workspaces.ts)) — exposed via
[composables/useWorkspaceRole.ts](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](components/settings/panel/General.vue))
* Edit Longform Quests — ⚠️ enforced but also requires the workspace be
published for external apps: `appControlsDisabled = !isLead || !externalAppAccess`
([Apps.vue](components/settings/panel/Apps.vue))
* Toggle App-Enabled Flag — ✅ enforced (`isLead`, [Apps.vue](components/settings/panel/Apps.vue))
* Delete Workspace — ✅ enforced (`isLead`, [Delete.vue](components/settings/panel/Delete.vue))
* Define User Teams — ✅ enforced (`isLead`, [teams/index.vue](pages/workspace/[id]/settings/teams/index.vue), [MembersDialog.vue](components/teams/MembersDialog.vue))
* Define Groups or Roles — ✅ enforced (`isLead`, [members.vue](pages/workspace/[id]/settings/members.vue))
* Export to TDEI — ❌ **not gated by the workspace `lead` role.** Eligibility is
`pg.roles.includes('poc') || pg.roles.includes('<type>_data_generator')` —
i.e. **TDEI project-group roles**, not the workspace role
([export/tdei.vue](pages/workspace/[id]/export/tdei.vue)). A Lead without one
of those TDEI roles cannot export; a non-lead with `poc` can.
* Validate Changeset — ✅ enforced (`isValidator`, [review/Toolbar.vue](components/review/Toolbar.vue))
* Move Workspace from Project Group to Project Group — ❌ **not implemented.**
No `move`/`transfer`/`changeProjectGroup` code 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 the `validator` workspace role
* Validate Changeset — ✅ enforced (`isValidator` covers `validator`)
* 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](pages/workspace/[id]/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.

## Test status

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.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introdu

### ⚠️ 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.

# TODOs
* Remove "any" typing, currently ignored by linter/tsc

## Dev Setup

```zsh
Expand Down
7 changes: 5 additions & 2 deletions components/AppLogo.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<template>
<img class="app-logo img-fluid" src="~/assets/img/tdei-logo.png" alt="" />
<img
class="app-logo img-fluid"
src="~/assets/img/tdei-logo.png"
alt=""
>
</template>

Loading
Loading