diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..abcb72b --- /dev/null +++ b/.claude/settings.json @@ -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\\)\")" + ] + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ee27ec..4f8be43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index f42750b..9de4e5a 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -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 diff --git a/.gitignore b/.gitignore index 4a7f73a..f8a2fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ logs .env .env.* !.env.example + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.nuxtrc b/.nuxtrc new file mode 100644 index 0000000..640f280 --- /dev/null +++ b/.nuxtrc @@ -0,0 +1 @@ +setups.@nuxt/test-utils="4.0.3" \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..35eaaf7 --- /dev/null +++ b/CLAUDE.md @@ -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 `` + visible + `