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-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-10 22:01:13 +08:00
|
|
|
testIgnore: [SERIAL_SPECS, VISUAL_SPECS],
|
|
|
|
|
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-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-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-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-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 };
|