/** Verify region select + full-page stitch in the extension overlay. */
import { chromium } from "playwright-core";
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
const root = dirname(fileURLToPath(import.meta.url));
const SERVER = "http://localhost:8001";
const PAGE_PORT = 8898;
const stamp = Date.now();
const EMAIL = `region-e2e-${stamp}@example.com`;
const PASSWORD = "region-pass-1";
// tall test page (2500px) with two colored blocks
const pageHtml = `<!doctype html><html><head><title>Region target</title></head>
<body style="margin:0;font-family:sans-serif">
<div style="height:800px;background:#233">Top block</div>
<div id="mid" style="height:800px;background:#5a2">Middle block <button id="target" style="margin:40px">Buggy</button></div>
<div style="height:900px;background:#235">Bottom block</div>
</body></html>`;
const httpServer = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(pageHtml);
});
await new Promise((r) => httpServer.listen(PAGE_PORT, r));
async function api(path, options = {}) {
const r = await fetch(SERVER + path, options);
if (!r.ok) throw new Error(`API ${path} -> ${r.status}: ${await r.text()}`);
return r.json();
}
await api("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nickname: `region-e2e-${stamp}`, email: EMAIL, password: PASSWORD }),
});
const login = await api("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Client": "extension" },
body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
});
const auth = { Authorization: `Bearer ${login.token}` };
const project = await api("/api/projects", {
method: "POST",
headers: { "Content-Type": "application/json", ...auth },
body: JSON.stringify({ name: "Region E2E" }),
});
const executablePath = join(homedir(), ".cache/ms-playwright/chromium-1234/chrome-linux64/chrome");
const context = await chromium.launchPersistentContext("", {
executablePath,
headless: true,
args: [
`--disable-extensions-except=${join(root, "dist/chrome")}`,
`--load-extension=${join(root, "dist/chrome")}`,
],
});
let [worker] = context.serviceWorkers();
if (!worker) worker = await context.waitForEvent("serviceworker", { timeout: 10000 });
// sign in through options
const options = await context.newPage();
await options.goto(`chrome-extension://${new URL(worker.url()).host}/src/options/options.html`);
await options.fill('input[type="email"]', EMAIL);
await options.fill('input[type="password"]', PASSWORD);
await options.click("text=Sign in");
await options.waitForSelector("text=Default project", { timeout: 10000 });
await options.selectOption("select", project.id);
await options.close();
const page = await context.newPage({ viewport: { width: 1000, height: 700 } });
await page.goto(`http://localhost:${PAGE_PORT}/`);
await worker.evaluate(() => self.__lttTriggerAction());
await page.waitForTimeout(1000);
await page.locator("#target").scrollIntoViewIfNeeded();
const box = await page.locator("#target").boundingBox();
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
// --- case 1: drag a region over the middle block ---
await page.waitForTimeout(1200); // capture lands, selector visible
await page.screenshot({ path: "/tmp/ltt-shots/18-region-selector.png" });
const mid = await page.locator("#mid").boundingBox();
await page.mouse.move(mid.x + 30, mid.y + 30);
await page.mouse.down();
await page.mouse.move(mid.x + 500, mid.y + 400, { steps: 6 });
await page.mouse.up();
await page.waitForTimeout(1500); // composer mount
await page.keyboard.type("Region shot");
await page.keyboard.press("Tab");
await page.keyboard.type("cropped");
await page.keyboard.press("Control+Enter");
await page.waitForTimeout(4000);
const reports1 = await api(`/api/projects/${project.id}/reports`, { headers: auth });
const items1 = reports1.items ?? reports1;
if (!items1.length) throw new Error("no report after region submit");
const report1 = await api(`/api/reports/${items1[0].share_token}`);
const shot1 = report1.attachments[0];
console.log("region shot:", JSON.stringify({ filename: shot1.filename, size: shot1.size }));
const dim1 = await pngSize(SERVER, items1[0].share_token, shot1.file_id);
console.log("region dims:", JSON.stringify(dim1), "(expect ~470x400 @dpr1)");
// --- case 2: whole page (Enter) → stitched 1000x2500 ---
const page2 = await context.newPage({ viewport: { width: 1000, height: 700 } });
await page2.goto(`http://localhost:${PAGE_PORT}/`);
await worker.evaluate(() => self.__lttTriggerAction());
await page2.waitForTimeout(1000);
await page2.locator("#target").scrollIntoViewIfNeeded();
const box2 = await page2.locator("#target").boundingBox();
await page2.mouse.click(box2.x + box2.width / 2, box2.y + box2.height / 2);
await page2.waitForTimeout(1200);
await page2.keyboard.press("Enter"); // whole page
await page2.waitForTimeout(2500); // stitching scrolls the page
await page2.screenshot({ path: "/tmp/ltt-shots/19-fullpage-stitching.png" });
await page2.waitForTimeout(5000); // composer mounts after stitching
await page2.keyboard.type("Full page shot");
await page2.keyboard.press("Tab");
await page2.keyboard.type("stitched");
await page2.keyboard.press("Control+Enter");
await page2.waitForTimeout(6000);
const items2 = (await api(`/api/projects/${project.id}/reports`, { headers: auth })).items;
const report2 = await api(`/api/reports/${items2[0].share_token}`);
const dim2 = await pngSize(SERVER, items2[0].share_token, report2.attachments[0].file_id);
console.log("fullpage dims:", JSON.stringify(dim2), "(expect 1000x2500 @dpr1)");
await context.close();
httpServer.close();
async function pngSize(server, reportToken, fileId) {
const r = await fetch(`${server}/api/reports/by-token/${reportToken}/files/${fileId}`);
if (!r.ok) throw new Error(`file fetch -> ${r.status}`);
const buf = Buffer.from(await r.arrayBuffer());
return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) };
}