Files
cinny/e2e/helpers.ts
T
jaredandClaude Opus 5 0cb0f91e43 test(e2e): Playwright smoke test — boot tier always, E2EE composer tier with credentials
Boot tier (runs against vite preview of dist/): login screen renders with
no page or console errors, sw.js is served and registers, the bundled
Element Call mounts in a frame with no failed asset requests. E2EE tier
(skipped without E2E_HOMESERVER/E2E_USER/E2E_PASSWORD): password login,
create an encrypted room, send text, attach a compressed JPEG, and assert
at the network level that every send is m.room.encrypted with no
plaintext body/url/file — the regression test #6/#7/#11 lacked.
Secrets and local usage documented in LOTUS_TESTING.md.

Fixes #90

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-13 00:56:23 -04:00

75 lines
2.8 KiB
TypeScript

import { Page } from '@playwright/test';
// Console noise that is expected on a clean boot and must not fail the smoke
// test. Keep this list short and specific — every entry should name a known,
// understood source.
const BENIGN_CONSOLE_PATTERNS: RegExp[] = [
// README: after login you may see a 404 for a missing avatar thumbnail —
// "not a login failure". Also covers the generic resource-404 console line.
/_matrix\/(client|media)\/v\d+\/(media\/)?thumbnail/i,
/Failed to load resource: the server responded with a status of 404/i,
// The login page probes `POST /_matrix/client/v3/register` to learn whether
// registration is open; the homeserver answers 401 + UIA flows by design.
/Failed to load resource: the server responded with a status of 401/i,
// Homeserver discovery pings can fail on a runner with no outbound network.
/\/\.well-known\/matrix\/client/i,
/Failed to fetch|NetworkError|ERR_NAME_NOT_RESOLVED|ERR_INTERNET_DISCONNECTED/i,
// React devtools hint in production bundles.
/Download the React DevTools/i,
];
export type ConsoleCollector = {
errors: string[];
pageErrors: string[];
/** Errors not matched by the benign allowlist. */
unexpected: () => string[];
};
/**
* Records console.error lines and uncaught page errors for the given page.
* Attach BEFORE navigating so nothing emitted during boot is missed.
*/
export function collectConsole(page: Page): ConsoleCollector {
const errors: string[] = [];
const pageErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', (err) => {
pageErrors.push(err.message);
});
return {
errors,
pageErrors,
unexpected: () => [
...pageErrors.map((m) => `pageerror: ${m}`),
...errors.filter((m) => !BENIGN_CONSOLE_PATTERNS.some((re) => re.test(m))),
],
};
}
/**
* Generates a small JPEG in the browser (canvas.toBlob) and returns its bytes.
* JPEG rather than PNG so the composer's "Compress image" path actually
* re-encodes (compressImage() deliberately skips PNG to preserve alpha).
*/
export async function generateJpeg(page: Page, size = 96): Promise<Buffer> {
const dataUrl = await page.evaluate((px) => {
const canvas = document.createElement('canvas');
canvas.width = px;
canvas.height = px;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('canvas 2d context unavailable');
const grad = ctx.createLinearGradient(0, 0, px, px);
grad.addColorStop(0, '#7c3aed');
grad.addColorStop(1, '#f59e0b');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, px, px);
ctx.fillStyle = '#fff';
ctx.font = `${Math.floor(px / 4)}px sans-serif`;
ctx.fillText('e2e', px / 8, px / 2);
return canvas.toDataURL('image/jpeg', 0.95);
}, size);
return Buffer.from(dataUrl.split(',')[1], 'base64');
}