Add unit, integration and e2e test suites

tests/{unit,integration,e2e}/, at the repo root (not under frontend/,
since e2e exercises the frontend and PocketBase together) — all
against real code rather than mocks:

- unit/: Vitest (nuxt environment) — pure functions in server/utils
  and shared/utils, plus components/composables via
  @nuxt/test-utils/runtime's mountSuspended.
- integration/: Vitest (node environment) — @nuxt/test-utils/e2e's
  setup() builds and runs the real Nitro server against a real
  ephemeral PocketBase instance, exercising the actual /content/* and
  /admin routes end to end.
- e2e/: Playwright, a real browser against the built app and another
  ephemeral PocketBase, covering the user-facing flows (home, blog
  archive, article detail incl. JSON-LD, category filter, 404s, the
  /admin redirect).

The Vitest/Playwright tooling (and node_modules) stays in frontend/,
the repo's only npm project; its configs just point at ../tests/.
Playwright resolves packages from the node_modules nearest each spec
file, so `npm run test:e2e` first symlinks tests/node_modules to
frontend/node_modules (pretest:e2e, idempotent, gitignored).

tests/support/pocketbase.ts is the shared piece behind integration
and e2e: it downloads the same pinned PocketBase binary
pocketbase/Dockerfile uses, starts it against the real
pocketbase/pb_migrations/ (not a copy), and seeds it from
tests/support/fixtures.ts — so these tests run against the exact
schema and API rules that ship to production, catching the class of
bug that only shows up when PocketBase actually enforces them (e.g. a
wrong filter/fields query string silently returning nothing, or too
much).

CLAUDE.md and docs/frontend.md document the new npm scripts and the
single-test commands for each layer.
This commit is contained in:
2026-09-11 11:48:43 +02:00
parent 6609e3ae28
commit 91181f2170
23 changed files with 1767 additions and 75 deletions
+1042 -62
View File
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -9,7 +9,12 @@
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"typecheck": "nuxt typecheck",
"lint": "eslint ."
"lint": "eslint .",
"test:unit": "vitest run --config vitest.unit.config.ts",
"test:integration": "vitest run --config vitest.integration.config.ts",
"pretest:e2e": "ln -sfn ../frontend/node_modules ../tests/node_modules",
"test:e2e": "playwright test",
"test": "npm run test:unit && npm run test:integration && npm run test:e2e"
},
"dependencies": {
"marked": "^18.0.11",
@@ -20,8 +25,14 @@
},
"devDependencies": {
"@nuxt/eslint": "^1.17.0",
"@nuxt/test-utils": "^4.3.2",
"@playwright/test": "^1.63.0",
"@vitest/coverage-v8": "^5.0.0",
"@vue/test-utils": "^2.5.0",
"eslint": "^10.10.0",
"happy-dom": "^20.14.3",
"typescript": "^5.9.3",
"vitest": "^5.0.0",
"vue-tsc": "^3.3.11"
}
}
+35
View File
@@ -0,0 +1,35 @@
import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
// Fixed ports so the PocketBase instance (started in global-setup.ts) and the
// built Nuxt server (started by Playwright's webServer below) can reference
// each other statically — see tests/e2e/global-setup.ts for why.
export const E2E_POCKETBASE_PORT = 8095
const APP_PORT = 3100
const BASE_URL = `http://127.0.0.1:${APP_PORT}`
const POCKETBASE_URL = `http://127.0.0.1:${E2E_POCKETBASE_PORT}`
export default defineConfig({
testDir: '../tests/e2e',
fullyParallel: false,
retries: 0,
reporter: [['list']],
use: {
baseURL: BASE_URL,
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
globalSetup: fileURLToPath(new URL('../tests/e2e/global-setup.ts', import.meta.url)),
webServer: {
command: 'npm run build && node .output/server/index.mjs',
url: BASE_URL,
reuseExistingServer: false,
timeout: 120000,
env: {
HOST: '127.0.0.1',
PORT: String(APP_PORT),
NUXT_POCKETBASE_URL: POCKETBASE_URL,
NUXT_PUBLIC_POCKETBASE_URL: POCKETBASE_URL,
NUXT_PUBLIC_SITE_URL: BASE_URL,
},
},
})
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
include: ['../tests/integration/**/*.test.ts'],
testTimeout: 30000,
hookTimeout: 60000,
// Integration tests boot a real Nuxt server + PocketBase instance; running
// several of those concurrently is wasteful, not parallel-safe by design.
fileParallelism: false,
},
})
+8
View File
@@ -0,0 +1,8 @@
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environment: 'nuxt',
include: ['../tests/unit/**/*.test.ts'],
},
})