From e82aaba60dda984c8d7fc2c0dc29cc8ec163d855 Mon Sep 17 00:00:00 2001 From: Moshfegh Hamedani Date: Fri, 13 Feb 2026 08:46:19 -0800 Subject: [PATCH] Set up Playwright --- .gitignore | 3 +++ CLAUDE.md | 13 ++++++++++++- bun.lock | 28 ++++++++++++++++++++++++++++ client/vite.config.ts | 2 +- e2e/global-setup.ts | 33 +++++++++++++++++++++++++++++++++ e2e/global-teardown.ts | 3 +++ e2e/tests/.gitkeep | 0 package.json | 14 ++++++++++++++ playwright.config.ts | 33 +++++++++++++++++++++++++++++++++ server/.env.test | 11 +++++++++++ server/src/index.ts | 3 +++ test-results/.last-run.json | 4 ++++ tsconfig.json | 10 ++++++++++ 13 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 bun.lock create mode 100644 e2e/global-setup.ts create mode 100644 e2e/global-teardown.ts create mode 100644 e2e/tests/.gitkeep create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 server/.env.test create mode 100644 test-results/.last-run.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59c975a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +e2e/test-results/ +playwright-report/ diff --git a/CLAUDE.md b/CLAUDE.md index 1636692..3c8d1d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,7 @@ A ticket management system that uses AI to classify, respond to, and route suppo ``` /client - React frontend (Vite) /server - Express backend +/e2e - Playwright E2E tests ``` ## Development @@ -29,7 +30,7 @@ cd server && bun run dev cd client && bun run dev ``` -The client proxies `/api/*` requests to the server via Vite config. +The client proxies `/api/*` requests to the server via Vite config (target is configurable via `VITE_API_URL` env var, defaults to `http://localhost:3000`). ## Key Conventions @@ -50,3 +51,13 @@ The client proxies `/api/*` requests to the server via Vite config. - **Admin route protection (client)**: `AdminRoute` component wraps admin-only routes; redirects non-admins to `/` - **Sign-up is disabled** — users are seeded via `prisma/seed.ts` - **User roles**: `admin` and `agent` (defined as Prisma enum, default `agent`) +- **Rate limiting**: Auth routes are rate-limited, but only enforced when `NODE_ENV=production` + +## E2E Testing + +- **Framework**: Playwright (config at root `playwright.config.ts`) +- **Test database**: `helpdesk_test` (isolated from dev `helpdesk` DB), configured in `server/.env.test` +- **Ports**: Test server on 3001, test client on 5174 (dev uses 3000/5173) +- **Global setup** (`e2e/global-setup.ts`): Runs `prisma migrate reset --force` then seeds the test DB +- **Tests directory**: `e2e/tests/` +- **Run tests**: `bun run test:e2e` from root (also `test:e2e:ui`, `test:e2e:headed`) diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2497dc7 --- /dev/null +++ b/bun.lock @@ -0,0 +1,28 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "helpdesk", + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/node": "^25.2.3", + "dotenv": "^17.3.1", + }, + }, + }, + "packages": { + "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + + "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/client/vite.config.ts b/client/vite.config.ts index 2b1fc0f..9cb7ca0 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ port: 5173, proxy: { '/api': { - target: 'http://localhost:3000', + target: process.env.VITE_API_URL || 'http://localhost:3000', changeOrigin: true, }, }, diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts new file mode 100644 index 0000000..0d0b8a0 --- /dev/null +++ b/e2e/global-setup.ts @@ -0,0 +1,33 @@ +import { execSync } from "child_process"; +import path from "path"; +import dotenv from "dotenv"; + +export default function globalSetup() { + const envPath = path.resolve(__dirname, "../server/.env.test"); + const env = dotenv.config({ path: envPath }); + + console.log("Resetting test database..."); + + const serverDir = path.resolve(__dirname, "../server"); + const execEnv = { + ...process.env, + ...env.parsed, + PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: "Yes", + }; + + execSync("bunx prisma migrate reset --force", { + cwd: serverDir, + stdio: "inherit", + env: execEnv, + }); + + console.log("Running seed..."); + + execSync("bun prisma/seed.ts", { + cwd: serverDir, + stdio: "inherit", + env: execEnv, + }); + + console.log("Test database ready."); +} diff --git a/e2e/global-teardown.ts b/e2e/global-teardown.ts new file mode 100644 index 0000000..ecb558f --- /dev/null +++ b/e2e/global-teardown.ts @@ -0,0 +1,3 @@ +export default function globalTeardown() { + console.log("Test run complete. Test database left intact for debugging."); +} diff --git a/e2e/tests/.gitkeep b/e2e/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..536f44d --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "helpdesk", + "private": true, + "scripts": { + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/node": "^25.2.3", + "dotenv": "^17.3.1" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..c811259 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e/tests", + globalSetup: "./e2e/global-setup.ts", + globalTeardown: "./e2e/global-teardown.ts", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: "html", + use: { + baseURL: "http://localhost:5174", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: [ + { + command: "bun run --cwd server --env-file=.env.test src/index.ts", + url: "http://localhost:3001/api/health", + reuseExistingServer: !process.env.CI, + }, + { + command: "VITE_API_URL=http://localhost:3001 bun run --cwd client vite --port 5174", + url: "http://localhost:5174", + reuseExistingServer: !process.env.CI, + }, + ], +}); diff --git a/server/.env.test b/server/.env.test new file mode 100644 index 0000000..4740c16 --- /dev/null +++ b/server/.env.test @@ -0,0 +1,11 @@ +DATABASE_URL="postgresql://postgres:MyPassword!@localhost:5432/helpdesk_test?schema=public" + +BETTER_AUTH_SECRET="test-secret-do-not-use-in-production" +BETTER_AUTH_URL="http://localhost:3001" + +TRUSTED_ORIGINS="http://localhost:5174" + +PORT=3001 + +SEED_ADMIN_EMAIL="admin@example.com" +SEED_ADMIN_PASSWORD="password123" diff --git a/server/src/index.ts b/server/src/index.ts index a98fd8e..03c2983 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -21,12 +21,15 @@ app.use( }) ); +const isProduction = process.env.NODE_ENV === "production"; + const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes limit: 20, standardHeaders: "draft-8", legacyHeaders: false, message: { error: "Too many requests, please try again later" }, + skip: () => !isProduction, }); // Mount Better Auth handler BEFORE express.json() diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4e371ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "types": ["node"] + }, + "include": ["e2e/**/*.ts", "playwright.config.ts"] +}