2026-06-13 10:15:23 +08:00
|
|
|
import { randomBytes } from "node:crypto";
|
2026-03-22 19:28:57 +08:00
|
|
|
import path from "node:path";
|
2026-03-25 09:27:12 +08:00
|
|
|
import { defineConfig, devices } from "@playwright/test";
|
2026-03-22 19:28:57 +08:00
|
|
|
|
2026-04-30 16:11:33 +08:00
|
|
|
const authFile = path.join(__dirname, ".playwright", ".auth", "user.json");
|
2026-04-24 01:24:15 +08:00
|
|
|
|
|
|
|
|
const TEST_WEB_PORT = 2349;
|
2026-03-22 19:28:57 +08:00
|
|
|
|
2026-06-13 10:15:23 +08:00
|
|
|
// Fresh Postgres database per e2e run. The create-db script (chained before
|
|
|
|
|
// the API in the webServer command) CREATEs it and cleans up stale databases
|
|
|
|
|
// from previous runs. The API auto-migrates the empty database at boot.
|
|
|
|
|
const E2E_PG_BASE_URL =
|
|
|
|
|
process.env.E2E_PG_BASE_URL || "postgres://snapotter:snapotter@localhost:5432/snapotter";
|
|
|
|
|
const e2eDbName = `snapotter_e2e_${process.pid}_${randomBytes(4).toString("hex")}`;
|
|
|
|
|
const e2eDatabaseUrl = (() => {
|
|
|
|
|
const url = new URL(E2E_PG_BASE_URL);
|
|
|
|
|
url.pathname = `/${e2eDbName}`;
|
|
|
|
|
return url.toString();
|
|
|
|
|
})();
|
|
|
|
|
|
2026-06-10 22:01:13 +08:00
|
|
|
// Specs that mutate global server state (settings, users, roles, API keys)
|
|
|
|
|
// or assert on global lists/timing. These run in the chromium-serial project
|
|
|
|
|
// with --workers=1; everything else parallelizes safely.
|
|
|
|
|
const SERIAL_SPECS =
|
|
|
|
|
/gui-settings-|settings\.spec|rbac|security|people|api\.spec|state-bleed|full-session|gui-file-carry|i18n|theme|gui-performance/;
|
|
|
|
|
|
|
|
|
|
// Screenshot-comparison specs. Separate project because baselines are
|
|
|
|
|
// platform-specific: they run locally (darwin baselines) and via the
|
|
|
|
|
// update-visual-baselines workflow, but not in the nightly linux run until
|
|
|
|
|
// linux baselines are committed.
|
|
|
|
|
const VISUAL_SPECS = /visual-regression|gui-visual-/;
|
|
|
|
|
|
2026-06-20 02:15:50 +08:00
|
|
|
// Device-emulated specs (real touch, UA, DPR). Tagged @mobile or @tablet
|
|
|
|
|
// and routed to the dedicated device projects below.
|
2026-06-20 03:44:26 +08:00
|
|
|
const DEVICE_SPECS = /device-mobile|device-tablet|device-visual|device-a11y/;
|
2026-06-20 02:15:50 +08:00
|
|
|
|
2026-03-22 19:28:57 +08:00
|
|
|
export default defineConfig({
|
|
|
|
|
testDir: "./tests/e2e",
|
|
|
|
|
timeout: 30_000,
|
|
|
|
|
expect: {
|
|
|
|
|
timeout: 10_000,
|
2026-03-23 11:46:45 +08:00
|
|
|
toHaveScreenshot: {
|
|
|
|
|
maxDiffPixelRatio: 0.01,
|
|
|
|
|
animations: "disabled",
|
|
|
|
|
caret: "hide",
|
|
|
|
|
},
|
2026-03-22 19:28:57 +08:00
|
|
|
},
|
2026-06-10 22:01:13 +08:00
|
|
|
// Platform-suffixed baselines: darwin baselines serve local runs on macOS,
|
|
|
|
|
// linux baselines (generated by the update-visual-baselines workflow) serve CI.
|
|
|
|
|
snapshotPathTemplate: "{testDir}/__screenshots__/{testFilePath}/{arg}-{platform}{ext}",
|
2026-03-22 19:28:57 +08:00
|
|
|
fullyParallel: false,
|
2026-06-10 22:01:13 +08:00
|
|
|
retries: process.env.CI ? 1 : 0,
|
|
|
|
|
// Files run across workers; tests within a file stay ordered. The serial
|
|
|
|
|
// bucket is pinned to --workers=1 by its run command. Default is 2: the
|
|
|
|
|
// dev-mode webServers saturate beyond that and 30s-timeout tests start
|
|
|
|
|
// flaking. Raise via PW_WORKERS on stronger setups.
|
|
|
|
|
workers: process.env.PW_WORKERS ? Number(process.env.PW_WORKERS) : 2,
|
2026-03-22 19:28:57 +08:00
|
|
|
reporter: "html",
|
|
|
|
|
use: {
|
2026-04-24 01:24:15 +08:00
|
|
|
baseURL: `http://localhost:${TEST_WEB_PORT}`,
|
2026-03-22 19:28:57 +08:00
|
|
|
screenshot: "only-on-failure",
|
|
|
|
|
trace: "retain-on-failure",
|
|
|
|
|
},
|
|
|
|
|
projects: [
|
|
|
|
|
{
|
|
|
|
|
name: "setup",
|
|
|
|
|
testMatch: /auth\.setup\.ts/,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "chromium",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["Desktop Chrome"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
2026-06-20 02:15:50 +08:00
|
|
|
testIgnore: [SERIAL_SPECS, VISUAL_SPECS, DEVICE_SPECS],
|
2026-06-10 22:01:13 +08:00
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "chromium-serial",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["Desktop Chrome"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: SERIAL_SPECS,
|
|
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "chromium-visual",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["Desktop Chrome"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: VISUAL_SPECS,
|
2026-03-22 19:28:57 +08:00
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
2026-05-01 04:17:19 +08:00
|
|
|
{
|
|
|
|
|
name: "firefox",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["Desktop Firefox"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: /gui-cross-browser\.spec\.ts/,
|
|
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "webkit",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["Desktop Safari"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: /gui-cross-browser\.spec\.ts/,
|
|
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
2026-06-20 02:15:50 +08:00
|
|
|
|
|
|
|
|
// ---- Real device-emulated projects (touch, UA, DPR, engine) ----
|
|
|
|
|
{
|
|
|
|
|
name: "mobile-chromium",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["Pixel 7"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: DEVICE_SPECS,
|
|
|
|
|
grep: /@mobile/,
|
|
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "mobile-webkit",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["iPhone 14"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: DEVICE_SPECS,
|
|
|
|
|
grep: /@mobile/,
|
2026-06-28 18:57:53 +08:00
|
|
|
// @visual baselines are platform-suffixed but not browser-suffixed, so a
|
|
|
|
|
// single baseline cannot match both chromium and webkit rendering. Visual
|
|
|
|
|
// regression runs on the chromium device projects only; webkit covers the
|
|
|
|
|
// functional device flows.
|
|
|
|
|
grepInvert: /@visual/,
|
2026-06-20 02:15:50 +08:00
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "tablet-webkit",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["iPad (gen 7)"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: DEVICE_SPECS,
|
|
|
|
|
grep: /@tablet/,
|
2026-06-28 18:57:53 +08:00
|
|
|
grepInvert: /@visual/,
|
2026-06-20 02:15:50 +08:00
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "tablet-chromium",
|
|
|
|
|
use: {
|
|
|
|
|
...devices["Galaxy Tab S9"],
|
|
|
|
|
storageState: authFile,
|
|
|
|
|
},
|
|
|
|
|
testMatch: DEVICE_SPECS,
|
|
|
|
|
grep: /@tablet/,
|
|
|
|
|
dependencies: ["setup"],
|
|
|
|
|
},
|
2026-03-22 19:28:57 +08:00
|
|
|
],
|
|
|
|
|
webServer: [
|
|
|
|
|
{
|
2026-06-13 10:15:23 +08:00
|
|
|
command: `node tests/e2e-pg-create-db.cjs ${e2eDbName} && pnpm --filter @snapotter/api dev`,
|
2026-03-23 16:38:27 +08:00
|
|
|
port: 13490,
|
2026-03-22 19:28:57 +08:00
|
|
|
reuseExistingServer: !process.env.CI,
|
|
|
|
|
env: {
|
|
|
|
|
AUTH_ENABLED: "true",
|
|
|
|
|
DEFAULT_USERNAME: "admin",
|
|
|
|
|
DEFAULT_PASSWORD: "admin",
|
2026-03-23 11:46:45 +08:00
|
|
|
RATE_LIMIT_PER_MIN: "50000",
|
2026-06-28 18:57:53 +08:00
|
|
|
// The login route has its own per-minute attempt cap (default 10). The
|
|
|
|
|
// RBAC/settings specs sign in many times in quick succession, so raise
|
|
|
|
|
// it well above any single run to avoid 429s that cascade into
|
|
|
|
|
// create-user 401s and login timeouts.
|
|
|
|
|
LOGIN_ATTEMPT_LIMIT: "100000",
|
2026-03-28 11:19:09 +08:00
|
|
|
SKIP_MUST_CHANGE_PASSWORD: "true",
|
2026-04-24 01:24:15 +08:00
|
|
|
ANALYTICS_ENABLED: "false",
|
2026-06-28 18:57:53 +08:00
|
|
|
// The api-keys management routes cap at 30/min per IP in production. The
|
|
|
|
|
// serial api-keys specs hit the list endpoint far more than that on a
|
|
|
|
|
// shared IP, so raise the cap well above any single run.
|
|
|
|
|
API_KEYS_RATE_LIMIT_PER_MIN: "100000",
|
2026-06-13 10:15:23 +08:00
|
|
|
DATABASE_URL: e2eDatabaseUrl,
|
2026-06-13 10:17:13 +08:00
|
|
|
REDIS_URL: process.env.REDIS_URL ?? "redis://localhost:6379",
|
|
|
|
|
BULLMQ_PREFIX: e2eDbName,
|
2026-06-10 22:01:13 +08:00
|
|
|
// The in-repo docker/feature-manifest.json makes the API think it is
|
|
|
|
|
// inside Docker and try to mkdir /data; point it somewhere writable.
|
|
|
|
|
DATA_DIR: path.join(__dirname, "test-results", ".e2e-data"),
|
2026-06-28 18:57:53 +08:00
|
|
|
// Point feature-manifest detection at a path that does not exist so the
|
|
|
|
|
// API runs in native mode: it reports every AI bundle as available
|
|
|
|
|
// (apps/api/src/routes/features.ts) instead of gating each AI tool
|
|
|
|
|
// behind a multi-GB "requires an additional download" install prompt.
|
|
|
|
|
// The e2e env never downloads the model bundles, so without this every
|
|
|
|
|
// AI-tool GUI test is blocked before reaching a control. Real inference
|
|
|
|
|
// still no-ops, but the AI GUI specs assert on settings only and the
|
|
|
|
|
// processing specs skip when the sidecar is absent.
|
|
|
|
|
FEATURE_MANIFEST_PATH: path.join(__dirname, "test-results", ".no-feature-manifest.json"),
|
2026-03-22 19:28:57 +08:00
|
|
|
},
|
|
|
|
|
timeout: 30_000,
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-06-10 22:01:13 +08:00
|
|
|
// Production build + static preview: the dev server's on-demand
|
|
|
|
|
// transform saturates under parallel workers and flakes 30s-timeout
|
|
|
|
|
// tests. The build adds ~40s once per run and removes that whole class.
|
|
|
|
|
command: "pnpm --filter @snapotter/web build && pnpm --filter @snapotter/web preview",
|
2026-04-24 01:24:15 +08:00
|
|
|
port: TEST_WEB_PORT,
|
2026-03-22 19:28:57 +08:00
|
|
|
reuseExistingServer: !process.env.CI,
|
2026-04-24 01:24:15 +08:00
|
|
|
env: {
|
|
|
|
|
PORT: String(TEST_WEB_PORT),
|
|
|
|
|
VITE_API_URL: "http://localhost:13490",
|
|
|
|
|
},
|
2026-06-10 22:01:13 +08:00
|
|
|
timeout: 240_000,
|
2026-03-22 19:28:57 +08:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export { authFile };
|