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
75 lines
2.8 KiB
TypeScript
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');
|
|
}
|