// See README.md in this directory. Needs a running server, passed as argv[2].
// Drive the real page with real key events over the DevTools protocol.
// Asserting that a handler LOOKS right is not evidence that a key works.
import { spawn } from 'child_process'
const URL_ = process.argv[2]
const PORT = 9333
const dir = '/tmp/claude-1000/cdp-profile-' + PORT
const chrome = spawn('chromium', [
'--headless=new', '--no-sandbox', '--disable-gpu', '--window-size=1600,1000',
'--remote-debugging-port=' + PORT, '--user-data-dir=' + dir, URL_
], { stdio: 'ignore' })
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function target () {
for (let i = 0; i < 60; i++) {
try {
const res = await fetch('http://127.0.0.1:' + PORT + '/json/list')
const list = await res.json()
const page = list.find(t => t.type === 'page' && t.webSocketDebuggerUrl)
if (page) return page.webSocketDebuggerUrl
} catch {}
await new Promise(r => setTimeout(r, 250))
}
throw new Error('no devtools target')
}
const ws = new WebSocket(await target())
await new Promise(r => { ws.onopen = r })
let id = 0
const pending = new Map()
ws.onmessage = (m) => {
const msg = JSON.parse(m.data)
if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id) }
}
function send (method, params = {}) {
const myId = ++id
ws.send(JSON.stringify({ id: myId, method, params }))
return new Promise(r => pending.set(myId, r))
}
async function evaluate (expr) {
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
if (r.result && r.result.exceptionDetails) return { error: r.result.exceptionDetails.text }
return r.result && r.result.result ? r.result.result.value : undefined
}
const VK = { g: 71, f: 70, r: 82, o: 79, '[': 219, ']': 221 }
async function key (k) {
const code = k === '[' ? 'BracketLeft' : k === ']' ? 'BracketRight' : 'Key' + k.toUpperCase()
const base = { key: k, code, windowsVirtualKeyCode: VK[k], nativeVirtualKeyCode: VK[k] }
await send('Input.dispatchKeyEvent', { type: 'keyDown', text: k, ...base })
await send('Input.dispatchKeyEvent', { type: 'keyUp', ...base })
await new Promise(r => setTimeout(r, 450))
}
async function moveTo (x, y) {
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, buttons: 0 })
await new Promise(r => setTimeout(r, 200))
}
await send('Runtime.enable')
await new Promise(r => setTimeout(r, 5000))
const out = []
const say = (label, value) => { out.push(label + ': ' + JSON.stringify(value)); console.log(label + ': ' + JSON.stringify(value)) }
say('tab', await evaluate('typeof currentTab !== "undefined" ? currentTab : "n/a"'))
say('nodes drawn', await evaluate('document.querySelectorAll(".nv-node").length'))
// hover the first node's BODY (not its head, which opens it)
// The first node whose CENTRE actually hits its own body: most of the board is
// scrolled off-screen, and pointing at empty space proves nothing.
const rect = await evaluate(`(() => {
for (const n of document.querySelectorAll('.nv-node')) {
const r = n.getBoundingClientRect();
const x = Math.round(r.x + r.width / 2), y = Math.round(r.y + r.height / 2);
const el = document.elementFromPoint(x, y);
if (el && el.getAttribute('class') === 'nv-node-body') return { x, y };
}
return null;
})()`)
say('first node body point', rect)
if (rect) await moveTo(rect.x, rect.y)
say('NV.hover after move', await evaluate('NV.hover'))
await key('g')
say('after G -> relations depth', await evaluate('NV.relations && NV.relations.depth'))
say('after G -> ring boxes', await evaluate('document.querySelectorAll(".nv-frame-ring").length'))
say('after G -> nodes drawn now', await evaluate('NV.nodes.length'))
say('ring labels', await evaluate('[...document.querySelectorAll(".nv-frame-label")].map(t => t.textContent)'))
await key(']')
say('after ] -> depth', await evaluate('NV.relations && NV.relations.depth'))
say('after ] -> nodes drawn now', await evaluate('NV.nodes.length'))
await key('[')
say('after [ -> depth', await evaluate('NV.relations && NV.relations.depth'))
await key('[')
say('after [[ -> relations left', await evaluate('NV.relations === null'))
await key('f')
say('after F -> showFrames', await evaluate('NV.showFrames'))
await key('f')
await key('r')
say('after R -> circuit', await evaluate('NV.circuit'))
await key('r')
// the typing guard
await evaluate(`(() => { const i = document.querySelector('input[type=text], #search, input'); if (i) { i.focus(); return i.id || i.type } return 'none' })()`)
say('focused input', await evaluate('document.activeElement.tagName'))
await key('g')
say('G while typing -> relations stays null', await evaluate('NV.relations === null'))
console.log('---')
ws.close()
chrome.kill('SIGKILL')
process.exit(0)