// Read-only DOM/visual check of isolated signed-out X WebView; themes change only in this // disposable profile and the original theme is restored in finally. // node scripts/native-signedout-theme-check.mjs import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; const [port = '9345', out = '../../verification/theme-matrix/native-signedout'] = process.argv.slice(2); await mkdir(out, { recursive: true }); const targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); const shellTarget = targets.find(t => t.type === 'page' && t.url.startsWith('http://tauri.localhost')); const xTarget = targets.find(t => t.type === 'page' && t.url.startsWith('https://x.com/i/jf/onboarding')); const googleTarget = targets.find(t => t.type === 'iframe' && t.url.startsWith('https://accounts.google.com/gsi/button')); if (!shellTarget || !xTarget) throw new Error('isolated shell or signed-out X page missing'); function connect(target) { const ws = new WebSocket(target.webSocketDebuggerUrl); let id = 0; const pending = new Map(); ws.onmessage = event => { const msg = JSON.parse(event.data); if (pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); } }; const ready = new Promise(resolve => { ws.onopen = resolve; }); const send = async (method, params = {}) => { await ready; return new Promise((resolve, reject) => { const n = ++id; const timeout = setTimeout(() => { pending.delete(n); reject(new Error(`${method} timed out`)); }, 15000); pending.set(n, msg => { clearTimeout(timeout); msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result); }); ws.send(JSON.stringify({ id: n, method, params })); }); }; const evalJs = async expression => { const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text); return result.result.value; }; return { send, evalJs, close: () => ws.close() }; } const shell = connect(shellTarget), x = connect(xTarget), google = googleTarget ? connect(googleTarget) : null; const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const invoke = (command, args = {}) => shell.evalJs(`window.__TAURI_INTERNALS__.invoke(${JSON.stringify(command)},${JSON.stringify(args)})`); const original = await invoke('theme_get'); const rows = []; try { for (const preset of ['original', 'x', 'classic', 'solarized-light', 'night-ink', 'terminal', 'paper', 'high-contrast']) { if (preset !== 'original') { await invoke('theme_set', { theme: { ...original, preset, accent: null } }); await wait(1800); } const dom = await x.evalJs(`(() => { const view = e => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < innerHeight; }; const style = e => { const s = getComputedStyle(e); return { color:s.color, bg:s.backgroundColor, opacity:s.opacity, visibility:s.visibility, display:s.display, font:s.fontSize }; }; return { url:location.href, theme:window.wayBetter?.theme.current().preset, body:style(document.body), buttons:[...document.querySelectorAll('button,[role="button"]')].filter(view).slice(0,20).map(e => ({ text:e.textContent?.trim().slice(0,100), aria:e.getAttribute('aria-label'), html:e.outerHTML.slice(0,350), ...style(e), descendants:[...e.querySelectorAll('span,p,div')].filter(view).slice(0,8).map(c => ({text:c.textContent?.trim().slice(0,100),...style(c)})) })), iframe:[...document.querySelectorAll('iframe')].filter(view).map(e => ({title:e.title,src:e.src.slice(0,100),box:(()=>{const r=e.getBoundingClientRect();return [r.x,r.y,r.width,r.height]})()})), headings:[...document.querySelectorAll('h1,h2')].filter(view).map(e=>e.textContent?.trim()) }; })()`); const shot = await x.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false }); await writeFile(join(out, `${preset}.png`), Buffer.from(shot.data, 'base64')); const googleDom = google ? await google.evalJs(`(() => ({ text: document.body?.textContent?.trim().slice(0,150), buttons: [...document.querySelectorAll('button,[role="button"]')].map(e => ({ text: e.textContent?.trim().slice(0,80), bg:getComputedStyle(e).backgroundColor, color:getComputedStyle(e).color, children:[...e.querySelectorAll('span')].map(c=>({text:c.textContent?.trim().slice(0,50), color:getComputedStyle(c).color})) })) }))()`) : null; if (google) { const iframeShot = await google.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false }).catch(() => null); if (iframeShot) await writeFile(join(out, `${preset}-google.png`), Buffer.from(iframeShot.data, 'base64')); } rows.push({ preset, ...dom, googleDom }); } } finally { await invoke('theme_set', { theme: original }).catch(() => {}); x.close(); shell.close(); google?.close(); } await writeFile(join(out, 'results.json'), JSON.stringify({ original, rows }, null, 2)); console.log(JSON.stringify({ original, rows: rows.map(r => ({ preset:r.preset, theme:r.theme, url:r.url, buttons:r.buttons.filter(b=>b.text).map(b=>({text:b.text.slice(0,30),bg:b.bg,color:b.color,innerBg:b.descendants[0]?.bg,innerColor:b.descendants[0]?.color})),googleDom:r.googleDom })) }, null, 2));