Files
SnapOtter/playwright.config.ts
T
SnapOtter e44be61053 test: add real device-emulated mobile and tablet testing (phase 3)
Replace the fake resized-desktop mobile specs with real Playwright device
projects (Pixel 7, iPhone 14, iPad gen 7, Galaxy Tab S9) that exercise
real touch, mobile UA, DPR, and WebKit engine.

Device projects in playwright.config.ts:
- mobile-chromium (Pixel 7, 412x839, Chromium)
- mobile-webkit (iPhone 14, 390x664, WebKit)
- tablet-webkit (iPad gen 7, 810x1080, WebKit)
- tablet-chromium (Galaxy Tab S9, 640x1024, Chromium)

Device specs (16 mobile, 10 tablet):
- Core flow: navigate to tool, upload, process, download
- Responsive chrome: bottom-nav, sidebar hidden, search, overflow
- Touch interactions: before-after slider, crop canvas
- Editor gate: phone asserts "Desktop Recommended" message
- Editor tablet: iPad (810px) renders canvas, Galaxy Tab (640px) shows gate
- SSE visibility-recovery regression guard
- RTL Arabic locale responsive check

Key finding: Galaxy Tab S9 viewport (640px) is below the 768px mobile
breakpoint, so useMobile() returns true and the editor shows the mobile
gate. Only iPad gen 7 (810px) is classified as non-mobile.

Component tests (17 tests, Vitest/jsdom):
- use-mobile hook: breakpoint behavior across all 4 device widths
- mobile-bottom-nav: render, navigation links, settings callback, icons

CI wiring:
- ci.yml: mobile-chromium smoke job (PR gate)
- nightly.yml: full device matrix with webkit
- update-visual-baselines.yml: webkit + device projects for goldens

Parity: 13218 passed, 0 dropped (PARITY OK)
2026-06-20 02:15:50 +08:00

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/;
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 };