From 52e0cfaa832ff940ab8ac3a7e9ef07d9e6c5d381 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 20 Sep 2026 13:11:38 -0400 Subject: [PATCH] ci(e2e): run the tagged specs under WebKit as the Safari/iOS proxy (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new Playwright projects — 'webkit' (Desktop Safari) for tests tagged @webkit and 'iphone' (iPhone 14 descriptor) for @ios — covering boot, login + send/receive, the thread panel and the gallery lightbox. The CI e2e job installs webkit next to chromium. WebKit reports handled fetch failures (well-known probes, a wasm fetch cut short by our own navigation) as page errors with its own wording, so the benign allowlist now applies to page errors as well. Locally: 8/8 green twice in a row against the local Synapse. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .gitea/workflows/ci.yml | 7 +++++-- LOTUS_TESTING.md | 2 ++ e2e/boot.spec.ts | 4 ++-- e2e/helpers.ts | 11 ++++++++++- e2e/local-homeserver.spec.ts | 6 +++--- package.json | 2 +- playwright.config.ts | 11 ++++++++++- 7 files changed, 33 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 13bdf9483..19c70aa6c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -275,8 +275,11 @@ jobs: sleep $((attempt * 15)) done - - name: Install Playwright Chromium - run: npx playwright install --with-deps chromium + # [Gitea #221] WebKit too — Playwright's Safari/iOS proxy for the tagged + # subset (see playwright.config.ts projects). `--with-deps` pulls the + # GTK/GStreamer libraries WebKit needs on the runner. + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium webkit - name: Build run: npm run build diff --git a/LOTUS_TESTING.md b/LOTUS_TESTING.md index 84b9b4398..fcbba2b4d 100644 --- a/LOTUS_TESTING.md +++ b/LOTUS_TESTING.md @@ -57,6 +57,8 @@ Browser-level smoke tests under `e2e/` (config: `playwright.config.ts`). They bo | **E2EE composer** (gated) | `e2e/e2ee-composer.spec.ts` | only when `E2E_HOMESERVER`, `E2E_USER`, `E2E_PASSWORD` are all set | password login → `/home/create/` with the End-to-End Encryption switch on (asserts `createRoom` carries `m.room.encryption`) → text message renders → attach a generated JPEG with "Compress image before uploading" ticked, image renders → every `PUT …/rooms/*/send/*` was `m.room.encrypted` with `ciphertext` and no plaintext `body` / `url` / `file` / `mxc://` | | **Local homeserver** (Gitea #220) | `e2e/local-homeserver.spec.ts` | when a Synapse answers at `E2E_LOCAL_HS` (default `http://localhost:8008`); the CI `e2e` job starts one with `scripts/dev-homeserver.sh start` + `dev-seed.py`, locally run the same two commands | registers its own `e2e_alice_*`/`e2e_bob_*` users and rooms over the CS API, then drives the built client: login + send/receive, own message scrolls into view (#212), `/kick` failure toast (#216), upload 413 sentence (#213), forward provenance header, thread panel + drawer at 1400 px (#218), timeline image → gallery lightbox (#219), clock-skew banner via `page.clock` (#158), status save under the presence rate limit (#226), long-press action sheet on a Pixel 7 emulation (#166). Helpers in `e2e/localHs.ts`; add a test here whenever a fix was reproduced with a scratch Playwright script | +**Browsers** (Gitea #221): everything runs under Chromium; the tests tagged `@webkit` (boot, login + send/receive, thread panel, lightbox) also run under Playwright's WebKit as desktop Safari, and those tagged `@ios` under the `iPhone 14` descriptor — the closest CI gets to Safari/iOS. It catches WebKit-only breakage (CSS, `dvh`, IndexedDB, media decode) but does not emulate the on-screen keyboard or Home-Screen install; a real iPhone pass (#166/#199) stays manual. Locally: `npx playwright install --with-deps webkit` once, then `npx playwright test --project=webkit --project=iphone`. WebKit words handled fetch failures as page errors (`TypeError: Load failed`, `due to access control checks`), so the allowlist in `e2e/helpers.ts` applies to page errors too. + **CI secrets** (Gitea → repo → Settings → Actions → Secrets; the `e2e` job forwards them via `env:`; until they exist the E2EE tier reports `skipped`, the boot tier still runs): - `E2E_HOMESERVER` — server name as typed on the login page (e.g. `matrix.example.org`). Must offer `m.login.password`; a next-gen-auth (MAS/OIDC-issuer) server shows only the OIDC button and the tier will fail at the username field. diff --git a/e2e/boot.spec.ts b/e2e/boot.spec.ts index c8d986027..d8e1d53b9 100644 --- a/e2e/boot.spec.ts +++ b/e2e/boot.spec.ts @@ -5,7 +5,7 @@ import { collectConsole } from './helpers'; // `vite preview` (see playwright.config.ts webServer). No homeserver needed. test.describe('boot', () => { - test('client boots to the login screen without errors', async ({ page }) => { + test('client boots to the login screen without errors @webkit @ios', async ({ page }) => { const consoleLog = collectConsole(page); await page.goto('/'); @@ -24,7 +24,7 @@ test.describe('boot', () => { expect(consoleLog.unexpected(), 'unexpected console/page errors during boot').toEqual([]); }); - test('service worker script is served and registers', async ({ page }) => { + test('service worker script is served and registers @webkit @ios', async ({ page }) => { const swResponse = await page.request.get('/sw.js'); expect(swResponse.status(), 'GET /sw.js').toBe(200); expect(swResponse.headers()['content-type'] ?? '').toMatch(/javascript/); diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 500036fa0..c18fc270e 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -18,6 +18,11 @@ const BENIGN_CONSOLE_PATTERNS: RegExp[] = [ // autodiscovery probes https://localhost/, which is either not listening // (CI) or a self-signed dev server (local calls stack). /ERR_CONNECTION_REFUSED|ERR_CERT_AUTHORITY_INVALID|ERR_SSL_PROTOCOL_ERROR/i, + // WebKit's spellings of the same discovery failures (#221). + /Unacceptable TLS certificate|Could not connect to the server|TypeError: Load failed/i, + // Also a fetch cut short by our own navigation (e.g. the crypto wasm while + // the test moves from /home to a room) — WebKit words that the same way. + /due to access control checks/i, // React devtools hint in production bundles. /Download the React DevTools/i, ]; @@ -45,8 +50,12 @@ export function collectConsole(page: Page): ConsoleCollector { return { errors, pageErrors, + // WebKit surfaces handled fetch failures (well-known probes) as page + // errors rather than console lines, so the allowlist applies to both. unexpected: () => [ - ...pageErrors.map((m) => `pageerror: ${m}`), + ...pageErrors + .filter((m) => !BENIGN_CONSOLE_PATTERNS.some((re) => re.test(m))) + .map((m) => `pageerror: ${m}`), ...errors.filter((m) => !BENIGN_CONSOLE_PATTERNS.some((re) => re.test(m))), ], }; diff --git a/e2e/local-homeserver.spec.ts b/e2e/local-homeserver.spec.ts index f632cc4c1..78f02e23d 100644 --- a/e2e/local-homeserver.spec.ts +++ b/e2e/local-homeserver.spec.ts @@ -28,7 +28,7 @@ test.describe('local homeserver regression', () => { bob = await ensureUser(uniq('e2e_bob_')); }); - test('logs in, opens a room, sends and receives', async ({ page }) => { + test('logs in, opens a room, sends and receives @webkit @ios', async ({ page }) => { const console_ = collectConsole(page); const room = await createRoom(alice, 'Regression Room', { invite: [bob.userId] }); await joinRoom(bob, room); @@ -140,7 +140,7 @@ test.describe('local homeserver regression', () => { await expect(page.getByText('the original message')).toBeVisible(); }); - test('thread panel: opens from the chip and yields the member drawer at 1400px (#218)', async ({ + test('thread panel: opens from the chip and yields the member drawer at 1400px (#218) @webkit', async ({ browser, }) => { const ctx = await browser.newContext({ viewport: { width: 1400, height: 850 } }); @@ -169,7 +169,7 @@ test.describe('local homeserver regression', () => { await ctx.close(); }); - test('timeline image opens the gallery lightbox (#219)', async ({ page }) => { + test('timeline image opens the gallery lightbox (#219) @webkit', async ({ page }) => { const room = await createRoom(alice, 'Lightbox Room'); const png = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', diff --git a/package.json b/package.json index 9c82e31b5..25c6c48d0 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "typecheck": "tsc --noEmit", "test": "node --import tsx --test $(find src -name '*.test.ts')", "test:e2e": "playwright test", - "test:e2e:install": "playwright install chromium", + "test:e2e:install": "playwright install chromium webkit", "prepare": "husky", "commit": "git-cz", "postinstall": "node scripts/patch-folds.mjs", diff --git a/playwright.config.ts b/playwright.config.ts index 227273f98..b2c28f314 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,7 +25,16 @@ export default defineConfig({ trace: 'retain-on-failure', ...devices['Desktop Chrome'], }, - projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + // [Gitea #221] WebKit as the Safari/iOS proxy. It runs the subset tagged + // @webkit / @ios (boot, composer round-trip, thread panel, lightbox): it + // catches WebKit-only breakage (CSS, dvh, IndexedDB, media decode) that + // Chromium emulation can't, but does not emulate the iOS keyboard or the + // Home-Screen install flow. + { name: 'webkit', use: { ...devices['Desktop Safari'] }, grep: /@webkit/ }, + { name: 'iphone', use: { ...devices['iPhone 14'] }, grep: /@ios/ }, + ], webServer: { command: `npx vite preview --port ${PORT} --strictPort`, url: BASE_URL,