From 876f1da020df2150d3c2c971e028145758d257f9 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 4 May 2026 17:50:06 +0530 Subject: [PATCH] test(web): add Playwright config and dashboard smoke test --- surfsense_web/playwright.config.ts | 65 +++++++++++++++++++++++++++ surfsense_web/tests/auth.setup.ts | 57 +++++++++++++++++++++++ surfsense_web/tests/dashboard.spec.ts | 21 +++++++++ 3 files changed, 143 insertions(+) create mode 100644 surfsense_web/playwright.config.ts create mode 100644 surfsense_web/tests/auth.setup.ts create mode 100644 surfsense_web/tests/dashboard.spec.ts diff --git a/surfsense_web/playwright.config.ts b/surfsense_web/playwright.config.ts new file mode 100644 index 000000000..a217a1424 --- /dev/null +++ b/surfsense_web/playwright.config.ts @@ -0,0 +1,65 @@ +import { defineConfig, devices } from "@playwright/test"; + +const PORT = process.env.PORT || "3000"; +const BACKEND_PORT = process.env.BACKEND_PORT || "8000"; +const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://localhost:${PORT}`; + +/** + * Playwright configuration for SurfSense web E2E tests. + * + * Tests live under `tests/` and are NEVER bundled into the production Next.js + * build (`.next/standalone/`) or the Electron desktop build, because: + * - This file and `tests/` are listed in `.dockerignore`. + * - `electron-builder.yml` only ships `.next/standalone/`, not source files. + * - `@playwright/test` is a `devDependency`, so production `pnpm install` + * with `--prod` skips it entirely. + * + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: "./tests", + timeout: 30_000, + expect: { timeout: 5_000 }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI + ? [["html", { open: "never" }], ["github"], ["list"]] + : [["html", { open: "on-failure" }], ["list"]], + use: { + baseURL, + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + extraHTTPHeaders: { + "x-playwright-test": "true", + }, + }, + projects: [ + { + name: "setup", + testMatch: /.*\.setup\.ts/, + }, + { + name: "chromium", + dependencies: ["setup"], + use: { + ...devices["Desktop Chrome"], + storageState: "playwright/.auth/user.json", + }, + }, + ], + webServer: process.env.PLAYWRIGHT_NO_WEB_SERVER + ? undefined + : { + command: "pnpm dev", + url: `http://localhost:${PORT}`, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + NEXT_PUBLIC_FASTAPI_BACKEND_URL: `http://localhost:${BACKEND_PORT}`, + NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: "LOCAL", + }, + }, +}); diff --git a/surfsense_web/tests/auth.setup.ts b/surfsense_web/tests/auth.setup.ts new file mode 100644 index 000000000..865fd1bb9 --- /dev/null +++ b/surfsense_web/tests/auth.setup.ts @@ -0,0 +1,57 @@ +import path from "node:path"; +import { expect, test as setup } from "@playwright/test"; + +/** + * One-time authentication setup. Logs in via the FastAPI backend directly + * (skipping the UI) and persists the resulting localStorage token so every + * test in the chromium project starts already authenticated. + * + * Mirrors the real auth flow in `lib/apis/auth-api.service.ts`: + * POST /auth/jwt/login -> { access_token } + * localStorage.setItem("surfsense_bearer_token", access_token) + * + * Requires a seeded test user in the dev/test DB. Configure via env: + * PLAYWRIGHT_TEST_EMAIL, PLAYWRIGHT_TEST_PASSWORD + * NEXT_PUBLIC_FASTAPI_BACKEND_URL (defaults to http://localhost:8000) + */ + +const authFile = path.join(__dirname, "..", "playwright", ".auth", "user.json"); + +const TEST_USER_EMAIL = process.env.PLAYWRIGHT_TEST_EMAIL || "test@surfsense.test"; +const TEST_USER_PASSWORD = process.env.PLAYWRIGHT_TEST_PASSWORD || "TestPassword123!"; +const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000"; +const STORAGE_KEY = "surfsense_bearer_token"; + +setup("authenticate", async ({ page, request }) => { + const response = await request.post(`${BACKEND_URL}/auth/jwt/login`, { + form: { + username: TEST_USER_EMAIL, + password: TEST_USER_PASSWORD, + grant_type: "password", + }, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }); + + expect( + response.ok(), + `Login to ${BACKEND_URL}/auth/jwt/login failed (${response.status()}). ` + + `Check that the backend is running and that PLAYWRIGHT_TEST_EMAIL ` + + `(${TEST_USER_EMAIL}) is seeded with PLAYWRIGHT_TEST_PASSWORD. ` + + `Body: ${await response.text()}` + ).toBeTruthy(); + + const { access_token } = (await response.json()) as { access_token: string }; + expect(access_token, "Backend response missing access_token").toBeTruthy(); + + await page.addInitScript( + ({ key, token }) => { + localStorage.setItem(key, token); + }, + { key: STORAGE_KEY, token: access_token } + ); + + await page.goto("/dashboard"); + await expect(page).toHaveURL(/\/dashboard/); + + await page.context().storageState({ path: authFile }); +}); diff --git a/surfsense_web/tests/dashboard.spec.ts b/surfsense_web/tests/dashboard.spec.ts new file mode 100644 index 000000000..406a0dbda --- /dev/null +++ b/surfsense_web/tests/dashboard.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from "@playwright/test"; + +/** + * Tracer-bullet test: proves the entire E2E pipeline works end-to-end. + * + * Verifies: + * - Web server is reachable + * - Auth setup ran successfully (storageState contains valid token) + * - Dashboard route renders for an authenticated user + * + * Keep this test minimal. Product-specific behaviour belongs in dedicated + * spec files (new-chat, search-spaces, editor-panel, etc.). + */ +test.describe("Dashboard", () => { + test("loads dashboard with sidebar navigation for authenticated user", async ({ page }) => { + await page.goto("/dashboard"); + + await expect(page).toHaveURL(/\/dashboard/); + await expect(page.getByRole("navigation").first()).toBeVisible(); + }); +});