CI / Build & Quality Checks (push) Successful in 1m29s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Successful in 5s
CI / Playwright smoke (e2e) (push) Successful in 4m17s
The e2e job now runs scripts/dev-homeserver.sh start + dev-seed.py before Playwright (installing python3-venv if the runner lacks it) and stops it afterwards. e2e/local-homeserver.spec.ts registers its own users and rooms over the CS API and drives the built client — no prod secrets — covering the fixes that were reproduced with scratch scripts this week: login/send/receive, own-message scroll (#212), /kick toast (#216), upload 413 wording (#213), forward provenance, thread panel at 1400px (#218), timeline lightbox (#219), clock-skew banner via page.clock (#158), status save under the presence limit (#226), long-press action sheet on a Pixel 7 emulation (#166). Skips itself when no homeserver answers, so > lotus-chat@4.12.7-lotus test:e2e > playwright test Running 16 tests using 1 worker ✓ 1 [chromium] › e2e/boot.spec.ts:8:3 › boot › client boots to the login screen without errors (1.5s) ✓ 2 [chromium] › e2e/boot.spec.ts:27:3 › boot › service worker script is served and registers (1.4s) ✓ 3 [chromium] › e2e/boot.spec.ts:54:3 › boot › bundled Element Call loads in a frame (3.8s) - 4 [chromium] › e2e/e2ee-composer.spec.ts:67:3 › E2EE composer › logs in with a password and reaches the client - 5 [chromium] › e2e/e2ee-composer.spec.ts:84:3 › E2EE composer › creates a private encrypted room and sends a text message - 6 [chromium] › e2e/e2ee-composer.spec.ts:146:3 › E2EE composer › attaches a compressed image and it is sent encrypted ✓ 7 [chromium] › e2e/local-homeserver.spec.ts:31:3 › local homeserver regression › logs in, opens a room, sends and receives (4.5s) ✓ 8 [chromium] › e2e/local-homeserver.spec.ts:58:3 › local homeserver regression › your own message scrolls into view even after scrolling up (#212) (11.4s) ✓ 9 [chromium] › e2e/local-homeserver.spec.ts:76:3 › local homeserver regression › /kick failure is reported, not swallowed (#216) (5.4s) ✓ 10 [chromium] › e2e/local-homeserver.spec.ts:90:3 › local homeserver regression › upload failure shows a plain sentence, never the raw MatrixError (#213) (3.7s) ✓ 11 [chromium] › e2e/local-homeserver.spec.ts:122:3 › local homeserver regression › forwarded message carries its provenance header (6.7s) ✓ 12 [chromium] › e2e/local-homeserver.spec.ts:143:3 › local homeserver regression › thread panel: opens from the chip and yields the member drawer at 1400px (#218) (4.7s) ✓ 13 [chromium] › e2e/local-homeserver.spec.ts:172:3 › local homeserver regression › timeline image opens the gallery lightbox (#219) (3.9s) ✓ 14 [chromium] › e2e/local-homeserver.spec.ts:204:3 › local homeserver regression › warns when the local clock is far off the server (#158) (6.5s) ✓ 15 [chromium] › e2e/local-homeserver.spec.ts:221:3 › local homeserver regression › status save survives the presence rate limit (#226) (12.6s) ✓ 16 [chromium] › e2e/local-homeserver.spec.ts:265:3 › local homeserver regression › touch: long-press opens the message action sheet (#166) (5.0s) 3 skipped 13 passed (1.2m) still works cold. 13 pass locally against dist + the dev homeserver in 1.8 min. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
79 lines
3.1 KiB
TypeScript
79 lines
3.1 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,
|
|
// Tier 3 (local homeserver named "localhost"): the client's well-known
|
|
// 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,
|
|
// 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');
|
|
}
|