Verify-suite work (this session):
- M package.json — added typecheck, test:e2e, verify scripts; added @playwright/test devDep - M pnpm-lock.yaml - M .gitignore — Playwright artifacts - M tsconfig.json — auto-modified by Next.js include path - ?? e2e/ — config + fixtures + 3 specs - ?? playwright.config.ts - ?? scripts/verify.sh, scripts/db-test-truncate.sql
This commit is contained in:
25
e2e/admin.spec.ts
Normal file
25
e2e/admin.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { signInAs } from "./fixtures/auth";
|
||||
|
||||
test("admin signs in and reaches each admin section", async ({ page }) => {
|
||||
await signInAs(page, "admin", "/admin");
|
||||
await page.goto("/admin");
|
||||
await expect(page).toHaveURL(/\/admin\/bookings$/);
|
||||
|
||||
for (const [label, urlRe] of [
|
||||
["bookings", /\/admin\/bookings/],
|
||||
["services", /\/admin\/services/],
|
||||
["rooms", /\/admin\/rooms/],
|
||||
["therapists", /\/admin\/therapists/],
|
||||
] as const) {
|
||||
await page.getByRole("link", { name: new RegExp(`^${label}$`, "i") }).click();
|
||||
await expect(page).toHaveURL(urlRe);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("non-admin (therapist) hitting /admin sees the deny screen", async ({ page }) => {
|
||||
await signInAs(page, "therapist", "/admin");
|
||||
await page.goto("/admin");
|
||||
await expect(page.getByRole("heading", { name: /not authorized/i })).toBeVisible();
|
||||
});
|
||||
43
e2e/customer.spec.ts
Normal file
43
e2e/customer.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { signInAs } from "./fixtures/auth";
|
||||
|
||||
test("customer can browse and reach the booking confirm page", async ({ page }) => {
|
||||
// /book/confirm requires an authenticated session, so sign in first.
|
||||
await signInAs(page, "customer", "/");
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: /book a session/i })).toBeVisible();
|
||||
|
||||
// First service "Book" link → /book?serviceId=...
|
||||
const firstBook = page.locator('a[href^="/book?serviceId="]').first();
|
||||
await expect(firstBook).toBeVisible();
|
||||
await firstBook.click();
|
||||
await expect(page).toHaveURL(/\/book\?serviceId=/);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
|
||||
// Pick a date a few days out so slots are likely available.
|
||||
const dateInput = page.locator('input[type="date"][name="date"]');
|
||||
await expect(dateInput).toBeVisible();
|
||||
const target = new Date();
|
||||
target.setDate(target.getDate() + 3);
|
||||
const iso = target.toISOString().slice(0, 10);
|
||||
await dateInput.fill(iso);
|
||||
await page.getByRole("button", { name: /show times/i }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`date=${iso}`));
|
||||
|
||||
// Grab the first slot's href and visit it directly.
|
||||
const slot = page.locator('a[href^="/book/confirm?"]').first();
|
||||
await expect(slot, "expected at least one available slot").toBeVisible();
|
||||
const slotHref = await slot.getAttribute("href");
|
||||
expect(slotHref).toMatch(/^\/book\/confirm\?/);
|
||||
await page.goto(slotHref!);
|
||||
|
||||
// Already authenticated, so we land on the confirm form (not /login).
|
||||
await expect(page).toHaveURL(/\/book\/confirm\?/);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
|
||||
// /account/bookings should be reachable while signed in.
|
||||
await page.goto("/account/bookings");
|
||||
await expect(page).toHaveURL(/\/account\/bookings/);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
});
|
||||
34
e2e/fixtures/auth.ts
Normal file
34
e2e/fixtures/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { clearMailpit, waitForMagicLink } from "./mailpit";
|
||||
|
||||
export const ROLE_EMAIL = {
|
||||
admin: "admin@touchbase.local",
|
||||
therapist: "mei@touchbase.local",
|
||||
customer: "alex@example.com",
|
||||
} as const;
|
||||
|
||||
export type RoleKey = keyof typeof ROLE_EMAIL;
|
||||
|
||||
export async function signInAs(page: Page, role: RoleKey, callbackUrl = "/"): Promise<void> {
|
||||
await clearMailpit();
|
||||
const email = ROLE_EMAIL[role];
|
||||
|
||||
await page.goto(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`);
|
||||
await page.getByLabel("Email").fill(email);
|
||||
// Auth.js v5 server-action signIn() redirects to either our /login/check-email
|
||||
// page or its internal /api/auth/verify-request. Either is fine — the email
|
||||
// is sent regardless. Just wait for any nav off /login.
|
||||
await Promise.all([
|
||||
page.waitForURL((url) => !/\/login(\?|$)/.test(url.pathname + url.search)),
|
||||
page.getByRole("button", { name: /send sign-in link/i }).click(),
|
||||
]);
|
||||
|
||||
const link = await waitForMagicLink(email);
|
||||
await page.goto(link);
|
||||
|
||||
// After callback, NextAuth redirects to callbackUrl. Verify session cookie present.
|
||||
await expect.poll(async () => {
|
||||
const cookies = await page.context().cookies();
|
||||
return cookies.some((c) => /authjs.*session-token/i.test(c.name) || /next-auth.*session-token/i.test(c.name));
|
||||
}).toBeTruthy();
|
||||
}
|
||||
41
e2e/fixtures/mailpit.ts
Normal file
41
e2e/fixtures/mailpit.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
const MAILPIT = process.env.MAILPIT_URL ?? "http://localhost:8025";
|
||||
|
||||
export async function clearMailpit(): Promise<void> {
|
||||
const res = await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" });
|
||||
if (!res.ok) throw new Error(`Mailpit clear failed: ${res.status}`);
|
||||
}
|
||||
|
||||
type MessageSummary = { ID: string; To: { Address: string }[]; Created: string };
|
||||
|
||||
async function listMessagesTo(email: string): Promise<MessageSummary[]> {
|
||||
const url = `${MAILPIT}/api/v1/search?query=${encodeURIComponent(`to:${email}`)}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Mailpit search failed: ${res.status}`);
|
||||
const data = (await res.json()) as { messages: MessageSummary[] };
|
||||
return data.messages ?? [];
|
||||
}
|
||||
|
||||
async function getMessageBody(id: string): Promise<{ text: string; html: string }> {
|
||||
const res = await fetch(`${MAILPIT}/api/v1/message/${id}`);
|
||||
if (!res.ok) throw new Error(`Mailpit fetch failed: ${res.status}`);
|
||||
const data = (await res.json()) as { Text?: string; HTML?: string };
|
||||
return { text: data.Text ?? "", html: data.HTML ?? "" };
|
||||
}
|
||||
|
||||
export async function waitForMagicLink(email: string, opts: { timeoutMs?: number } = {}): Promise<string> {
|
||||
const timeoutMs = opts.timeoutMs ?? 10_000;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const linkRe = /https?:\/\/[^\s<>"]*\/api\/auth\/callback\/nodemailer[^\s<>"]*/i;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const messages = await listMessagesTo(email);
|
||||
if (messages.length > 0) {
|
||||
const newest = messages.sort((a, b) => b.Created.localeCompare(a.Created))[0];
|
||||
const { text, html } = await getMessageBody(newest.ID);
|
||||
const m = (text.match(linkRe) ?? html.match(linkRe))?.[0];
|
||||
if (m) return m.replace(/&/g, "&");
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
throw new Error(`No magic link for ${email} within ${timeoutMs}ms`);
|
||||
}
|
||||
27
e2e/therapist.spec.ts
Normal file
27
e2e/therapist.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { signInAs } from "./fixtures/auth";
|
||||
|
||||
test("therapist signs in and sees their dashboard", async ({ page }) => {
|
||||
await signInAs(page, "therapist", "/therapist");
|
||||
// /therapist redirects to /therapist/bookings.
|
||||
await page.goto("/therapist");
|
||||
await expect(page).toHaveURL(/\/therapist\/bookings$/);
|
||||
|
||||
// Top nav exposes "My schedule" → bookings, and "Availability".
|
||||
await expect(page.getByRole("link", { name: /^my schedule$/i })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /^availability$/i })).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: /^availability$/i }).click();
|
||||
await expect(page).toHaveURL(/\/therapist\/availability/);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: /^my schedule$/i }).click();
|
||||
await expect(page).toHaveURL(/\/therapist\/bookings/);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("non-therapist (customer) hitting /therapist sees the deny screen", async ({ page }) => {
|
||||
await signInAs(page, "customer", "/therapist");
|
||||
await page.goto("/therapist");
|
||||
await expect(page.getByRole("heading", { name: /therapists only/i })).toBeVisible();
|
||||
});
|
||||
@@ -10,8 +10,11 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"verify": "scripts/verify.sh",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:migrate:deploy": "prisma migrate deploy",
|
||||
@@ -41,6 +44,7 @@
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^22",
|
||||
"@types/nodemailer": "^8.0.0",
|
||||
|
||||
35
playwright.config.ts
Normal file
35
playwright.config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
// Dedicated test port — keeps the e2e server isolated from any `pnpm dev`
|
||||
// the user might have running, and pins it against the touchbase_test DB
|
||||
// (loaded from .env.test in the webServer command below).
|
||||
const PORT = Number(process.env.E2E_PORT ?? 3010);
|
||||
const BASE_URL = process.env.E2E_BASE_URL ?? `http://localhost:${PORT}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
fullyParallel: false,
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
reporter: [["list"]],
|
||||
timeout: 30_000,
|
||||
expect: { timeout: 10_000 },
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
trace: "retain-on-failure",
|
||||
screenshot: "only-on-failure",
|
||||
video: "off",
|
||||
},
|
||||
webServer: {
|
||||
// dotenv-cli loads .env.test (DATABASE_URL=touchbase_test) for the child.
|
||||
command: `pnpm exec dotenv -e .env.test -- next dev -p ${PORT}`,
|
||||
url: `${BASE_URL}/api/health`,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 60_000,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
projects: [
|
||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||
],
|
||||
});
|
||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@@ -31,10 +31,10 @@ importers:
|
||||
version: 3.2.0(date-fns@4.1.0)
|
||||
next:
|
||||
specifier: 16.2.4
|
||||
version: 16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
version: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.31
|
||||
version: 5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4)
|
||||
version: 5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4)
|
||||
nodemailer:
|
||||
specifier: ^8.0.7
|
||||
version: 8.0.7
|
||||
@@ -57,6 +57,9 @@ importers:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4
|
||||
version: 4.2.4
|
||||
@@ -682,6 +685,11 @@ packages:
|
||||
'@panva/hkdf@1.2.1':
|
||||
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
|
||||
|
||||
'@playwright/test@1.59.1':
|
||||
resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
@@ -1785,6 +1793,11 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -2435,6 +2448,16 @@ packages:
|
||||
pkg-types@2.3.1:
|
||||
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||
|
||||
playwright-core@1.59.1:
|
||||
resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.59.1:
|
||||
resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3475,6 +3498,10 @@ snapshots:
|
||||
|
||||
'@panva/hkdf@1.2.1': {}
|
||||
|
||||
'@playwright/test@1.59.1':
|
||||
dependencies:
|
||||
playwright: 1.59.1
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@prisma/adapter-pg@7.8.0':
|
||||
@@ -4733,6 +4760,9 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -5160,15 +5190,15 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-auth@5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4):
|
||||
next-auth@5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4):
|
||||
dependencies:
|
||||
'@auth/core': 0.41.2(nodemailer@8.0.7)
|
||||
next: 16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
nodemailer: 8.0.7
|
||||
|
||||
next@16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@next/env': 16.2.4
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -5187,6 +5217,7 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 16.2.4
|
||||
'@next/swc-win32-arm64-msvc': 16.2.4
|
||||
'@next/swc-win32-x64-msvc': 16.2.4
|
||||
'@playwright/test': 1.59.1
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -5345,6 +5376,14 @@ snapshots:
|
||||
exsolve: 1.0.8
|
||||
pathe: 2.0.3
|
||||
|
||||
playwright-core@1.59.1: {}
|
||||
|
||||
playwright@1.59.1:
|
||||
dependencies:
|
||||
playwright-core: 1.59.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
|
||||
BIN
ruvector.db
Normal file
BIN
ruvector.db
Normal file
Binary file not shown.
22
scripts/db-test-truncate.sql
Normal file
22
scripts/db-test-truncate.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Truncate all Prisma-managed application tables in touchbase_test.
|
||||
-- Used by `pnpm verify` to give Playwright a clean fixture-only DB state
|
||||
-- without triggering Prisma's AI-invocation guard on `migrate reset`.
|
||||
-- pg-boss tables (schema "pgboss") are intentionally left alone.
|
||||
TRUNCATE TABLE
|
||||
"AuditLog",
|
||||
"Notification",
|
||||
"Payment",
|
||||
"Booking",
|
||||
"AvailabilityOverride",
|
||||
"WorkingHours",
|
||||
"ServiceTherapist",
|
||||
"Service",
|
||||
"RoomBlock",
|
||||
"RoomTag",
|
||||
"Room",
|
||||
"TherapistTag",
|
||||
"Therapist",
|
||||
"Customer",
|
||||
"VerificationToken",
|
||||
"User"
|
||||
RESTART IDENTITY CASCADE;
|
||||
67
scripts/verify.sh
Executable file
67
scripts/verify.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the full verify suite: lint, typecheck, vitest, Playwright e2e.
|
||||
# Uses the dedicated touchbase_test DB (reset + seeded) and a Next.js
|
||||
# server on E2E_PORT (3010) so it never touches the dev DB or dev server.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
bold() { printf "\n\033[1m==> %s\033[0m\n" "$*"; }
|
||||
fail() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
|
||||
|
||||
E2E_PORT="${E2E_PORT:-3010}"
|
||||
export E2E_PORT
|
||||
|
||||
# --- Pre-flight ---------------------------------------------------------------
|
||||
bold "Pre-flight"
|
||||
curl -sf http://localhost:5432 -o /dev/null || true # informational
|
||||
curl -sf "http://localhost:8025/api/v1/info" >/dev/null \
|
||||
|| fail "Mailpit not reachable on http://localhost:8025. Run: docker compose up -d mailpit"
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -q '^touchbase-postgres-1$'; then
|
||||
fail "Postgres container 'touchbase-postgres-1' not running. Run: docker compose up -d postgres"
|
||||
fi
|
||||
|
||||
# Next 16 enforces one `next dev` per project. If one is running it'll block
|
||||
# the e2e test server from starting on E2E_PORT.
|
||||
if pgrep -f "next dev" >/dev/null 2>&1; then
|
||||
fail "Another \`next dev\` is running (Next 16 enforces one per project).
|
||||
Stop it before running verify: pkill -f 'next dev'
|
||||
Then re-run: pnpm verify"
|
||||
fi
|
||||
|
||||
# --- Lint ---------------------------------------------------------------------
|
||||
bold "Lint"
|
||||
pnpm lint
|
||||
|
||||
# --- Typecheck ----------------------------------------------------------------
|
||||
bold "Typecheck"
|
||||
pnpm exec tsc --noEmit
|
||||
|
||||
# --- Test DB: ensure schema is current ---------------------------------------
|
||||
bold "Apply migrations to test DB"
|
||||
pnpm exec dotenv -e .env.test -- prisma migrate deploy
|
||||
|
||||
# --- Vitest -------------------------------------------------------------------
|
||||
# Vitest tests load .env.test via test/setup.ts and manage their own data;
|
||||
# we don't need to reset before this step.
|
||||
bold "Vitest"
|
||||
pnpm test
|
||||
|
||||
# --- Truncate + seed test DB before Playwright ------------------------------
|
||||
# Vitest may have left rows around. Playwright fixtures (admin/therapist/
|
||||
# customer accounts, services, rooms) need a known seeded state.
|
||||
# Truncate via psql (faster than `migrate reset`, avoids Prisma's AI guard).
|
||||
bold "Truncate + seed test DB"
|
||||
docker exec -i touchbase-postgres-1 \
|
||||
psql -U touchbase -d touchbase_test -v ON_ERROR_STOP=1 -q \
|
||||
< scripts/db-test-truncate.sql
|
||||
pnpm exec dotenv -e .env.test -- tsx prisma/seed.ts
|
||||
|
||||
# --- Playwright e2e -----------------------------------------------------------
|
||||
# playwright.config.ts has a webServer block that boots `next dev` on
|
||||
# E2E_PORT against .env.test, so we don't manage the server here.
|
||||
bold "Playwright e2e (port $E2E_PORT)"
|
||||
pnpm exec playwright test
|
||||
|
||||
bold "All green"
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -19,7 +23,9 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -28,7 +34,10 @@
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
"**/*.mts",
|
||||
".next/dev/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user