waybetter-for-x/apps/desktop/scripts/accounts-check.mjs
2026-10-01 06:01:13 +07:00

77 lines
6 KiB
JavaScript

// 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 => `<button data-testid="SideNav_AccountSwitcher_Button"><div data-testid="UserAvatar-Container-CRTkafa"><img src="${AVATAR}"></div>${labelled ? '<div><span><span>CRT kafa</span></span><span>@CRTkafa</span></div>' : ''}</button>`;
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(`<body><header>${button(true)}</header></body>`);
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(`<body><header>${button(false)}</header></body>`);
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(`<body><div><a data-testid="DashButton_ProfileIcon_Link" href="#"><div data-testid="UserAvatar-Container-playfishies"><img src="${AVATAR}"></div></a></div></body>`);
await page.addScriptTag({ content: bundle });
check('narrow layout (background tab, small pane): the drawer avatar button', (await page.evaluate(() => BTAccount.readAccount(document)))?.handle === 'playfishies');
check('no account button: nothing', await page.evaluate(() => BTAccount.readAccount(new DOMParser().parseFromString('<p>x</p>', '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 = '<div data-testid="loginButton">Sign in</div>'; await tick();
document.body.innerHTML = document.body.innerHTML + '<i></i>'; await tick();
document.body.innerHTML = '<button data-testid="SideNav_AccountSwitcher_Button"><div data-testid="UserAvatar-Container-playfishies"></div><span>Fishies</span></button>'; 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":"playfishies"')).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);