Newer
Older
bugtrail / packages / extension / verify-cursor.mjs
/** 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");