// Drive headless Edge over CDP: log in to the LOCAL test server, open a view, screenshot it. import fs from 'node:fs'; import crypto from 'node:crypto'; const [, , outPrefix, ...steps] = process.argv; // steps: JS snippets to run before each shot, separated by || const secret = fs.readFileSync('creds.txt', 'utf8').split('\n')[1].trim(); function totp(secret) { const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = ''; for (const c of secret) bits += alpha.indexOf(c).toString(2).padStart(5, '0'); const key = Buffer.from(bits.match(/.{8}/g).map(b => parseInt(b, 2))); const ctr = Buffer.alloc(8); ctr.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000))); const h = crypto.createHmac('sha1', key).update(ctr).digest(); const o = h[19] & 15; return String((h.readUInt32BE(o) & 0x7fffffff) % 1000000).padStart(6, '0'); } const list = await (await fetch('http://127.0.0.1:9333/json')).json(); const page = list.find(t => t.type === 'page'); const ws = new WebSocket(page.webSocketDebuggerUrl); await new Promise(r => ws.onopen = r); let id = 0; const pend = {}; const logs = []; ws.onmessage = ev => { const m = JSON.parse(ev.data); if (m.id && pend[m.id]) { pend[m.id](m.result || m.error); delete pend[m.id]; } if (m.method === 'Runtime.exceptionThrown') logs.push('EXC: ' + JSON.stringify(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text)); if (m.method === 'Runtime.consoleAPICalled' && ['error', 'warning'].includes(m.params.type)) logs.push(m.params.type + ': ' + m.params.args.map(a => a.value ?? a.description).join(' ')); }; const send = (method, params = {}) => new Promise(r => { const i = ++id; pend[i] = r; ws.send(JSON.stringify({ id: i, method, params })); }); const sleep = ms => new Promise(r => setTimeout(r, ms)); const ev = async expr => (await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })).result?.value; await send('Runtime.enable'); await send('Page.enable'); await send('Emulation.setDeviceMetricsOverride', { width: 1100, height: 900, deviceScaleFactor: 1, mobile: false }); await send('Page.navigate', { url: 'http://127.0.0.1:8080/' }); await sleep(1500); console.log('login:', await ev(`fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:'testpw123',code:'${totp(secret)}'})}).then(r=>r.status)`)); await send('Page.navigate', { url: 'http://127.0.0.1:8080/' }); await sleep(2500); let n = 0; for (const step of steps.join(' ').split('||')) { const s = step.trim(); if (!s) continue; if (s.startsWith('wait:')) { await sleep(Number(s.slice(5))); continue; } if (s.startsWith('shot:')) { const img = await send('Page.captureScreenshot', { format: 'png' }); const f = `${outPrefix}-${s.slice(5)}.png`; fs.writeFileSync(f, Buffer.from(img.data, 'base64')); console.log('saved', f); continue; } console.log('eval:', JSON.stringify(await ev(s))); } console.log('console problems:', logs.length ? logs : 'none'); ws.close();