mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Add scoped axe accessibility audit (a11y.spec.ts, device-a11y.spec.ts)
scanning home, one tool per modality, editor, and login across desktop
chromium and mobile-chromium in EN and AR locales. Uses a committed
baseline (a11y-baseline.json) to gate on NEW critical/serious violations
while documenting existing debt.
Add device-visual.spec.ts with curated screenshots (home, resize tool,
settings dialog) on mobile-chromium and tablet-chromium. Six darwin
baselines generated; linux baselines deferred to the existing
update-visual-baselines workflow.
Trivial a11y fixes applied:
- Login page: outer div -> main (fixes landmark-one-main, reduces region)
- Editor page: outer div -> main for both desktop and mobile gate
- AppLayout main: add tabIndex={-1} for skip-link focusability
Updated DEVICE_SPECS regex to route device-visual and device-a11y specs.
Added @axe-core/playwright as a devDependency.
195 lines
6.0 KiB
TypeScript
195 lines
6.0 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import path from "node:path";
|
|
import { defineConfig, devices } from "@playwright/test";
|
|
|
|
const authFile = path.join(__dirname, ".playwright", ".auth", "user.json");
|
|
|
|
const TEST_WEB_PORT = 2349;
|
|
|
|
// 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();
|
|
})();
|
|
|
|
// 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-/;
|
|
|
|
// Device-emulated specs (real touch, UA, DPR). Tagged @mobile or @tablet
|
|
// and routed to the dedicated device projects below.
|
|
const DEVICE_SPECS = /device-mobile|device-tablet|device-visual|device-a11y/;
|
|
|
|
export default defineConfig({
|
|
testDir: "./tests/e2e",
|
|
timeout: 30_000,
|
|
expect: {
|
|
timeout: 10_000,
|
|
toHaveScreenshot: {
|
|
maxDiffPixelRatio: 0.01,
|
|
animations: "disabled",
|
|
caret: "hide",
|
|
},
|
|
},
|
|
// 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}",
|
|
fullyParallel: false,
|
|
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,
|
|
reporter: "html",
|
|
use: {
|
|
baseURL: `http://localhost:${TEST_WEB_PORT}`,
|
|
screenshot: "only-on-failure",
|
|
trace: "retain-on-failure",
|
|
},
|
|
projects: [
|
|
{
|
|
name: "setup",
|
|
testMatch: /auth\.setup\.ts/,
|
|
},
|
|
{
|
|
name: "chromium",
|
|
use: {
|
|
...devices["Desktop Chrome"],
|
|
storageState: authFile,
|
|
},
|
|
testIgnore: [SERIAL_SPECS, VISUAL_SPECS, DEVICE_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,
|
|
dependencies: ["setup"],
|
|
},
|
|
{
|
|
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"],
|
|
},
|
|
|
|
// ---- 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/,
|
|
dependencies: ["setup"],
|
|
},
|
|
{
|
|
name: "tablet-webkit",
|
|
use: {
|
|
...devices["iPad (gen 7)"],
|
|
storageState: authFile,
|
|
},
|
|
testMatch: DEVICE_SPECS,
|
|
grep: /@tablet/,
|
|
dependencies: ["setup"],
|
|
},
|
|
{
|
|
name: "tablet-chromium",
|
|
use: {
|
|
...devices["Galaxy Tab S9"],
|
|
storageState: authFile,
|
|
},
|
|
testMatch: DEVICE_SPECS,
|
|
grep: /@tablet/,
|
|
dependencies: ["setup"],
|
|
},
|
|
],
|
|
webServer: [
|
|
{
|
|
command: `node tests/e2e-pg-create-db.cjs ${e2eDbName} && pnpm --filter @snapotter/api dev`,
|
|
port: 13490,
|
|
reuseExistingServer: !process.env.CI,
|
|
env: {
|
|
AUTH_ENABLED: "true",
|
|
DEFAULT_USERNAME: "admin",
|
|
DEFAULT_PASSWORD: "admin",
|
|
RATE_LIMIT_PER_MIN: "50000",
|
|
SKIP_MUST_CHANGE_PASSWORD: "true",
|
|
ANALYTICS_ENABLED: "false",
|
|
DATABASE_URL: e2eDatabaseUrl,
|
|
REDIS_URL: process.env.REDIS_URL ?? "redis://localhost:6379",
|
|
BULLMQ_PREFIX: e2eDbName,
|
|
// 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"),
|
|
},
|
|
timeout: 30_000,
|
|
},
|
|
{
|
|
// 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",
|
|
port: TEST_WEB_PORT,
|
|
reuseExistingServer: !process.env.CI,
|
|
env: {
|
|
PORT: String(TEST_WEB_PORT),
|
|
VITE_API_URL: "http://localhost:13490",
|
|
},
|
|
timeout: 240_000,
|
|
},
|
|
],
|
|
});
|
|
|
|
export { authFile };
|