111 lines
7.3 KiB
JavaScript
111 lines
7.3 KiB
JavaScript
// 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=<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 <port> <outDir> [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());
|
|
}
|