#!/usr/bin/env node // E2E UI check with headless Chromium over the DevTools protocol (no playwright dep). // node deploy/e2e-ui.mjs [port] // Loads the local DSH (cookie minted from the browser-session secret), opens the Devices // panel, screenshots it, then opens /r// and screenshots that. import { spawn } from 'node:child_process' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { cookieName, mintCookie } from '../lib/auth-cookie.mjs' const [chrome, outDir, port = '3081'] = process.argv.slice(2) const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh') const secret = fs.readFileSync(path.join(home, '.credentials.yaml'), 'utf8').match(/browser-session:[\s\S]*?secret:\s*([A-Za-z0-9_-]{43})/)[1] const authority = `127.0.0.1:${port}` const cookieVal = mintCookie(authority, secret, 1).split('=').slice(1).join('=') fs.mkdirSync(outDir, { recursive: true }) const prof = fs.mkdtempSync(path.join(os.tmpdir(), 'dshr-chrome-')) const proc = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', `--user-data-dir=${prof}`, '--remote-debugging-port=0', '--window-size=1500,950', 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] }) const wsUrl = await new Promise((resolve, reject) => { let buf = '' proc.stderr.on('data', (d) => { buf += d; const m = buf.match(/DevTools listening on (ws:\/\/\S+)/); if (m) resolve(m[1]) }) setTimeout(() => reject(new Error('chrome did not start')), 15000) }) const list = await (await fetch(wsUrl.replace(/^ws/, 'http').replace(/\/devtools\/browser\/.*/, '/json/list'))).json() const page = list.find((t) => t.type === 'page') const ws = new WebSocket(page.webSocketDebuggerUrl) await new Promise((r) => ws.addEventListener('open', r)) let id = 0 const pending = new Map() const logs = [] ws.addEventListener('message', (ev) => { const m = JSON.parse(ev.data) if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id) } if (m.method === 'Runtime.consoleAPICalled') logs.push(`${m.params.type}: ${m.params.args.map((a) => a.value ?? a.description ?? '').join(' ')}`.slice(0, 300)) if (m.method === 'Runtime.exceptionThrown') logs.push(`EXC: ${m.params.exceptionDetails?.exception?.description || m.params.exceptionDetails?.text}`.slice(0, Number(process.env.EXC_LEN || 400))) }) const send = (method, params = {}) => new Promise((resolve) => { const i = ++id; pending.set(i, resolve); ws.send(JSON.stringify({ id: i, method, params })) }) const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) const evalJs = async (expr) => (await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })).result?.result?.value const shot = async (name) => { const r = await send('Page.captureScreenshot', { format: 'png' }) fs.writeFileSync(path.join(outDir, name), Buffer.from(r.result.data, 'base64')) console.log('screenshot', path.join(outDir, name)) } await send('Page.enable'); await send('Runtime.enable') await send('Network.enable') await send('Network.setCookie', { name: cookieName(authority), value: cookieVal, domain: '127.0.0.1', path: '/', httpOnly: true }) await send('Page.navigate', { url: `http://${authority}/` }) await sleep(9000) // Click the Devices panel entry (its icon carries data-dsh-panel-entry). const clicked = await evalJs(`(() => { const svg = document.querySelector('[data-dsh-panel-entry="dsh-remote-devices"]'); if (!svg) return 'no-entry'; (svg.closest('button,a,[role=button]') || svg).click(); return 'clicked' })()`) console.log('devices entry:', clicked) await sleep(3500) console.log('panel text:', String(await evalJs(`(document.querySelector('[data-dsh-plugin="dsh-remote"]')?.innerText || 'NO PANEL').slice(0, 600)`)).replace(/\n+/g, ' | ')) await shot('devices-panel.png') const localErrors = logs.filter((l) => /EXC/.test(l)).length console.log('local page exceptions:', localErrors, logs.filter((l) => /EXC/.test(l)).map((l) => l.slice(0, 90)).join(' || ')) logs.length = 0 const remote = process.env.REMOTE_DEVICE || 'ai-pc' const remoteSession = process.env.REMOTE_SESSION || '' await send('Page.navigate', { url: `http://${authority}/r/${remote}/${remoteSession ? `#/session/${remoteSession}` : ''}` }) await sleep(remoteSession ? 14000 : 10000) console.log('remote title/banner:', await evalJs(`document.title + ' :: ' + (document.querySelector('a[title*="another device"]')?.innerText || 'no banner') + ' :: hash=' + location.hash`)) if (remoteSession) { console.log('remote session view:', String(await evalJs(`document.body.innerText.slice(0, 500)`)).replace(/\n+/g, ' | ')) } await shot('remote-ui.png') console.log('console:', logs.filter((l) => /error|EXC|warn/i.test(l)).slice(0, 12).join('\n ')) ws.close(); proc.kill('SIGKILL'); fs.rmSync(prof, { recursive: true, force: true }) process.exit(0)