eldouma-graphics / tools /test-snap.mjs
Moeeldouma's picture
Deploy Eldouma Graphics β€” Docker Space, bring-your-own-key demo
39d98a0 verified
Raw
History Blame Contribute Delete
6.47 kB
// Drive a real timeline drag headlessly to prove the magnet: two clips on DIFFERENT tracks; drag the
// second so its start nears the first's END β€” assert it locks EXACTLY to that edge and a guide line shows.
import { spawn, spawnSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import path from "node:path";
import WebSocket from "ws";
const CHROME = process.env.CHROME_BIN || "chromium-browser";
const CDP_PORT = Number(process.env.CDP_PORT || (9400 + Math.floor(Math.random() * 400)));
const HOME = process.env.HOME || "/tmp";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let BROWSER = null;
async function launchChrome() {
const userDir = mkdtempSync(path.join(HOME, "elg-snap-"));
const proc = spawn(CHROME, ["--headless=new","--no-sandbox","--disable-gpu","--disable-dev-shm-usage","--no-first-run",`--remote-debugging-port=${CDP_PORT}`,`--user-data-dir=${userDir}`,"--window-size=1600,1000","about:blank"], { stdio: "ignore", detached: true });
for (let i = 0; i < 150; i++) { try { const r = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`); if (r.ok) { BROWSER = { proc, userDir, browserWsUrl: (await r.json()).webSocketDebuggerUrl }; return; } } catch {} await sleep(200); }
throw new Error("chromium CDP not up");
}
async function shutdown(code) { const b = BROWSER; if (b) { try { b.proc.kill("SIGKILL"); } catch {} try { spawnSync("pkill", ["-9", "-f", b.userDir]); } catch {} try { rmSync(b.userDir, { recursive: true, force: true }); } catch {} } process.exit(code); }
class CDP { constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map(); this.handlers = new Map(); ws.on("message", (raw) => { const m = JSON.parse(raw); if (m.id && this.pending.has(m.id)) { const { res, rej } = this.pending.get(m.id); this.pending.delete(m.id); m.error ? rej(new Error(m.error.message)) : res(m.result); } else if (m.method) (this.handlers.get(m.method) || []).forEach((h) => h(m.params)); }); } send(method, params = {}) { const id = ++this.id; return new Promise((res, rej) => { this.pending.set(id, { res, rej }); this.ws.send(JSON.stringify({ id, method, params })); }); } once(method) { return new Promise((res) => { const a = this.handlers.get(method) || []; a.push(res); this.handlers.set(method, a); }); } }
async function openPage(url) { const r = await fetch(`http://127.0.0.1:${CDP_PORT}/json/new?${encodeURIComponent(url)}`, { method: "PUT" }); const tab = await r.json(); const ws = new WebSocket(tab.webSocketDebuggerUrl, { perMessageDeflate: false, maxPayload: 64 * 1024 * 1024 }); await new Promise((res, rej) => { ws.once("open", res); ws.once("error", rej); }); const cdp = new CDP(ws); await cdp.send("Page.enable"); await cdp.send("Runtime.enable"); return cdp; }
async function evalJs(cdp, expr) { const { result, exceptionDetails } = await cdp.send("Runtime.evaluate", { expression: expr, returnByValue: true, awaitPromise: true }); if (exceptionDetails) throw new Error(exceptionDetails.exception?.description || exceptionDetails.text || "eval threw"); return result.value; }
const script = `(async () => {
const S = window.useStore; if (!S) return { error: "no store" };
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
if (!S.getState().loaded) { try { await S.getState().bootProject(); } catch (e) {} }
for (let i = 0; i < 50 && !S.getState().loaded; i++) await sleep(150);
await sleep(400);
// clean 2-track scenario (in-memory only; setState doesn't autosave β†’ demo on disk stays intact)
const t1 = { id: "tk_scenes", kind: "video", name: "S", role: "scenes" }, t2 = { id: "tk_v2", kind: "video", name: "V2" };
const A = { id: "mA", template: "bigStatement", props: { statement: "A" }, theme: "dark", accent: "#E7723B", anim: "rise", label: "AAAZ", trackId: "tk_scenes", startMs: 2000, durationMs: 2000 };
const B = { id: "mB", template: "bigStatement", props: { statement: "B" }, theme: "dark", accent: "#5DE3FF", anim: "rise", label: "BBBZ", trackId: "tk_v2", startMs: 5000, durationMs: 2000 };
window.useStore.setState({ motion: [A, B], videos: [] , tracks: [t1, t2] });
S.getState().setPlayhead(0);
await sleep(600);
const A_end = 4000;
const before = { guides: document.querySelectorAll('[class*="z-40"]').length };
// locate B's clip div by its title
const el = [...document.querySelectorAll('[title]')].find(e => (e.getAttribute('title') || '').startsWith('BBBZ'));
if (!el) return { error: "no B clip element found" };
const r = el.getBoundingClientRect();
const pxps = r.width / (B.durationMs / 1000); // clip width encodes px-per-second β†’ robust to zoom
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
// move B left by ~950ms so its start (5000) lands ~4050 β†’ inside the magnet of A's end (4000)
const dpx = (950 / 1000) * pxps;
el.dispatchEvent(new PointerEvent('pointerdown', { clientX: cx, clientY: cy, button: 0, bubbles: true, cancelable: true }));
await sleep(20);
window.dispatchEvent(new PointerEvent('pointermove', { clientX: cx - 5, clientY: cy, bubbles: true }));
await sleep(20);
window.dispatchEvent(new PointerEvent('pointermove', { clientX: Math.round(cx - dpx), clientY: cy, bubbles: true }));
await sleep(50);
const duringStart = S.getState().motion.find(m => m.id === 'mB').startMs;
const guidesDuring = document.querySelectorAll('[class*="z-40"]').length;
window.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
await sleep(60);
const finalStart = S.getState().motion.find(m => m.id === 'mB').startMs;
return { pxps: Math.round(pxps), duringStart, finalStart, snappedToEdge: finalStart === A_end, guideAppeared: guidesDuring > before.guides };
})()`;
const run = async () => {
await launchChrome();
const cdp = await openPage("http://localhost:5174");
await Promise.race([cdp.once("Page.loadEventFired"), sleep(8000)]);
await sleep(1500);
await evalJs(cdp, "try{localStorage.setItem('ed.tourDone','1')}catch(e){}; location.reload(); true");
await Promise.race([cdp.once("Page.loadEventFired"), sleep(8000)]);
await sleep(3500);
const r = await evalJs(cdp, script);
console.log("RESULT:", JSON.stringify(r, null, 2));
console.log(r && r.snappedToEdge && r.guideAppeared ? "\nβœ“ PASS β€” dragged clip snapped EXACTLY to the other track's edge, guide line shown" : "\nβœ— CHECK above");
};
run().then(() => shutdown(0)).catch(async (e) => { console.error("error:", e.message); await shutdown(1); });