waybetter-for-x/apps/desktop/scripts/native-signedout-theme-check.mjs
2026-10-02 02:54:04 +07:00

79 lines
5.4 KiB
JavaScript

// 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 <CDP port> <outDir>
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));