// Live check of plugin palette commands, workspaces, backup and encrypted folder sync, and the // developer-mode watcher in the running native app (release build started with // WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=). Plain CDP. X pages are only // read (the Focus Mode command hides counts locally); nothing is liked, posted or followed. The // owner's state (plugin on/off, workspaces, sync settings, developer mode) is saved first and // restored at the end; the test plugin and the temporary sync folder are deleted. // // node scripts/platform-live-check.mjs // SKIP=commands,workspaces leaves those parts out (the workspace restore reloads every tab) import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const [port = '9333', out = '../../verification/platform'] = process.argv.slice(2); const skip = new Set((process.env.SKIP ?? '').split(',').filter(Boolean)); mkdirSync(out, { recursive: true }); const appData = join(process.env.APPDATA ?? '', 'app.bettertwttr.desktop'); 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); let x = xs[0]; const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const results = []; const check = (name, ok, detail = '') => { results.push({ name, ok: !!ok }); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`); }; const invoke = (cmd, args = {}) => shell.evaluate(`window.__TAURI_INTERNALS__.invoke(${JSON.stringify(cmd)}, ${JSON.stringify(args)})`); const invokeError = (cmd, args = {}) => shell.evaluate(`window.__TAURI_INTERNALS__.invoke(${JSON.stringify(cmd)}, ${JSON.stringify(args)}).then(() => 'no error', e => String(e))`); const shot = async name => { const result = await shell.send('Page.captureScreenshot', { format: 'png' }); if (result.result?.data) writeFileSync(join(out, `${name}.png`), Buffer.from(result.result.data, 'base64')); }; const xLabel = await x.evaluate(`window.__TAURI_INTERNALS__?.metadata?.currentWebview?.label ?? ''`); const tabId = xLabel.replace(/^tab-/, ''); const FOCUS = 'bt-focus-mode.plugin.js'; const TEST_PLUGIN = 'wb-live-check.plugin.js'; const plugins0 = await invoke('plugins_list'); const focusWasOn = plugins0.find(plugin => plugin.file === FOCUS)?.enabled ?? false; const workspaces0 = await invoke('workspaces_get'); const dev0 = await invoke('dev_get'); const syncFile = join(appData, 'sync.json'); const syncBackup = existsSync(syncFile) ? readFileSync(syncFile) : null; const keyFile = join(appData, 'sync-key.json'); const keyBackup = existsSync(keyFile) ? readFileSync(keyFile) : null; const syncDir = mkdtempSync(join(tmpdir(), 'wb-sync-')); console.log(`X tab ${tabId} (${x.url}); focus mode was ${focusWasOn ? 'on' : 'off'}; ${workspaces0.items.length} workspaces; dev ${dev0.enabled ? 'on' : 'off'}`); try { // 1. Plugin commands: Focus Mode registers two; the tab reports them; the shell runs one there. if (!skip.has('commands')) { if (!focusWasOn) await invoke('plugin_set_enabled', { file: FOCUS, enabled: true }); await wait(1500); const listed = await x.evaluate(`window.wayBetter.api.commands.list().map(c => c.id)`); check('Focus Mode registered its commands in the X tab', listed.includes(`${FOCUS}:toggle`) && listed.includes(`${FOCUS}:settings`), listed.join(', ')); const cssBefore = await x.evaluate(`!!document.querySelector('style[data-better-twttr-plugin="focus-mode"]')`); await invoke('tab_run_command', { id: tabId, command: `${FOCUS}:toggle` }); await wait(700); const cssAfter = await x.evaluate(`!!document.querySelector('style[data-better-twttr-plugin="focus-mode"]')`); check('tab_run_command ran the toggle in that tab', cssBefore !== cssAfter, `focus CSS ${cssBefore} → ${cssAfter}`); await invoke('tab_run_command', { id: tabId, command: `${FOCUS}:toggle` }); await wait(500); check('an unreported command is refused natively', /no such command/.test(await invokeError('tab_run_command', { id: tabId, command: `${FOCUS}:nope` }))); check('a malformed id is refused natively', /no such command|VALIDATION/.test(await invokeError('tab_run_command', { id: tabId, command: '_:a");alert(1);("' }))); // The palette lists them under "Focus Mode" (focus the X tab's panel first). await shell.evaluate(`(() => { const tab = [...document.querySelectorAll('.dv-tab')].find(t => t.querySelector('.bt-tab')); return true; })()`); await shell.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: 'k', code: 'KeyK', modifiers: 2, windowsVirtualKeyCode: 75 }); await shell.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'k', code: 'KeyK', modifiers: 2, windowsVirtualKeyCode: 75 }); await wait(600); await shell.evaluate(`(() => { const input = document.querySelector('.bt-palette-input'); const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; set.call(input, 'focus mode'); input.dispatchEvent(new Event('input', { bubbles: true })); })()`); await wait(500); const groups = await shell.evaluate(`[...document.querySelectorAll('.bt-palette-group')].map(g => g.textContent)`); check('Ctrl+K lists the commands under the plugin name', groups.includes('Focus Mode'), groups.join(' | ')); await shot('app-palette-plugin-commands'); await shell.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }); await wait(300); // Stopping the plugin removes its commands in the page and in the shell's list. await invoke('plugin_set_enabled', { file: FOCUS, enabled: false }); await wait(800); const afterStop = await x.evaluate(`window.wayBetter.api.commands.list().map(c => c.id)`); check('turning the plugin off removes its commands', !afterStop.some(id => id.startsWith(FOCUS)), afterStop.join(', ') || 'none'); } // 2. Workspaces: save the live layout through the titlebar menu, restore it, delete it. if (!skip.has('workspaces')) { await shell.evaluate(`document.querySelector('.bt-ws-button').click()`); await wait(400); await shell.evaluate(`[...document.querySelectorAll('.bt-ws-save')].at(0)?.click()`); await wait(300); await shell.evaluate(`(() => { const input = document.querySelector('.bt-ws-input'); const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; set.call(input, 'Live check'); input.dispatchEvent(new Event('input', { bubbles: true })); })()`); await wait(200); await shot('app-workspace-save'); await shell.evaluate(`document.querySelector('.bt-ws-form').requestSubmit()`); await wait(800); const stored = await invoke('workspaces_get'); const saved = stored.items.find(item => item.name === 'Live check'); const savedUrls = saved ? Object.values(saved.layout.panels).map(panel => panel.params.url).filter(Boolean) : []; check('the layout was saved natively (workspaces.json)', !!saved, `${savedUrls.length} web tabs`); await shell.evaluate(`document.querySelector('.bt-ws-button').click()`); await wait(400); await shot('app-workspace-menu'); await shell.evaluate(`[...document.querySelectorAll('.bt-ws-main')].find(b => b.textContent.startsWith('Live check')).click()`); await wait(3000); const titles = await shell.evaluate(`document.querySelector('.bt-ws-button').textContent`); check('restoring it keeps the titlebar on that workspace', titles.includes('Live check'), titles); const refreshed = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); const xNow = refreshed.filter(t => t.type === 'page' && t.url.startsWith('https://')).map(t => t.url); check('restored tabs load their pages in new webviews', savedUrls.every(url => xNow.some(now => now.split('?')[0] === url.split('?')[0])), `${xNow.length} pages`); await invoke('workspaces_set', { store: workspaces0 }); // The restored tabs are new webviews: talk to the new X tab from here on. await wait(2000); const fresh = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()).find(t => t.type === 'page' && t.url.startsWith('https://x.com/')); if (fresh) { x = connect(fresh); xs.push(x); } await x.evaluate('document.readyState'); } // 3. Backup: every section, no secrets; applying the same alerts is a no-op. const backup = await invoke('backup_collect', { includeDrafts: false }); const text = JSON.stringify(backup); check('backup has every native section', ['themes', 'plugins', 'customCss', 'features', 'settings', 'alerts', 'accounts', 'workspaces'].every(key => key in backup), Object.keys(backup).join(',')); check('backup carries no cookie, token, session or key fields', !/"(cookies?|auth_?token|ct0|csrf|password|session(Id)?|privateKey|signingKey|key)"\s*:/i.test(text) && !text.includes('EBWebView')); check('accounts are names and colors only', backup.accounts.profiles.every(profile => Object.keys(profile).sort().join() === 'color,id,name')); check('drafts only when asked', !('drafts' in backup) && 'drafts' in await invoke('backup_collect', { includeDrafts: true })); const applied = await invoke('backup_apply', { sections: { alerts: backup.alerts } }); check('backup_apply applies a section through its setter', applied.applied.includes('alerts'), JSON.stringify(applied)); // 4. Encrypted folder sync, through a temporary folder (the dialog is replaced by sync.json). const local = JSON.parse(syncBackup?.toString() || '{}'); writeFileSync(syncFile, JSON.stringify({ ...local, folder: syncDir, device: local.device || 'a1b2c3d4e5f60718', deviceName: local.deviceName || 'Live check', autoPush: false, lastPulled: 0, lastPushedHash: '', lastPushedAt: 0 })); const status0 = await invoke('sync_status'); check('a new sync folder is not set up yet', status0.folder === syncDir && !status0.initialized && !status0.unlocked); const started = Date.now(); await invoke('sync_unlock', { passphrase: 'live check passphrase', remember: false }); check('setting up derives the key (Argon2id, 64 MiB)', true, `${Date.now() - started} ms`); const pushed = await invoke('sync_push', { extra: null, force: true }); const snapshots = readdirSync(join(syncDir, 'wayBetter Sync', 'snapshots')); const sealed = readFileSync(join(syncDir, 'wayBetter Sync', 'snapshots', snapshots[0]), 'utf8'); check('a snapshot was written, encrypted', pushed.pushed && snapshots.length === 1 && !sealed.includes('homeUrl') && !sealed.includes('x.com'), snapshots.join(',')); const unchanged = await invoke('sync_push', { extra: null, force: false }).catch(error => ({ error: String(error) })); check('an unchanged push writes nothing (auto push is off here)', !unchanged.pushed); await invoke('sync_lock'); check('the wrong passphrase is refused', /wrong passphrase/.test(await invokeError('sync_unlock', { passphrase: 'not the passphrase', remember: false }))); const reopened = await invoke('sync_unlock', { passphrase: 'live check passphrase', remember: false }); check('the right passphrase unlocks again', reopened.unlocked && reopened.snapshots.length === 1); const opened = await invoke('sync_open', { name: snapshots[0] }); check('the snapshot decrypts to the settings (never drafts)', 'settings' in opened && !('drafts' in opened)); await invoke('sync_lock'); // 5. Developer mode: a test plugin reloads on save; a syntax error is caught with its line. const pluginsDir = dev0.pluginsFolder; const source = version => `/**\n * @name Live check\n * @version ${version}\n * @permissions commands, network:none\n */\nmodule.exports = class {\n start() { window.__wbLive = '${version}'; this.scope = wayBetter.api.scope(); this.scope.commands.register({ id: 'hello', title: 'Live check ${version}', run() {} }); }\n stop() { window.__wbLive = 'stopped ${version}'; this.scope.dispose(); }\n};\n`; writeFileSync(join(pluginsDir, TEST_PLUGIN), source('1')); await wait(400); await invoke('plugin_set_enabled', { file: TEST_PLUGIN, enabled: true }); await invoke('dev_set', { enabled: true }); await wait(1200); check('the test plugin runs', await x.evaluate('window.__wbLive') === '1'); writeFileSync(join(pluginsDir, TEST_PLUGIN), source('2')); await wait(2500); const v2 = await x.evaluate('[window.__wbLive, window.wayBetter.api.commands.list().filter(c => c.id.startsWith("wb-live")).map(c => c.title).join()]'); check('saving reloads it in the X tab (stop, then the new version)', v2[0] === '2' && v2[1] === 'Live check 2', JSON.stringify(v2)); writeFileSync(join(pluginsDir, TEST_PLUGIN), source('3').replace("start() {", "start() { const x = ;")); await wait(2500); const dev = await invoke('dev_get'); const error = dev.errors.find(entry => entry.file === TEST_PLUGIN); check('a syntax error is reported with file, line and column; the old version keeps running', !!error && error.line === 7 && await x.evaluate('window.__wbLive') === '2', error ? `${error.file}:${error.line}:${error.column} ${error.message}` : 'no error'); await shot('app-dev-error-chip'); writeFileSync(join(pluginsDir, TEST_PLUGIN), source('4').replace("start() {", "start() { null.boom;")); await wait(2500); const toast = await x.evaluate(`(() => { const host = document.getElementById('bt-plugin-toast'); return host?.shadowRoot?.textContent ?? ''; })()`); check('a start error shows as a toast in the X tab with file and line', /wb-live-check\.plugin\.js:7/.test(toast), toast.slice(0, 160)); } finally { await invoke('plugin_set_enabled', { file: TEST_PLUGIN, enabled: false }).catch(() => {}); try { rmSync(join(dev0.pluginsFolder, TEST_PLUGIN)); } catch { /* not created */ } await invoke('dev_set', { enabled: dev0.enabled }).catch(() => {}); await invoke('plugin_set_enabled', { file: FOCUS, enabled: focusWasOn }).catch(() => {}); await invoke('workspaces_set', { store: workspaces0 }).catch(() => {}); await invoke('sync_lock').catch(() => {}); if (syncBackup) writeFileSync(syncFile, syncBackup); else rmSync(syncFile, { force: true }); if (keyBackup) writeFileSync(keyFile, keyBackup); else rmSync(keyFile, { force: true }); rmSync(syncDir, { recursive: true, force: true }); const failed = results.filter(result => !result.ok).length; console.log(`\n${results.length - failed}/${results.length} live checks passed; owner state restored`); shell.close(); xs.forEach(tab => tab.close()); }