// Power User Mode, page side: the built X runtime (src-tauri/x-runtime.js) and tab bridge
// (src-tauri/tab-bridge.js) on an X-shaped fixture served at https://x.com/home (Playwright route;
// no live X), at deck column widths 320/360/400. With a fake Tauri IPC that records calls, it checks:
// X's chrome is hidden and the content fills the column, filters (keyword, /regex/, replies,
// reposts, media only), unread counting (baseline, new posts above, seen on scroll, Alt+R), the
// unread number reaching `tab_report`, deck keys forwarded only outside text fields, and that the
// column module makes no request of its own. Screenshots: verification/deck/fixture/.
//
// node scripts/build-inject.mjs && node scripts/column-e2e.mjs
import { chromium } from 'playwright';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
const app = fileURLToPath(new URL('..', import.meta.url));
const out = join(app, '../../verification/deck/fixture');
await mkdir(out, { recursive: true });
const runtime = await readFile(join(app, 'src-tauri/x-runtime.js'), 'utf8');
const bridge = await readFile(join(app, '../../waybetter/crates/waybetter-host/tab-bridge.js'), 'utf8');
const failures = [];
const check = (label, ok, detail = '') => {
console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` (${detail})` : ''}`);
if (!ok) failures.push(label);
};
let nextId = 9000;
const post = ({ id = String(nextId++), name = 'Lena Ortiz', handle = 'lena', text = 'A post', reply = false, repost = false, media = false } = {}) => `
${repost ? `
Maya reposted
` : ''}
${reply ? `
` : ''}
${text}
${media ? `
` : ''}
123
`;
const FIXTURE = `Home / X
${[
post({ id: '101', text: 'Good morning timeline' }),
post({ id: '102', text: 'Huge GIVEAWAY, retweet to win', name: 'Spam Bot', handle: 'spam' }),
post({ id: '103', text: 'gm frens' }),
post({ id: '104', text: 'A reply in the timeline', reply: true }),
post({ id: '105', text: 'Reposted thing', repost: true }),
post({ id: '106', text: 'Photo post', media: true }),
...Array.from({ length: 10 }, (_, i) => post({ id: String(110 + i), text: `Older post ${i}` })),
].join('')}
Trends
`;
const browser = await chromium.launch();
const errors = [];
async function openColumn(width, config) {
const context = await browser.newContext({ viewport: { width, height: 800 }, deviceScaleFactor: 1 });
const page = await context.newPage();
page.on('pageerror', error => errors.push(`${width}: ${error.message}`));
const requests = [];
page.on('request', request => requests.push(request.url()));
await page.route('https://x.com/**', route => route.request().resourceType() === 'document'
? route.fulfill({ status: 200, contentType: 'text/html; charset=utf-8', body: FIXTURE })
: route.fulfill({ status: 204, body: '' }));
// A fake Tauri IPC that records what the page asks native code for.
await page.addInitScript(() => {
window.__calls = [];
window.__TAURI_INTERNALS__ = { invoke: (cmd, args) => { window.__calls.push({ cmd, args }); return Promise.resolve(null); } };
});
await page.addInitScript({ content: bridge });
if (config) await page.addInitScript(value => { window.__btColumn = value; }, config);
await page.addInitScript({ content: runtime });
await page.goto('https://x.com/home');
await page.waitForFunction(() => !!window.BetterTwttr?.column);
await page.waitForTimeout(400);
return { page, context, requests };
}
const CONFIG = {
url: 'https://x.com/home', kind: 'home', density: 'compact',
filters: { terms: ['giveaway', '/^gm\\b/'], hideReplies: false, hideReposts: false, mediaOnly: false },
flags: { disableVideoAutoplay: true }, unread: true, readSeq: 0, jumpSeq: 0,
};
const shown = (page, selector) => page.$eval(selector, el => { const box = el.getBoundingClientRect(); return getComputedStyle(el).display !== 'none' && box.width > 0 && box.height > 0; }).catch(() => false);
const visibleIds = page => page.$$eval('article[data-testid="tweet"]', els => els.filter(el => el.getBoundingClientRect().height > 0).map(el => el.querySelector('a[href*="/status/"]').getAttribute('href').split('/').pop()));
for (const width of [320, 360, 400]) {
const { page, context, requests } = await openColumn(width, CONFIG);
check(`${width}: column mode on`, await page.evaluate(() => document.documentElement.hasAttribute('data-wb-column')));
check(`${width}: X's rail, side column and inline composer hidden`,
!(await shown(page, 'header[role="banner"]')) && !(await shown(page, '[data-testid="sidebarColumn"]')) && !(await shown(page, '[data-testid="tweetTextarea_0_label"]')));
const primary = await page.$eval('[data-testid="primaryColumn"]', el => { const b = el.getBoundingClientRect(); return [Math.round(b.left), Math.round(b.width)]; });
const inner = await page.evaluate(() => document.documentElement.clientWidth);
check(`${width}: content fills the column`, primary[0] === 0 && Math.abs(primary[1] - inner) <= 1, `${primary} of ${inner}`);
check(`${width}: the page title row is hidden on the column's own page, its tabs stay`,
await page.$eval('[data-testid="primaryColumn"] [role="tablist"]', el => el.getBoundingClientRect().height > 0)
&& await page.$eval('[data-testid="primaryColumn"] > div > div:first-child > div:first-child > div:first-child', el => getComputedStyle(el).display === 'none'));
const ids = await visibleIds(page);
check(`${width}: keyword and /regex/ filters hide posts`, !ids.includes('102') && !ids.includes('103') && ids.includes('101'), ids.slice(0, 6).join(','));
await page.screenshot({ path: join(out, `column-${width}.png`) });
if (width === 360) {
// Replies, reposts, media only.
await page.evaluate(config => window.BetterTwttr.column.set(config), { ...CONFIG, filters: { terms: [], hideReplies: true, hideReposts: true, mediaOnly: false } });
await page.waitForTimeout(200);
let now = await visibleIds(page);
check('hide replies and reposts', !now.includes('104') && !now.includes('105') && now.includes('102'), now.slice(0, 6).join(','));
await page.evaluate(config => window.BetterTwttr.column.set(config), { ...CONFIG, filters: { terms: [], hideReplies: false, hideReposts: false, mediaOnly: true } });
await page.waitForTimeout(200);
now = await visibleIds(page);
check('media only', now.length === 1 && now[0] === '106', now.join(','));
await page.evaluate(config => window.BetterTwttr.column.set(config), CONFIG);
await page.waitForTimeout(200);
// Unread: scrolled down, two new posts arrive on top.
await page.evaluate(() => window.scrollTo(0, 900));
await page.waitForTimeout(200);
await page.evaluate(() => {
const timeline = document.getElementById('timeline');
timeline.insertAdjacentHTML('afterbegin', window.__post({ id: '201', text: 'Brand new post' }) + window.__post({ id: '202', text: 'Another new one' }));
window.scrollBy(0, timeline.firstElementChild.offsetHeight * 2);
});
await page.waitForTimeout(700);
const unread = await page.evaluate(() => window.BetterTwttr.column.unread());
check('two new posts above count as unread', unread === 2, `${unread}`);
const reports = await page.evaluate(() => window.__calls.filter(c => c.cmd === 'tab_report' && c.args?.report && 'unread' in c.args.report).map(c => c.args.report.unread));
check('the unread number reaches tab_report (a number only)', reports.at(-1) === 2, reports.join(','));
check('unread posts are marked', await page.$$eval('[data-wb-unread]', els => els.length) === 2);
await page.screenshot({ path: join(out, 'column-360-unread.png') });
await page.keyboard.press('Alt+U');
await page.waitForTimeout(300);
const top = await page.$eval('article a[href$="/status/202"]', el => Math.round(el.closest('[data-testid="cellInnerDiv"]').getBoundingClientRect().top));
check('Alt+U jumps to the next unread post', top >= -2 && top < 60, `${top}`);
await page.waitForTimeout(700);
const afterJump = await page.evaluate(() => window.BetterTwttr.column.unread());
check('posts scrolled into view are read', afterJump < 2, `${afterJump}`);
await page.evaluate(() => window.scrollTo(0, 900));
await page.evaluate(() => document.getElementById('timeline').insertAdjacentHTML('afterbegin', window.__post({ id: '203', text: 'Third' })));
await page.waitForTimeout(500);
await page.keyboard.press('Alt+R');
await page.waitForTimeout(500);
check('Alt+R marks the column read', await page.evaluate(() => window.BetterTwttr.column.unread()) === 0);
check('readSeq from the shell marks read too', await page.evaluate(config => {
document.getElementById('timeline').insertAdjacentHTML('afterbegin', window.__post({ id: '204', text: 'Fourth' }));
return new Promise(resolve => setTimeout(() => { window.BetterTwttr.column.set({ ...config, readSeq: 1 }); resolve(window.BetterTwttr.column.unread() === 0); }, 400));
}, CONFIG));
// Deck keys: forwarded by the tab bridge outside text fields only.
await page.evaluate(() => { window.__calls.length = 0; });
await page.keyboard.press(']');
await page.keyboard.press('Alt+2');
await page.keyboard.press('Alt+PageDown');
let keys = await page.evaluate(() => window.__calls.filter(c => c.cmd === 'tab_shortcut').map(c => c.args.shortcut));
check('deck keys are forwarded from a column', keys.join() === 'column-next,column-2,column-page-next', keys.join());
await page.evaluate(() => {
const input = document.createElement('div');
input.contentEditable = 'true';
input.id = 'fake-composer';
document.body.prepend(input);
input.focus();
window.__calls.length = 0;
});
await page.keyboard.press(']');
await page.keyboard.press('[');
keys = await page.evaluate(() => window.__calls.filter(c => c.cmd === 'tab_shortcut').map(c => c.args.shortcut));
const typed = await page.$eval('#fake-composer', el => el.textContent);
check('bare deck keys are ignored while typing', keys.length === 0 && typed === '][', `${keys.join()} typed "${typed}"`);
// Back to an ordinary tab: everything is undone.
await page.evaluate(() => window.BetterTwttr.column.set(null));
await page.waitForTimeout(200);
check('column off restores X', !(await page.evaluate(() => document.documentElement.hasAttribute('data-wb-column'))) && await shown(page, 'header[role="banner"]') && (await visibleIds(page)).includes('102'));
}
const own = requests.filter(url => !url.startsWith('https://x.com/home'));
check(`${width}: the column module made no request`, own.length === 0, own.slice(0, 3).join(' '));
await context.close();
}
// Keys in an ordinary (non-column) X tab: only Ctrl+Shift+D is a deck key there.
{
const { page, context } = await openColumn(900, null);
await page.evaluate(() => { window.__calls.length = 0; });
await page.keyboard.press(']');
await page.keyboard.press('Alt+2');
await page.keyboard.press('Control+Shift+D');
const keys = await page.evaluate(() => window.__calls.filter(c => c.cmd === 'tab_shortcut').map(c => c.args.shortcut));
check('ordinary tabs forward only the deck toggle', keys.join() === 'deck-toggle', keys.join());
await context.close();
}
await browser.close();
await writeFile(join(out, 'column-e2e.json'), JSON.stringify({ failures, errors }, null, 1));
console.log(errors.length ? `page errors:\n${errors.join('\n')}` : 'no page errors');
console.log(failures.length ? `${failures.length} FAILED` : 'all column checks passed');
process.exit(failures.length ? 1 : 0);