/** Visual check: the virtual cursor walking the recorded path during replay. */
import { chromium } from "playwright-core";
import { createServer } from "node:http";
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 pageHtml = `<!doctype html><html><head><title>Cursor target</title></head>
<body style="font-family:sans-serif;padding:40px">
<h1>Hover me</h1>
<button id="target" style="padding:20px 40px">Target button</button>
</body></html>`;
const httpServer = createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/html" }); res.end(pageHtml); });
await new Promise((resolve) => httpServer.listen(PAGE_PORT, resolve));
async function api(path, options = {}) {
const response = await fetch(SERVER + path, options);
if (!response.ok) throw new Error(`${path} -> ${response.status}: ${await response.text()}`);
return response.json();
}
const email = `cursor-${stamp}@example.com`;
const login = await api("/api/auth/login", {
method: "POST", headers: { "Content-Type": "application/json", "X-Client": "extension" },
body: JSON.stringify({ email, password: "cursor-password-1" }),
}).catch(async () => {
await api("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nickname: `cursor-${stamp}`, email, password: "cursor-password-1" }) });
return api("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json", "X-Client": "extension" },
body: JSON.stringify({ email, password: "cursor-password-1" }) });
});
const project = await api("/api/projects", {
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${login.token}` },
body: JSON.stringify({ name: "Cursor check" }),
});
// a synthetic track: a zigzag mouse path over 2.5 s ending on the button
const points = [];
for (let i = 0; i < 40; i++) {
points.push({ t: i * 50, x: 60 + i * 12, y: 120 + (i % 2) * 90 });
}
points.push({ t: 2050, x: 160, y: 165 });
const report = await api("/api/reports", {
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${login.token}` },
body: JSON.stringify({
project_id: project.id, type: "recording", title: "Cursor replay check",
page_url: `http://localhost:${PAGE_PORT}/`,
environment: { viewport: { w: 1280, h: 720 } },
steps: [{ type: "click", offset_ms: 2100, data: { element: { selector: "#target", unique_selector: "#target" }, button: 0 } }],
mouse_track: { viewport: { w: 1280, h: 720 }, points },
}),
});
const executablePath = join(homedir(), ".cache/ms-playwright/chromium-1234/chrome-linux64/chrome");
const context = await chromium.launchPersistentContext("", {
executablePath, headless: true,
args: [`--disable-extensions-except=${root}/dist/chrome`, `--load-extension=${root}/dist/chrome`],
});
let [worker] = context.serviceWorkers();
if (!worker) worker = await context.waitForEvent("serviceworker", { timeout: 10000 });
const extensionId = new URL(worker.url()).host;
const options = await context.newPage();
await options.goto(`chrome-extension://${extensionId}/src/options/options.html`);
await options.fill('input[type="email"]', email);
await options.fill('input[type="password"]', "cursor-password-1");
await options.click("text=Sign in");
await options.waitForSelector("text=Default project", { timeout: 10000 });
await options.selectOption("select", project.id);
// trigger the replay from the panel page exactly like the button does
const panel = await context.newPage();
await panel.goto(`${SERVER.replace("8001", "5173")}/r/${report.share_token}`);
await panel.waitForTimeout(2000);
void panel.evaluate(
(token) => window.postMessage({ source: "bugtrail-panel", type: "replay_report", token }, window.location.origin),
report.share_token
);
// catch the replay tab mid-animation and screenshot the moving cursor
let replayPage = null;
for (let i = 0; i < 60 && !replayPage; i++) {
await new Promise((r) => setTimeout(r, 250));
replayPage = context.pages().find((c) => c !== panel && c.url().includes(`localhost:${PAGE_PORT}`));
}
if (!replayPage) throw new Error("replay tab never opened");
for (let i = 0; i < 40; i++) {
const has = await replayPage.evaluate(() => Boolean(document.getElementById("ltt-cursor-host"))).catch(() => false);
if (has) break;
await replayPage.waitForTimeout(200);
}
await replayPage.waitForTimeout(600); // let it move a bit along the path
await replayPage.screenshot({ path: "/tmp/ltt-shots/30-replay-cursor.png" });
const pos = await replayPage.evaluate(() => document.getElementById("ltt-cursor-host")?.style.transform ?? null);
console.log("cursor transform mid-replay:", pos);
await new Promise((r) => setTimeout(r, 4000));
const posEnd = await replayPage.evaluate(() => document.getElementById("ltt-cursor-host")?.style.transform ?? null);
console.log("cursor transform after steps:", posEnd);
await context.close();
httpServer.close();
console.log("done");