98 lines
6.2 KiB
JavaScript
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);
|