149 lines
8.4 KiB
JavaScript
149 lines
8.4 KiB
JavaScript
// Account switcher screenshots and flow checks on the shell preview harness (`/?shell`, needs `vite dev`).
|
|
// Usage: node scripts/accounts-ux-shots.mjs [baseUrl] [outDir] [prefix]
|
|
// prefix "before" only takes screenshots; any other prefix also checks the click flow.
|
|
import { chromium } from 'playwright';
|
|
import { mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const base = process.argv[2] ?? 'http://127.0.0.1:1420';
|
|
const out = process.argv[3] ?? join(import.meta.dirname, '..', '..', '..', 'verification', 'accounts-ux');
|
|
const prefix = process.argv[4] ?? 'after';
|
|
mkdirSync(out, { recursive: true });
|
|
|
|
const WIDE = { width: 1440, height: 860 };
|
|
const failures = [];
|
|
const check = (label, ok, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` (${detail})` : ''}`); if (!ok) failures.push(label); };
|
|
|
|
const browser = await chromium.launch();
|
|
async function open(query, viewport = WIDE) {
|
|
const page = await browser.newPage({ viewport });
|
|
page.on('pageerror', error => failures.push(`page error: ${error.message}`));
|
|
await page.goto(`${base}/?shell&${query}`);
|
|
await page.waitForSelector('.bt-profile-switcher');
|
|
await page.waitForTimeout(400);
|
|
return page;
|
|
}
|
|
const layout = page => page.evaluate(() => window.__btShellLayout());
|
|
const shot = (page, name) => page.screenshot({ path: join(out, `${prefix}-${name}.png`) });
|
|
|
|
// Menu open, 4 accounts (two signed in, one signed out, one never signed in), split view.
|
|
let page = await open('tabs=5&profiles=4&split&open=accounts');
|
|
await page.waitForTimeout(300);
|
|
await shot(page, 'menu');
|
|
await page.close();
|
|
|
|
page = await open('tabs=5&profiles=4&split');
|
|
await shot(page, 'panes');
|
|
|
|
if (prefix !== 'before') {
|
|
// The focused pane's account is the one the titlebar shows and highlights.
|
|
const focusedTab = await page.evaluate(() => window.__btShellLayout().active);
|
|
const title = await page.getAttribute('.bt-profile-switcher', 'aria-label');
|
|
check('titlebar names the focused pane’s account', /CRT kafa|Fishies|Shitposts|New account/.test(title ?? ''), `${focusedTab}: ${title}`);
|
|
|
|
// Click an account that has no visible pane → a new pane on the right with its home feed.
|
|
await page.click('.bt-profile-switcher');
|
|
await page.waitForSelector('.bt-acct-menu');
|
|
const before = await layout(page);
|
|
await page.click('.bt-acct-row[data-profile="pnews01"] .bt-acct-main');
|
|
await page.waitForTimeout(300);
|
|
let after = await layout(page);
|
|
check('never-signed-in account opens X sign-in in a new pane', after.groups.length === before.groups.length + 1
|
|
&& after.groups.some(group => group.urls.includes('https://x.com/i/flow/login')), JSON.stringify(after.groups.map(g => g.urls)));
|
|
check('menu closes after a pick', !(await page.$('.bt-acct-menu')));
|
|
|
|
// Click an account whose pane is visible → that pane is focused, nothing new opens.
|
|
const visibleDefault = await page.evaluate(() => {
|
|
const api = window.__btShellLayout();
|
|
return api.groups.map(group => group.active);
|
|
});
|
|
await page.click('.bt-profile-switcher');
|
|
await page.click('.bt-acct-row[data-profile="default"] .bt-acct-main');
|
|
await page.waitForTimeout(250);
|
|
const refocus = await layout(page);
|
|
check('visible account pane is focused instead of duplicated', refocus.groups.length === after.groups.length
|
|
&& refocus.groups.flatMap(g => g.tabs).length === after.groups.flatMap(g => g.tabs).length, `active=${refocus.active} visible=${visibleDefault}`);
|
|
|
|
// The menu starts on the focused pane's account, not simply the first row.
|
|
await page.click('.bt-profile-switcher');
|
|
await page.click('.bt-acct-row[data-profile="pwork1"] .bt-acct-main', { modifiers: ['Shift'] });
|
|
await page.waitForTimeout(250);
|
|
await page.click('.bt-profile-switcher');
|
|
await page.waitForTimeout(150);
|
|
const startRow = await page.evaluate(() => document.activeElement?.closest('.bt-acct-row')?.getAttribute('data-profile'));
|
|
const focusedRow = await page.evaluate(() => document.querySelector('.bt-acct-row.is-focused')?.getAttribute('data-profile'));
|
|
check('menu opens on the focused pane’s account (not the first row)', startRow === 'pwork1' && focusedRow === 'pwork1', `${startRow} / ${focusedRow}`);
|
|
await page.keyboard.press('Escape');
|
|
|
|
// Keyboard: open with Enter, arrows move, Enter picks, Escape closes.
|
|
await page.focus('.bt-profile-switcher');
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.bt-acct-menu');
|
|
const focused0 = await page.evaluate(() => document.activeElement?.closest('.bt-acct-row')?.getAttribute('data-profile'));
|
|
await page.keyboard.press('ArrowDown');
|
|
const focused1 = await page.evaluate(() => document.activeElement?.closest('.bt-acct-row')?.getAttribute('data-profile'));
|
|
check('menu opens with focus on an account and arrows move it', !!focused0 && !!focused1 && focused0 !== focused1, `${focused0} → ${focused1}`);
|
|
await page.keyboard.press('ArrowRight');
|
|
await page.waitForTimeout(100);
|
|
check('ArrowRight opens the row’s actions', !!(await page.$('.bt-acct-sub')));
|
|
await shot(page, 'row-actions');
|
|
await page.keyboard.press('Escape');
|
|
check('Escape closes the row’s actions first', !(await page.$('.bt-acct-sub')) && !!(await page.$('.bt-acct-menu')));
|
|
await page.keyboard.press('Escape');
|
|
check('Escape closes the menu', !(await page.$('.bt-acct-menu')));
|
|
|
|
// Shift+click forces a new tab (in the focused group) even when a pane is visible.
|
|
const beforeShift = await layout(page);
|
|
await page.click('.bt-profile-switcher');
|
|
await page.click('.bt-acct-row[data-profile="default"] .bt-acct-main', { modifiers: ['Shift'] });
|
|
await page.waitForTimeout(250);
|
|
const afterShift = await layout(page);
|
|
check('Shift+click opens a new tab', afterShift.groups.flatMap(g => g.tabs).length === beforeShift.groups.flatMap(g => g.tabs).length + 1
|
|
&& afterShift.groups.length === beforeShift.groups.length);
|
|
|
|
// Use for new tabs from the row menu.
|
|
await page.click('.bt-profile-switcher');
|
|
await page.click('.bt-acct-row[data-profile="pwork1"] .bt-acct-more');
|
|
await page.click('.bt-acct-sub [data-action="default"]');
|
|
await page.waitForTimeout(150);
|
|
const active = await page.evaluate(async () => (await window.__TAURI_INTERNALS__.invoke('profiles_get')).active);
|
|
check('"Use for new tabs" sets the default account', active === 'pwork1', active);
|
|
check('the menu stays open to show the new default', !!(await page.$('.bt-acct-row[data-profile="pwork1"] .bt-acct-badge')));
|
|
await page.waitForTimeout(250);
|
|
await shot(page, 'menu-after-flow');
|
|
// The harness refuses like native code while a profile still has a synced tab.
|
|
const refused = await page.evaluate(async () => {
|
|
const current = await window.__TAURI_INTERNALS__.invoke('profiles_get');
|
|
try { await window.__TAURI_INTERNALS__.invoke('profiles_set', { settings: { ...current, profiles: current.profiles.filter(p => p.id !== 'pmemes1') } }); return false; }
|
|
catch { return true; }
|
|
});
|
|
check('removing an account with open tabs directly is refused (harness mirrors native)', refused);
|
|
// Remove asks first.
|
|
await page.click('.bt-acct-row[data-profile="pmemes1"] .bt-acct-more');
|
|
await page.click('.bt-acct-sub [data-action="remove"]');
|
|
check('Remove asks for confirmation', !!(await page.$('.bt-acct-confirm')));
|
|
await shot(page, 'remove-confirm');
|
|
await page.keyboard.press('Escape');
|
|
check('Cancel keeps the account', !(await page.$('.bt-acct-confirm')) && !!(await page.$('.bt-acct-row[data-profile="pmemes1"]')));
|
|
await page.click('.bt-acct-row[data-profile="pmemes1"] .bt-acct-more');
|
|
await page.click('.bt-acct-sub [data-action="remove"]');
|
|
await page.click('.bt-acct-confirm .bt-acct-remove');
|
|
await page.waitForTimeout(250);
|
|
const left = await page.evaluate(async () => (await window.__TAURI_INTERNALS__.invoke('profiles_get')).profiles.map(p => p.id));
|
|
const openProfiles = await page.evaluate(() => window.__btShellLayout().groups.flatMap(g => g.tabs));
|
|
check('Remove closes the account’s tabs, then removes it', !left.includes('pmemes1') && !openProfiles.includes('tab2'), `${left} / ${openProfiles}`);
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(150);
|
|
await shot(page, 'panes-after-flow');
|
|
}
|
|
await page.close();
|
|
|
|
// Single-account titlebar.
|
|
page = await open('tabs=4&profiles=1&open=accounts');
|
|
await page.waitForTimeout(300);
|
|
await shot(page, 'single-account');
|
|
await page.close();
|
|
|
|
await browser.close();
|
|
if (failures.length) { console.error(`\n${failures.length} failure(s):\n${failures.join('\n')}`); process.exit(1); }
|
|
console.log(`\nScreenshots in ${out}`);
|