Set up Playwright

This commit is contained in:
Moshfegh Hamedani 2026-02-13 08:46:19 -08:00
parent d7f4291b95
commit e82aaba60d
13 changed files with 155 additions and 2 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
e2e/test-results/
playwright-report/

View file

@ -17,6 +17,7 @@ A ticket management system that uses AI to classify, respond to, and route suppo
``` ```
/client - React frontend (Vite) /client - React frontend (Vite)
/server - Express backend /server - Express backend
/e2e - Playwright E2E tests
``` ```
## Development ## Development
@ -29,7 +30,7 @@ cd server && bun run dev
cd client && 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 ## 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 `/` - **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` - **Sign-up is disabled** — users are seeded via `prisma/seed.ts`
- **User roles**: `admin` and `agent` (defined as Prisma enum, default `agent`) - **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`)

28
bun.lock Normal file
View file

@ -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=="],
}
}

View file

@ -15,7 +15,7 @@ export default defineConfig({
port: 5173, port: 5173,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: process.env.VITE_API_URL || 'http://localhost:3000',
changeOrigin: true, changeOrigin: true,
}, },
}, },

33
e2e/global-setup.ts Normal file
View file

@ -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.");
}

3
e2e/global-teardown.ts Normal file
View file

@ -0,0 +1,3 @@
export default function globalTeardown() {
console.log("Test run complete. Test database left intact for debugging.");
}

0
e2e/tests/.gitkeep Normal file
View file

14
package.json Normal file
View file

@ -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"
}
}

33
playwright.config.ts Normal file
View file

@ -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,
},
],
});

11
server/.env.test Normal file
View file

@ -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"

View file

@ -21,12 +21,15 @@ app.use(
}) })
); );
const isProduction = process.env.NODE_ENV === "production";
const authLimiter = rateLimit({ const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
limit: 20, limit: 20,
standardHeaders: "draft-8", standardHeaders: "draft-8",
legacyHeaders: false, legacyHeaders: false,
message: { error: "Too many requests, please try again later" }, message: { error: "Too many requests, please try again later" },
skip: () => !isProduction,
}); });
// Mount Better Auth handler BEFORE express.json() // Mount Better Auth handler BEFORE express.json()

View file

@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}

10
tsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"types": ["node"]
},
"include": ["e2e/**/*.ts", "playwright.config.ts"]
}