// Live check of the Theme Builder, Google Fonts and pane themes in the running native app (release // build started with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=). Plain // CDP (Playwright's connectOverCDP trips over X's shared workers). X pages are only read, never // clicked. The owner's theme state is saved first and restored at the end. // // node scripts/theme-live-check.mjs [capture.ps1] import { execFileSync } from 'node:child_process'; const [port = '9333', out = '../../verification/theme-builder', capture] = process.argv.slice(2); 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 xTargets = targets.filter(t => t.type === 'page' && t.url.startsWith('https://x.com/')); if (!shellTarget || !xTargets.length) throw new Error('shell or X tab not found'); 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); 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 => { const n = ++id; pending.set(n, resolve); ws.send(JSON.stringify({ id: n, method, params })); }); }; const evaluate = async (expression) => { const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (result.result?.exceptionDetails) throw new Error(`${target.url}: ${result.result.exceptionDetails.exception?.description ?? result.result.exceptionDetails.text}`); return result.result?.result?.value; }; return { send, evaluate, close: () => ws.close(), url: target.url }; } const shell = connect(shellTarget); const xs = xTargets.map(connect); const log = []; const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const snap = name => { if (capture) log.push(execFileSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', capture, '-Out', `${out}/${name}.png`]).toString().trim()); }; const invoke = (cmd, args = {}) => shell.evaluate(`window.__TAURI_INTERNALS__.invoke(${JSON.stringify(cmd)}, ${JSON.stringify(args)})`); const key = async (keyName, code, modifiers) => { for (const type of ['rawKeyDown', 'keyUp']) await shell.send('Input.dispatchKeyEvent', { type, key: keyName, code, modifiers, windowsVirtualKeyCode: keyName.toUpperCase().charCodeAt(0) }); }; /** Runs DOM steps in the shell: `$(selector)`, `click(el)`, `type(el, text)` (React-compatible). */ const dom = body => shell.evaluate(`(async () => { const $ = (s, root = document) => root.querySelector(s); const $$ = (s, root = document) => [...root.querySelectorAll(s)]; const wait = ms => new Promise(r => setTimeout(r, ms)); const click = el => { if (!el) throw new Error('missing element'); el.click(); }; const type = (el, text) => { const set = Object.getOwnPropertyDescriptor(el.constructor.prototype, 'value').set; set.call(el, text); el.dispatchEvent(new Event('input', { bubbles: true })); }; const byText = (s, text) => $$(s).find(el => el.textContent.trim().startsWith(text)); ${body} })()`); const xState = x => x.evaluate(`(() => ({ url: location.pathname, body: getComputedStyle(document.body).backgroundColor, theme: window.wayBetter?.theme.current().preset, heading: window.wayBetter?.api.theme.current().fonts?.heading.stack.slice(0, 36), googleFaces: [...document.fonts].filter(f => f.wbGoogle).map(f => f.family + ':' + f.status).slice(0, 4), nameFont: getComputedStyle(document.querySelector('[data-testid="User-Name"] > div:first-child span') ?? document.body).fontFamily.slice(0, 36), }))()`); const original = { theme: await invoke('theme_get'), scopes: await invoke('theme_scopes_get'), fonts: await invoke('gfonts_installed') }; log.push(`original: preset ${original.theme.preset}, scopes ${JSON.stringify(original.scopes)}, fonts ${original.fonts.map(f => f.family).join(',') || 'none'}`); try { // 1. A real native Google Fonts download into app data. const font = await invoke('gfonts_install', { family: 'Space Grotesk', weights: { kind: 'range', min: 300, max: 700 } }); log.push(`installed ${font.family}: ${font.faces} faces, ${font.bytes} bytes`); const faces = await invoke('gfonts_faces', { family: 'Space Grotesk' }); log.push(`faces: ${faces.faces.map(face => `${face.weight.join('-')}:${face.data.length}`).join(' ')}`); const bad = await invoke('gfonts_install', { family: '../evil', weights: { kind: 'range', min: 100, max: 900 } }).catch(error => String(error)); log.push(`invalid family: ${bad}`); // 2. The Theme Builder in the panel; a Nord remix with a pink accent and Space Grotesk names, live on X. await key('W', 'KeyW', 2 | 8); await wait(1500); await shell.evaluate(`window.dispatchEvent(new CustomEvent('bt-panel-section', { detail: 'builder' }))`); await wait(1500); await dom(` click($('button[aria-label^="Start from"]')); await wait(200); click(byText('.bt-select-list li', 'Nord')); await wait(300); const accent = $('input[aria-label="Accent color hex"]'); type(accent, '#f472b6'); accent.dispatchEvent(new FocusEvent('blur')); accent.blur(); await wait(300); click($('button[aria-label^="Names and headings font"]')); await wait(400); type($('input[aria-label="Search Google Fonts"]'), 'Space Grotesk'); await wait(500); click($$('.wb-fb-list li[role=option]').find(li => li.textContent.includes('Space Grotesk'))); await wait(1500); `); await wait(1500); snap('app-01-font-browser'); await dom(`click($$('.wb-fb-actions button').find(b => /names and headings/i.test(b.textContent))); await wait(3000);`); await wait(2500); for (const x of xs) log.push(`builder live ${JSON.stringify(await xState(x))}`); snap('app-02-builder-live'); // 3. Leaving the builder ends the preview: every tab gets its own theme back. await shell.evaluate(`window.dispatchEvent(new CustomEvent('bt-panel-section', { detail: 'themes' }))`); await wait(2500); for (const x of xs) log.push(`builder closed ${JSON.stringify(await xState(x))}`); // 4. Pane themes: the first X tab gets Paper, the other tabs keep the app theme. const label = await xs[0].evaluate(`window.__TAURI_INTERNALS__?.metadata?.currentWebview?.label ?? ''`); const tabId = label.replace(/^tab-/, ''); await invoke('theme_scopes_set', { scopes: { ...original.scopes, panes: { ...original.scopes.panes, [tabId]: 'paper' } } }); await wait(2500); for (const x of xs) log.push(`pane ${tabId}=paper ${JSON.stringify(await xState(x))}`); snap('app-03-pane-theme'); } finally { await invoke('theme_preview', { theme: null }).catch(() => {}); await invoke('theme_scopes_set', { scopes: original.scopes }).catch(error => log.push(`restore scopes failed ${error}`)); await invoke('theme_set', { theme: original.theme }).catch(error => log.push(`restore theme failed ${error}`)); if (!original.fonts.some(font => font.family === 'Space Grotesk')) await invoke('gfonts_remove', { family: 'Space Grotesk' }).catch(() => {}); await wait(2000); for (const x of xs) log.push(`restored ${JSON.stringify(await xState(x))}`); console.log(log.join('\n')); shell.close(); xs.forEach(x => x.close()); }