AkurAI Build
Menu

Public repository / ecdd82c68dc11022c41dbf1ff22cac68e002a3e1

AkurAI-Mail / scripts/webmail-shortcuts-e2e.ts

Latest change c3ef99b092caa55fb6a3eed9d3e776734cc99e97 - OLI-241: framework migration plan; shortcuts slice onto AkurAI-Framework signals runtime by Ólafur Búi Ólafsson

// Browser check: mock API + built site + headless chromium over CDP. Run: bun run build && bun scripts/webmail-shortcuts-e2e.ts [out.png]
import { spawn } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { extname, join } from "node:path";

const OUT = process.argv[2] ?? "webmail-list.png";
const SITE_PORT = 4173;
const CDP_PORT = 9333;
const MESSAGES = Array.from({ length: 40 }, (_, i) => ({ id: `m${i}`, from: `Sender ${i} <s${i}@example.com>`, subject: `Subject ${i}`, preview: `Preview ${i}`, date: "2026-10-01T08:00:00Z", unread: i % 3 === 0 }));
let preferences: Record<string, unknown> = {};
const posts: any[] = [];
const json = (body: unknown) => new Response(JSON.stringify(body), { headers: { "content-type": "application/json" } });
const MIME: Record<string, string> = { ".js": "text/javascript", ".css": "text/css", ".html": "text/html", ".svg": "image/svg+xml" };

const server = Bun.serve({
  port: SITE_PORT,
  async fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/api/auth/check") return json({ authenticated: true, email: "me@example.com", admin: false });
    if (url.pathname === "/api/webmail") {
      if (req.method === "POST") {
        const body = await req.json();
        posts.push(body);
        if (body.action === "read") return json({ ok: true, message: { ...MESSAGES.find((m) => m.id === body.id), body: "hello", to: "me@example.com" } });
        if (body.action === "config" && body.kind === "preferences") preferences = body.value;
        return json({ ok: true });
      }
      return json({ folders: [{ name: "Inbox", path: "Inbox", unread: 14 }], messages: MESSAGES, total: MESSAGES.length, preferences });
    }
    const file = join("build", url.pathname);
    if (existsSync(file) && !file.endsWith("/")) return new Response(readFileSync(file), { headers: { "content-type": MIME[extname(file)] ?? "application/octet-stream" } });
    return new Response(readFileSync("build/index.html"), { headers: { "content-type": "text/html" } });
  },
});

const chrome = spawn("chromium", ["--headless=new", `--remote-debugging-port=${CDP_PORT}`, "--no-sandbox", "--window-size=1400,900", "--user-data-dir=" + (process.env.PAPERCLIP_RUN_SCRATCH_DIR ?? "/tmp") + "/webmail-e2e-profile", "about:blank"], { stdio: "ignore" });
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
let targets: any[] = [];
for (let i = 0; i < 50 && !targets.length; i++) {
  await sleep(200);
  targets = await fetch(`http://127.0.0.1:${CDP_PORT}/json`).then((r) => r.json()).catch(() => []);
}
const ws = new WebSocket(targets.find((t) => t.type === "page").webSocketDebuggerUrl);
await new Promise((r) => (ws.onopen = r));
let nextId = 1;
const pending = new Map<number, (v: any) => void>();
ws.onmessage = (m) => { const d = JSON.parse(String(m.data)); pending.get(d.id)?.(d); };
const send = (method: string, params: object = {}) => new Promise<any>((res) => { const id = nextId++; pending.set(id, res); ws.send(JSON.stringify({ id, method, params })); });
const evalJs = async (expression: string) => (await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true })).result.result.value;
const press = async (key: string, text = key.length === 1 ? key : undefined) => {
  await send("Input.dispatchKeyEvent", { type: "keyDown", key, text });
  await send("Input.dispatchKeyEvent", { type: "keyUp", key });
  await sleep(300);
};
const selectedSubject = () => evalJs(`document.querySelector("[data-message-list] button.selected strong")?.textContent ?? null`);
const failures: string[] = [];
const check = (name: string, ok: boolean, detail?: unknown) => { console.log(`${ok ? "PASS" : "FAIL"} ${name}${ok ? "" : " " + JSON.stringify(detail)}`); if (!ok) failures.push(name); };

await send("Page.enable");
await send("Page.navigate", { url: `http://127.0.0.1:${SITE_PORT}/webmail` });
await sleep(2500);
await send("Page.screenshot").then(() => {});
check("list renders", (await evalJs(`document.querySelectorAll("[data-message-list] button").length`)) > 10);

await press("Enter");
check("Enter opens first message", (await selectedSubject()) === "Subject 0", await selectedSubject());
for (let i = 0; i < 30; i++) await press("j");
check("j moves selection", (await selectedSubject()) === "Subject 30", await selectedSubject());
check("selected row scrolled into view", await evalJs(`(() => { const r = document.querySelector("[data-message-list] button.selected").getBoundingClientRect(); return r.top >= 0 && r.bottom <= innerHeight; })()`));
const shot = await send("Page.captureScreenshot", { format: "png" });
await Bun.write(OUT, Buffer.from(shot.result.data, "base64"));

await press("#");
check("# asks to delete", await evalJs(`/delete/i.test(document.body.innerText)`));
await press("Escape");

await evalJs(`document.querySelector('[aria-label="Open settings"]').click()`);
await sleep(300);
const clickToggle = () => evalJs(`[...document.querySelectorAll("label.setting-copy")].find(l => l.textContent.includes("Keyboard shortcuts")).querySelector("input").click()`);
await clickToggle();
await sleep(800);
check("toggle off persists to server preferences", preferences.shortcuts === false, preferences);
await press("Escape");
const before = await selectedSubject();
await press("j");
check("shortcuts disabled: j does nothing", (await selectedSubject()) === before, await selectedSubject());
await evalJs(`document.querySelector('[aria-label="Open settings"]').click()`);
await sleep(300);
await clickToggle();
await sleep(800);
check("toggle on persists", preferences.shortcuts === true, preferences);

ws.close(); chrome.kill(); server.stop();
console.log(`screenshot: ${OUT}; posts: ${posts.map((p) => p.action).join(",")}`);
process.exit(failures.length ? 1 : 0);