Add testing strategy: Vitest for unit tests, Playwright for E2E

- Vitest: unit tests for packages, components, and app logic (jsdom env,
  @testing-library/react, co-located test files)
- Playwright: E2E browser tests per app (journal.test.ts, planner.test.ts),
  auto-starts dev servers, scoped via testMatch
- Sample unit test for types package
- Smoke E2E tests for both apps
- Updated CLAUDE.md with test commands and strategy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-22 12:36:09 +01:00
parent 4fac18c165
commit 40b9c425a0
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
11 changed files with 873 additions and 6 deletions

3
.gitignore vendored
View file

@ -8,3 +8,6 @@ dist/
.DS_Store
*.log
.crit.json
e2e/results/
test-results/
playwright-report/

View file

@ -58,9 +58,21 @@ turbo dev # Start both apps in dev mode
turbo build # Build all packages and apps
turbo typecheck # Type-check all packages
turbo lint # Lint all packages
turbo test # Run tests
pnpm test # Run unit tests (vitest)
pnpm test:watch # Run unit tests in watch mode
pnpm test:e2e # Run E2E tests (playwright, requires dev servers)
pnpm test:e2e:ui # Run E2E tests with Playwright UI
```
## Testing Strategy
- **Unit tests** (Vitest + jsdom): For packages, components, utilities, and app logic.
Place test files next to source: `foo.ts``foo.test.ts`.
Uses `@testing-library/react` for component tests.
- **E2E tests** (Playwright): For browser behavior across both apps.
Tests live in `e2e/` at repo root. Scoped per app via `testMatch` in `playwright.config.ts`.
Playwright auto-starts dev servers if not already running.
## Code Conventions
- All user-facing strings must use i18n (`useTranslation()` hook, never hardcode strings)

9
e2e/journal.test.ts Normal file
View file

@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test.describe("Journal", () => {
test("loads the home page", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle("trails.cool");
await expect(page.getByText("Your outdoor activity journal")).toBeVisible();
});
});

9
e2e/planner.test.ts Normal file
View file

@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test.describe("Planner", () => {
test("loads the home page", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle("trails.cool Planner");
await expect(page.getByText("Collaborative route planning")).toBeVisible();
});
});

View file

@ -11,19 +11,27 @@
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"typecheck": "turbo typecheck"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"@react-router/dev": "^7.13.1",
"@react-router/node": "^7.13.1",
"@react-router/serve": "^7.13.1",
"@tailwindcss/vite": "^4.2.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^10.1.0",
"eslint-config-prettier": "^10.1.8",
"jsdom": "^29.0.1",
"playwright": "^1.58.2",
"prettier": "^3.8.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@ -31,6 +39,7 @@
"tailwindcss": "^4.2.2",
"turbo": "^2.8.20",
"typescript-eslint": "^8.57.1",
"vite": "catalog:"
"vite": "catalog:",
"vitest": "^4.1.0"
}
}

View file

@ -0,0 +1,15 @@
import { describe, it, expect } from "vitest";
import type { Waypoint, Route } from "./index";
describe("types", () => {
it("Waypoint type accepts valid data", () => {
const waypoint: Waypoint = { lat: 52.52, lon: 13.405 };
expect(waypoint.lat).toBe(52.52);
expect(waypoint.lon).toBe(13.405);
});
it("Waypoint supports isDayBreak flag", () => {
const waypoint: Waypoint = { lat: 48.137, lon: 11.576, name: "Munich", isDayBreak: true };
expect(waypoint.isDayBreak).toBe(true);
});
});

45
playwright.config.ts Normal file
View file

@ -0,0 +1,45 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
outputDir: "./e2e/results",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? "github" : "list",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "journal",
testMatch: "journal.test.ts",
use: {
...devices["Desktop Chrome"],
baseURL: "http://localhost:3000",
},
},
{
name: "planner",
testMatch: "planner.test.ts",
use: {
...devices["Desktop Chrome"],
baseURL: "http://localhost:3001",
},
},
],
webServer: [
{
command: "pnpm --filter @trails-cool/journal dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
{
command: "pnpm --filter @trails-cool/planner dev",
url: "http://localhost:3001",
reuseExistingServer: !process.env.CI,
},
],
});

756
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -12,9 +12,6 @@
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^build"]
}

11
vitest.config.ts Normal file
View file

@ -0,0 +1,11 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
include: ["packages/*/src/**/*.test.{ts,tsx}", "apps/*/app/**/*.test.{ts,tsx}"],
exclude: ["**/e2e/**"],
setupFiles: ["./vitest.setup.ts"],
},
});

1
vitest.setup.ts Normal file
View file

@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";