diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..eda7bf0 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,46 @@ +name: E2E Tests + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +permissions: + contents: read + +jobs: + e2e: + name: Playwright E2E + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + API_BASE_URL: http://localhost:9090 + OIDC_ISSUER_URL: http://localhost:4000 + OIDC_CLIENT_ID: test-only-not-a-real-id + OIDC_CLIENT_SECRET: test-only-not-a-real-secret + NEXT_PUBLIC_OIDC_PROVIDER_ID: oidc + BETTER_AUTH_URL: http://localhost:3000 + BETTER_AUTH_SECRET: test-only-not-a-real-better-auth-secret + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium + + - name: Run Playwright tests + run: pnpm test:e2e + + - name: Upload test artifacts + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: | + test-results/ + playwright-report/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 489f739..488066b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ # testing /coverage +# Playwright/Cucumber E2E artifacts +test-results/ +playwright-report/ +blob-report/ # next.js /.next/ diff --git a/CLAUDE.md b/CLAUDE.md index b26687b..b3b944a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,7 +155,7 @@ pnpm generate-client:nofetch # Regenerate without fetching ### Backend API -- **Base URL**: Configured via `NEXT_PUBLIC_API_URL` +- **Base URL**: Configured via `API_BASE_URL` (server-side only) - **Format**: Official MCP Registry API (upstream compatible) - **Endpoints**: - `GET /api/v0/servers` - List all MCP servers @@ -178,7 +178,7 @@ pnpm generate-client:nofetch # Regenerate without fetching ### Production 1. User accesses protected route -2. Redirected to `/sign-in` +2. Redirected to `/signin` 3. Better Auth initiates OIDC flow with configured provider 4. Provider redirects back with authorization code 5. Better Auth exchanges code for tokens @@ -222,6 +222,19 @@ pnpm generate-client:nofetch # Regenerate without fetching - **Testing Library** - Component testing - **jsdom** - DOM simulation +### E2E Tests (Playwright) + +- End-to-end tests live under `tests/e2e` and run against a live dev stack. +- Commands: + - `pnpm dev` – starts Next.js (3000), mock OIDC (4000), and MSW mock API (9090) + - `pnpm run test:e2e` – runs Playwright tests (headless) + - `pnpm run test:e2e:ui` – opens Playwright UI mode for interactive debugging + - `pnpm run test:e2e:debug` – runs with Playwright Inspector +- CI runs E2E tests via `.github/workflows/bdd.yml` and installs Playwright browsers. +- Install browsers locally once: `pnpm exec playwright install` + +Tests use custom fixtures for authentication. The `authenticatedPage` fixture handles login automatically. + ### Example Test ```typescript @@ -278,7 +291,10 @@ git push origin v0.x.x ### Authentication Not Working -- **Development**: Ensure OIDC mock is running (`pnpm oidc`) +- **Development**: + - Ensure OIDC mock is running (`pnpm oidc`) or start the full stack with `pnpm dev` + - Dev provider issues refresh tokens unconditionally and uses a short AccessToken TTL (15s) to exercise the refresh flow + - If you see origin errors (403), ensure `BETTER_AUTH_URL` matches the port you use (default `http://localhost:3000`) or include it in `TRUSTED_ORIGINS` - **Production**: Check environment variables: - `OIDC_ISSUER_URL` - OIDC provider URL - `OIDC_CLIENT_ID` - OAuth2 client ID @@ -289,7 +305,7 @@ git push origin v0.x.x ### API Calls Failing -- Check `NEXT_PUBLIC_API_URL` environment variable +- Check `API_BASE_URL` environment variable - Verify backend API is running - Check browser console for CORS errors diff --git a/README.md b/README.md index a1c295d..95d0d53 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ pnpm dev # Application will be available at http://localhost:3000 ``` +Authentication: the dev stack also starts a local OIDC provider (on :4000) and MSW mock API (on :9090). The `/signin` page initiates the OIDC flow and redirects back to `/catalog` on success. + ### Available Commands #### Development Commands (pnpm) @@ -273,25 +275,26 @@ BETTER_AUTH_URL=http://localhost:3000 ### Testing +#### Unit/Component Tests + ```bash -# Run all tests -pnpm test +pnpm test # Run all tests +pnpm test --watch # Watch mode +pnpm test --coverage # With coverage +``` -# Run tests in watch mode -pnpm test --watch +Uses Vitest + Testing Library + MSW. -# Run tests with coverage -pnpm test --coverage +#### E2E Tests (Playwright) -# Run specific test file -pnpm test src/components/navbar.test.tsx +```bash +pnpm exec playwright install # One-time browser install +pnpm test:e2e # Run tests (auto-starts dev server if needed) +pnpm test:e2e:ui # Playwright UI mode +pnpm test:e2e:debug # With Playwright Inspector ``` -Tests use: - -- **Vitest** - Test runner -- **Testing Library** - React component testing -- **MSW** - API mocking +Tests automatically start the dev stack if it's not already running. If you prefer to start it manually first, run `pnpm dev` before the tests. ### Mock Server @@ -467,6 +470,7 @@ For detailed information about the project: - [shadcn/ui Components](https://ui.shadcn.com) - [MCP Registry Official](https://github.com/modelcontextprotocol/registry) + ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. diff --git a/package.json b/package.json index b02a828..85fc000 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "lint": "biome check", "format": "biome format --write", "test": "vitest", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", "type-check": "tsc --noEmit", "prepare": "husky", "oidc": "node dev-auth/oidc-provider.mjs", @@ -51,6 +54,7 @@ "@hey-api/client-next": "0.5.1", "@hey-api/openapi-ts": "0.89.0", "@mswjs/http-middleware": "^0.10.2", + "@playwright/test": "^1.56.1", "@tailwindcss/postcss": "^4", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", diff --git a/playwright.config.mts b/playwright.config.mts new file mode 100644 index 0000000..ddaf463 --- /dev/null +++ b/playwright.config.mts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from "@playwright/test"; + +const BASE_URL = process.env.BASE_URL || "http://localhost:3000"; + +async function isServerRunning(): Promise { + try { + await fetch(BASE_URL, { signal: AbortSignal.timeout(2000) }); + return true; + } catch { + return false; + } +} + +const serverAlreadyRunning = await isServerRunning(); + +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? "github" : "list", + timeout: 30_000, + use: { + baseURL: BASE_URL, + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: serverAlreadyRunning + ? undefined + : { + command: "pnpm dev", + url: BASE_URL, + timeout: 120_000, + stdout: "pipe", + stderr: "pipe", + env: { + API_BASE_URL: "http://localhost:9090", + OIDC_ISSUER_URL: "http://localhost:4000", + OIDC_CLIENT_ID: "better-auth-dev", + OIDC_CLIENT_SECRET: "dev-secret-change-in-production", + NEXT_PUBLIC_OIDC_PROVIDER_ID: "okta", + BETTER_AUTH_URL: "http://localhost:3000", + BETTER_AUTH_SECRET: "e2e-test-secret-at-least-32-chars-long", + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fef6e06..8bbe2cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 3.0.1(ajv@8.17.1) better-auth: specifier: 1.4.6 - version: 1.4.6(next@16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.4.6(next@16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -55,13 +55,13 @@ importers: version: 2.12.4(@types/node@24.10.3)(typescript@5.9.3) next: specifier: 16.0.10 - version: 16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nuqs: specifier: ^2.8.1 - version: 2.8.5(next@16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 2.8.5(next@16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -87,6 +87,9 @@ importers: '@mswjs/http-middleware': specifier: ^0.10.2 version: 0.10.3(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3)) + '@playwright/test': + specifier: ^1.56.1 + version: 1.57.0 '@tailwindcss/postcss': specifier: ^4 version: 4.1.18 @@ -1012,6 +1015,11 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@playwright/test@1.57.0': + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + engines: {node: '>=18'} + hasBin: true + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -1742,6 +1750,10 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -2166,6 +2178,11 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + 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} @@ -2736,6 +2753,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + engines: {node: '>=18'} + hasBin: true + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -4000,6 +4027,10 @@ snapshots: '@open-draft/until@2.1.0': {} + '@playwright/test@1.57.0': + dependencies: + playwright: 1.57.0 + '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': @@ -4481,7 +4512,7 @@ snapshots: '@testing-library/jest-dom@6.9.1': dependencies: '@adobe/css-tools': 4.4.4 - aria-query: 5.3.0 + aria-query: 5.3.2 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 picocolors: 1.1.1 @@ -4651,6 +4682,8 @@ snapshots: dependencies: dequal: 2.0.3 + aria-query@5.3.2: {} + array-flatten@1.1.1: {} assertion-error@2.0.1: {} @@ -4661,7 +4694,7 @@ snapshots: baseline-browser-mapping@2.9.5: {} - better-auth@1.4.6(next@16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + better-auth@1.4.6(next@16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1) '@better-auth/telemetry': 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1)) @@ -4677,7 +4710,7 @@ snapshots: nanostores: 1.0.1 zod: 4.1.12 optionalDependencies: - next: 16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5090,6 +5123,9 @@ snapshots: fresh@0.5.2: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5517,7 +5553,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next@16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.0.10 '@swc/helpers': 0.5.15 @@ -5535,6 +5571,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.0.10 '@next/swc-win32-arm64-msvc': 16.0.10 '@next/swc-win32-x64-msvc': 16.0.10 + '@playwright/test': 1.57.0 babel-plugin-react-compiler: 1.0.0 sharp: 0.34.5 transitivePeerDependencies: @@ -5545,12 +5582,12 @@ snapshots: node-releases@2.0.27: {} - nuqs@2.8.5(next@16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): + nuqs@2.8.5(next@16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.3 optionalDependencies: - next: 16.0.10(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.0.10(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nypm@0.6.2: dependencies: @@ -5634,6 +5671,14 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + playwright-core@1.57.0: {} + + playwright@1.57.0: + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.4.31: dependencies: nanoid: 3.3.11 diff --git a/src/mocks/server.ts b/src/mocks/server.ts index 4a2acd5..8d2d03b 100644 --- a/src/mocks/server.ts +++ b/src/mocks/server.ts @@ -1,29 +1,30 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { createServer } from "@mswjs/http-middleware"; import { config } from "dotenv"; +import { HttpResponse, http } from "msw"; import { handlers } from "./handlers"; -// Load .env first, then .env.local (which overrides .env) config(); config({ path: ".env.local" }); -// Mock server runs on the port configured in API_BASE_URL -// This ensures the app can reach the mock server at the expected URL const apiBaseUrl = process.env.API_BASE_URL; - if (!apiBaseUrl) { throw new Error("API_BASE_URL environment variable is required"); } const port = new URL(apiBaseUrl).port; - if (!port) { throw new Error("API_BASE_URL must include a port number"); } -const httpServer = createServer(...handlers); +const healthHandler = http.get("/health", () => { + return HttpResponse.json({ status: "ok" }); +}); + +const httpServer = createServer(healthHandler, ...handlers); httpServer.on("request", (req: IncomingMessage, _res: ServerResponse) => { + if (req.url?.includes("/health")) return; console.log(`[mock] ${req.method} ${req.url}`); }); diff --git a/tests/e2e/catalog.spec.ts b/tests/e2e/catalog.spec.ts new file mode 100644 index 0000000..1e1c8ba --- /dev/null +++ b/tests/e2e/catalog.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from "./fixtures"; + +test.describe("Catalog page", () => { + test("displays MCP servers from the catalog", async ({ + authenticatedPage, + }) => { + await authenticatedPage.goto("/catalog"); + + await expect( + authenticatedPage.getByRole("heading", { name: "MCP Server Catalog" }), + ).toBeVisible(); + + await expect( + authenticatedPage.getByText("awslabs/aws-nova-canvas"), + ).toBeVisible(); + await expect( + authenticatedPage.getByText("github/mcp-github"), + ).toBeVisible(); + }); +}); diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts new file mode 100644 index 0000000..80dc0fb --- /dev/null +++ b/tests/e2e/fixtures.ts @@ -0,0 +1,24 @@ +import { test as base, expect, type Page } from "@playwright/test"; + +const BASE_URL = process.env.BASE_URL || "http://localhost:3000"; + +async function login(page: Page): Promise { + await page.goto(`${BASE_URL}/signin`); + + const signInButton = page.getByRole("button", { name: /oidc|okta/i }); + await expect(signInButton).toBeVisible({ timeout: 5000 }); + await signInButton.click(); + + await page.waitForURL((url) => !url.pathname.startsWith("/signin"), { + timeout: 30000, + }); +} + +export const test = base.extend<{ authenticatedPage: Page }>({ + authenticatedPage: async ({ page }, use) => { + await login(page); + await use(page); + }, +}); + +export { expect } from "@playwright/test"; diff --git a/tests/e2e/login.spec.ts b/tests/e2e/login.spec.ts new file mode 100644 index 0000000..e5e0bd8 --- /dev/null +++ b/tests/e2e/login.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from "./fixtures"; + +test.describe("Login flow", () => { + test("sign in and land on Catalog", async ({ page }) => { + await page.goto("/signin"); + await page.getByRole("button", { name: /oidc|okta/i }).click(); + await expect(page).toHaveURL(/\/catalog$/); + await expect( + page.getByRole("heading", { name: "MCP Server Catalog" }), + ).toBeVisible(); + }); + + test("log out from Catalog", async ({ authenticatedPage }) => { + await authenticatedPage.goto("/catalog"); + await authenticatedPage.getByRole("button", { name: "Test User" }).click(); + await authenticatedPage.getByRole("menuitem", { name: "Sign out" }).click(); + await expect(authenticatedPage).toHaveURL(/\/signin$/); + }); +}); diff --git a/vitest.config.mts b/vitest.config.mts index e38322e..6468d92 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -8,6 +8,7 @@ export default defineConfig({ globals: true, environment: "jsdom", setupFiles: ["src/mocks/test.setup.ts", "./vitest.setup.ts"], + exclude: ["**/node_modules/**", "**/dist/**", "tests/e2e/**"], env: { // Exactly 32 bytes for AES-256 BETTER_AUTH_SECRET: "12345678901234567890123456789012", // Exactly 32 bytes for AES-256