Spaces:
Running
Running
| // 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); }); | |