// Auth-bound accounts, end to end outside the native app: // 1. The X runtime's account reader (src/inject/accountReport.ts) on X-shaped markup: labelled and // icon-only account buttons, the signed-out page, and "report only on change". // 2. The shell (preview harness `/?shell`, needs `vite dev`): a native `profiles-changed` puts the // real login (@handle, picture) in the titlebar switcher, and "Add account" opens X's sign-in in a // fresh account. Not a live X test. // // bun run dev (in another terminal) then node scripts/accounts-check.mjs [baseUrl] import { build } from 'esbuild'; import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; const root = fileURLToPath(new URL('../', import.meta.url)); const base = process.argv[2] ?? 'http://127.0.0.1:1420'; const bundle = (await build({ stdin: { contents: "export { installAccountReport, readAccount } from './src/inject/accountReport.ts';", resolveDir: root, loader: 'ts' }, bundle: true, format: 'iife', globalName: 'BTAccount', write: false, target: 'chrome120', })).outputFiles[0].text; const failures = []; const check = (label, ok, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` (${detail})` : ''}`); if (!ok) failures.push(label); }; const AVATAR = 'https://pbs.twimg.com/profile_images/1826/abc_normal.jpg'; const button = labelled => ``; const browser = await chromium.launch(); const page = await browser.newPage(); await page.setContent(`
${button(true)}
`); await page.addScriptTag({ content: bundle }); check('labelled button: handle, name, sharper picture', JSON.stringify(await page.evaluate(() => BTAccount.readAccount(document))) === JSON.stringify({ handle: 'CRTkafa', name: 'CRT kafa', avatar: 'https://pbs.twimg.com/profile_images/1826/abc_x96.jpg' })); await page.setContent(`
${button(false)}
`); await page.addScriptTag({ content: bundle }); check('icon-only button: no name (native keeps the last one)', (await page.evaluate(() => BTAccount.readAccount(document)))?.name === ''); await page.setContent(`
`); await page.addScriptTag({ content: bundle }); check('narrow layout (background tab, small pane): the drawer avatar button', (await page.evaluate(() => BTAccount.readAccount(document)))?.handle === 'otheraccount'); check('no account button: nothing', await page.evaluate(() => BTAccount.readAccount(new DOMParser().parseFromString('

x

', 'text/html'))) === null); // Reports: once per change, signed-out only on X's own sign-in page. const reports = await page.evaluate(async () => { document.body.innerHTML = ''; const sent = []; BTAccount.installAccountReport(window, (command, args) => { sent.push([command, JSON.stringify(args.report)]); return Promise.resolve(); }); const tick = () => new Promise(r => setTimeout(r, 60)); await tick(); document.body.innerHTML = '
Sign in
'; await tick(); document.body.innerHTML = document.body.innerHTML + ''; await tick(); document.body.innerHTML = ''; await tick(); document.body.appendChild(document.createElement('b')); await tick(); return sent; }); check('nothing reported while the page is still empty', !reports.some(([, r]) => r === '{}')); check('signed-out page reported once', reports.filter(([, r]) => r === '{"signedOut":true}').length === 1, JSON.stringify(reports)); check('then the signed-in account, once', reports.filter(([, r]) => r.includes('"handle":"otheraccount"')).length === 1 && reports.every(([command]) => command === 'tab_report')); // Shell: native tells the switcher who is signed in. const shell = await browser.newPage({ viewport: { width: 1400, height: 800 } }); await shell.goto(`${base}/?shell`); await shell.waitForFunction(() => window.__btEmit && document.querySelector('.bt-profile-switcher')); await shell.evaluate(avatar => window.__btEmit('profiles-changed', { active: 'default', profiles: [{ id: 'default', name: 'CRT kafa', color: '#1d9bf0', account: { handle: 'CRTkafa', name: 'CRT kafa', avatar, signedIn: true } }] }), AVATAR); await shell.waitForTimeout(200); const titlebar = await shell.evaluate(() => ({ text: document.querySelector('.bt-profile-switcher-name')?.textContent, img: document.querySelector('.bt-profile-switcher img')?.getAttribute('src') })); check('titlebar shows the signed-in @handle and picture', titlebar.text === '@CRTkafa' && titlebar.img === AVATAR, JSON.stringify(titlebar)); await shell.click('.bt-profile-switcher'); const row = await shell.evaluate(() => document.querySelector('.bt-profile-row .bt-profile-meta')?.textContent); check('menu row shows the login', row?.startsWith('@CRTkafa'), row); check('no rename field anymore', await shell.locator('.bt-profile-name-input').count() === 0); const before = await shell.evaluate(() => window.__btShellLayout().groups.flatMap(g => g.urls).length); await shell.click('.bt-profile-add'); await shell.waitForTimeout(300); const after = await shell.evaluate(() => ({ urls: window.__btShellLayout().groups.flatMap(g => g.urls), name: document.querySelector('.bt-profile-switcher-name')?.textContent })); check('"Add account" opens X sign-in in a new tab', after.urls.length === before + 1 && after.urls.includes('https://x.com/i/flow/login'), JSON.stringify(after.urls.slice(-2))); check('and makes the new, not yet signed-in account active', after.name === 'New account', after.name); await browser.close(); console.log(failures.length ? `\n${failures.length} failed` : '\nall passed'); process.exit(failures.length ? 1 : 0);