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

98 lines
6.2 KiB
JavaScript

// "Links to X pages" check: the real click router (src/inject/linkRouting.ts) in Chromium over a
// hand-built page shaped like X's markup (timeline cells, posts with a permalink, a quote, media,
// action buttons, a hover card, the nav rail). Per click it asserts whether X's own navigation was
// diverted to a new-window request (window.open, stubbed) and with which URL. Not a live X test.
//
// node scripts/link-routing-check.mjs
import { build } from 'esbuild';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
const root = fileURLToPath(new URL('../', import.meta.url));
const bundle = (await build({
stdin: { contents: "export { installLinkRouting } from './src/inject/linkRouting.ts';", resolveDir: root, loader: 'ts' },
bundle: true, format: 'iife', globalName: 'BTLinks', write: false, target: 'chrome120',
})).outputFiles[0].text;
const PAGE = `<!doctype html><html><body>
<header role="banner"><nav role="navigation"><a id="nav-home" href="/home" data-testid="AppTabBar_Home_Link" role="link">Home</a><a id="nav-profile" href="/CRTkafa" role="link">Profile</a></nav></header>
<main role="main"><div data-testid="primaryColumn">
<div role="tablist"><a id="tab" role="tab" href="/home">Following</a></div>
<div data-testid="cellInnerDiv">
<article id="post" data-testid="tweet" tabindex="0" role="article">
<a id="author" href="/playfishies" role="link">Fishies</a>
<a id="permalink" href="/playfishies/status/111" role="link"><time>2h</time></a>
<div id="text" data-testid="tweetText">hello <a id="mention" href="/CRTkafa" role="link">@CRTkafa</a> <a id="hashtag" href="/hashtag/tauri" role="link">#tauri</a> <a id="external" href="https://t.co/abc" target="_blank" rel="noopener">t.co/abc</a></div>
<div id="quote" role="link" tabindex="0"><span id="quote-text">quoted</span><a id="quote-permalink" href="/CRTkafa/status/222" role="link"><time>1d</time></a></div>
<div data-testid="tweetPhoto"><a id="photo" href="/playfishies/status/111/photo/1" role="link"><img alt="" width="10" height="10"></a></div>
<div role="group"><button id="like" data-testid="like">Like</button><div id="reply" role="button" data-testid="reply">Reply</div></div>
</article>
</div>
<div data-testid="cellInnerDiv"><article id="focal" data-testid="tweet" tabindex="-1" role="article"><span id="focal-text">the open post</span><a href="/CRTkafa/status/333" role="link"><time>now</time></a></article></div>
<div data-testid="cellInnerDiv"><div id="usercell" data-testid="UserCell" role="button"><a id="usercell-link" href="/playfishies" role="link">Fishies</a></div></div>
</div></main>
<div id="layers"><div data-testid="HoverCard"><a id="hover-name" href="/playfishies" role="link">Fishies</a><a id="hover-following" href="/playfishies/following" role="link">30 Following</a></div></div>
<script>
window.opened = [];
window.open = (url, target, features) => { window.opened.push({ url, target, features }); return null; };
window.inPlace = [];
// Stands in for X's React root: whatever reaches it would navigate in place.
document.addEventListener('click', event => { if (!event.defaultPrevented) window.inPlace.push(event.target.id || event.target.closest('[id]')?.id); event.preventDefault(); });
</script>
</body></html>`;
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();
const page = await browser.newPage();
await page.route('https://x.com/**', route => route.fulfill({ contentType: 'text/html', body: PAGE }));
await page.goto('https://x.com/home');
await page.addScriptTag({ content: bundle });
await page.evaluate(() => { window.routing = BTLinks.installLinkRouting(window); });
const click = async (selector, mode, options = {}) => {
await page.evaluate(mode => { window.opened = []; window.inPlace = []; window.routing.configure({ x: mode }); }, mode);
await page.click(selector, { force: true, ...options });
return page.evaluate(() => ({ opened: window.opened.map(entry => entry.url), inPlace: window.inPlace }));
};
const DIVERTED = [
['#author', 'https://x.com/playfishies'],
['#permalink', 'https://x.com/playfishies/status/111'],
['#text', 'https://x.com/playfishies/status/111'],
['#mention', 'https://x.com/CRTkafa'],
['#hashtag', 'https://x.com/hashtag/tauri'],
['#quote-text', 'https://x.com/CRTkafa/status/222'],
['#hover-name', 'https://x.com/playfishies'],
['#hover-following', 'https://x.com/playfishies/following'],
];
const IN_PLACE = ['#nav-home', '#nav-profile', '#tab', '#photo', '#like', '#reply', '#focal-text', '#usercell-link', '#external'];
for (const mode of ['pane', 'tab', 'browser']) {
for (const [selector, url] of DIVERTED) {
const result = await click(selector, mode);
check(`${mode}: ${selector} opens ${url}`, result.opened.length === 1 && result.opened[0] === url && result.inPlace.length === 0, JSON.stringify(result));
}
for (const selector of IN_PLACE) {
const result = await click(selector, mode);
check(`${mode}: ${selector} left to X`, result.opened.length === 0, JSON.stringify(result));
}
}
for (const [selector] of DIVERTED) {
const result = await click(selector, 'same');
check(`same: ${selector} left to X`, result.opened.length === 0 && result.inPlace.length === 1, JSON.stringify(result));
}
const modified = await click('#permalink', 'pane', { modifiers: ['Control'] });
check('Ctrl+click keeps X\'s own new-tab behaviour', modified.opened.length === 0, JSON.stringify(modified));
await page.evaluate(() => { const range = document.createRange(); range.selectNodeContents(document.getElementById('text')); getSelection().removeAllRanges(); getSelection().addRange(range); });
const selecting = await page.evaluate(() => { window.opened = []; window.routing.configure({ x: 'pane' }); document.getElementById('text').dispatchEvent(new MouseEvent('click', { bubbles: true, button: 0 })); return window.opened.length; });
check('a click that ends a text selection is not a navigation', selecting === 0);
await browser.close();
console.log(failures.length ? `\n${failures.length} failed` : '\nall passed');
process.exit(failures.length ? 1 : 0);