Neutral sample account names in tests, check scripts and the site's demo UI

The framework moves to 0e344ee.
This commit is contained in:
CRTkafa 2026-10-19 01:02:34 +07:00
parent 83f8c363b4
commit 4c6817ecb5
42 changed files with 136 additions and 136 deletions

View file

@ -31,9 +31,9 @@ check('labelled button: handle, name, sharper picture', JSON.stringify(await pag
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.setContent(`<body><div><a data-testid="DashButton_ProfileIcon_Link" href="#"><div data-testid="UserAvatar-Container-otheraccount"><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('narrow layout (background tab, small pane): the drawer avatar button', (await page.evaluate(() => BTAccount.readAccount(document)))?.handle === 'otheraccount');
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.
@ -45,13 +45,13 @@ const reports = await page.evaluate(async () => {
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.innerHTML = '<button data-testid="SideNav_AccountSwitcher_Button"><div data-testid="UserAvatar-Container-otheraccount"></div><span>Sample</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'));
check('then the signed-in account, once', reports.filter(([, r]) => r.includes('"handle":"otheraccount"')).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 } });

View file

@ -39,7 +39,7 @@ 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}`);
check('titlebar names the focused pane’s account', /CRT kafa|Sample|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');

View file

@ -52,8 +52,8 @@ check('four visible, one parked off-page', boxes.filter(b => b.visible).length =
check('columns are never nav-redundant', boxes.every(b => b.navRedundant === false));
const urls = Object.fromEntries(Object.entries(state.columns).map(([id, config]) => [id, config.url]));
check('every column got its config with the right x.com page', urls.k0 === 'https://x.com/home' && urls.k1 === 'https://x.com/notifications'
&& urls.k2 === 'https://x.com/notifications/mentions' && urls.k3 === 'https://x.com/search?q=from%3Aplayfishies%20filter%3Amedia&f=live'
&& urls.k4 === 'https://x.com/playfishies', JSON.stringify(urls));
&& urls.k2 === 'https://x.com/notifications/mentions' && urls.k3 === 'https://x.com/search?q=from%3Aotheraccount%20filter%3Amedia&f=live'
&& urls.k4 === 'https://x.com/otheraccount', JSON.stringify(urls));
check('default column flags: autoplay off, promoted hidden', state.columns.k0?.flags?.disableVideoAutoplay === true && state.columns.k0?.flags?.hidePromotedPosts === true);
check('filters travel with the config', JSON.stringify(state.columns.k1?.filters?.terms) === '["giveaway"]');
const visible = boxes.filter(b => b.visible).sort((a, b) => a.x - b.x);
@ -70,7 +70,7 @@ await page.waitForTimeout(250);
titles = await columnTitles(page);
state = await harness(page);
boxes = state.sync.filter(item => item.role === 'column');
check('Alt+PageDown shows the last page, filled from the end', titles.length === 4 && titles[3] === '@playfishies' && titles[0] === 'Notifications', titles.join(', '));
check('Alt+PageDown shows the last page, filled from the end', titles.length === 4 && titles[3] === '@otheraccount' && titles[0] === 'Notifications', titles.join(', '));
check('the column that scrolled off is parked', boxes.filter(b => b.parked).map(b => b.id).join() === 'k0');
await shot(page, 'deck-page-2');
await page.keyboard.press('Alt+PageUp');
@ -126,14 +126,14 @@ check('Remove column drops it and its config', !titles.includes('Home') && !('k0
await page.click('.bt-deck-action:has-text("Column")');
await page.waitForSelector('.bt-addcol');
await page.click('.bt-addcol-type[data-kind="search"]');
await page.fill('#bt-addcol-field', 'wayBetter from:@playfishies');
await page.fill('#bt-addcol-field', 'wayBetter from:@otheraccount');
await page.click('.bt-addcol-account:nth-child(2)');
await shot(page, 'deck-add-column');
await page.click('.bt-addcol-foot .bt-deck-primary');
await page.waitForTimeout(400);
state = await harness(page);
const added = Object.entries(state.columns).find(([, config]) => config.kind === 'search' && config.url.includes('wayBetter'));
check('Add column sends a search config (operators normalized)', !!added && added[1].url === 'https://x.com/search?q=wayBetter%20from%3Aplayfishies&f=live', added?.[1].url);
check('Add column sends a search config (operators normalized)', !!added && added[1].url === 'https://x.com/search?q=wayBetter%20from%3Aotheraccount&f=live', added?.[1].url);
const addedBox = added && state.sync.find(b => b.id === added[0]);
check('the new column runs in the chosen account', addedBox?.profile === 'pwork1', addedBox?.profile);
await page.close();

View file

@ -21,18 +21,18 @@ const PAGE = `<!doctype html><html><body>
<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>
<a id="author" href="/otheraccount" role="link">Sample</a>
<a id="permalink" href="/otheraccount/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 data-testid="tweetPhoto"><a id="photo" href="/otheraccount/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 data-testid="cellInnerDiv"><div id="usercell" data-testid="UserCell" role="button"><a id="usercell-link" href="/otheraccount" role="link">Sample</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>
<div id="layers"><div data-testid="HoverCard"><a id="hover-name" href="/otheraccount" role="link">Sample</a><a id="hover-following" href="/otheraccount/following" role="link">30 Following</a></div></div>
<script>
window.opened = [];
window.open = (url, target, features) => { window.opened.push({ url, target, features }); return null; };
@ -62,14 +62,14 @@ const click = async (selector, mode, options = {}) => {
};
const DIVERTED = [
['#author', 'https://x.com/playfishies'],
['#permalink', 'https://x.com/playfishies/status/111'],
['#text', 'https://x.com/playfishies/status/111'],
['#author', 'https://x.com/otheraccount'],
['#permalink', 'https://x.com/otheraccount/status/111'],
['#text', 'https://x.com/otheraccount/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'],
['#hover-name', 'https://x.com/otheraccount'],
['#hover-following', 'https://x.com/otheraccount/following'],
];
const IN_PLACE = ['#nav-home', '#nav-profile', '#tab', '#photo', '#like', '#reply', '#focal-text', '#usercell-link', '#external'];

View file

@ -31,7 +31,7 @@ const { problems, checked } = await runtimeStartCheck({
featureDefaults: FEATURE_DEFAULTS,
pages: ['home', 'chat', 'profile', 'notifications'].map(cell => ({
name: cell,
path: `/${cell === 'chat' ? 'i/chat' : cell === 'profile' ? 'playfishies' : cell}`,
path: `/${cell === 'chat' ? 'i/chat' : cell === 'profile' ? 'otheraccount' : cell}`,
html: fixtureHtml(cell, 'dark'),
})),
verify: {

View file

@ -275,7 +275,7 @@ const ROUTES = {
// `#layers`: portaled surfaces. Real X gives them the layer color (rgb(20,20,20) in Lights out).
const LAYERS = {
'more-menu': m => `<div class="css-175oi2r" style="position: fixed; inset: 0; z-index: 10;"><div role="menu" class="css-175oi2r r-cl2sl0 r-1jyoszn r-card16" style="position: fixed; left: 24px; top: 150px; width: 318px; padding: 8px 0;">${['Lists', 'Communities', 'Creator Studio', 'Business', 'Ads', 'Create your Space', 'Settings and privacy'].map(t => `<a role="menuitem" href="#" class="css-175oi2r r-18u37iz" style="gap: 14px; padding: 12px 16px; align-items: center; text-decoration: none;"><div class="fx-icon" style="color: ${m.text};"></div>${T(m.text, t, ' r-b88u0q')}</a>`).join('')}</div></div>`,
'hover-card': m => `<div data-testid="hoverCardParent" class="css-175oi2r" style="position: fixed; left: 360px; top: 190px; z-index: 10;"><div data-testid="HoverCard" class="css-175oi2r r-cl2sl0 r-1jyoszn r-card16" style="width: 300px; padding: 16px; gap: 8px;"><div class="css-175oi2r r-18u37iz" style="justify-content: space-between; align-items: flex-start;"><div class="fx-avatar" style="width: 64px; height: 64px; flex-basis: 64px;"></div><button data-testid="1-follow" class="css-175oi2r r-sdzlij" style="background-color: ${m.fill}; padding: 8px 16px; border: 0;"><span class="css-1jxf684 r-chirp r-b88u0q" style="color: ${m.onFill};">Follow</span></button></div>${T(m.text, 'Fishies', ' r-b88u0q')}${T(m.muted, '@playfishies')}${T(m.text, 'The social aquarium that lives while you’re away.')}<div class="css-175oi2r r-18u37iz" style="gap: 12px;">${T(m.text, '<b>30</b>')}${T(m.muted, 'Following')}${T(m.text, '<b>21</b>')}${T(m.muted, 'Followers')}</div></div></div>`,
'hover-card': m => `<div data-testid="hoverCardParent" class="css-175oi2r" style="position: fixed; left: 360px; top: 190px; z-index: 10;"><div data-testid="HoverCard" class="css-175oi2r r-cl2sl0 r-1jyoszn r-card16" style="width: 300px; padding: 16px; gap: 8px;"><div class="css-175oi2r r-18u37iz" style="justify-content: space-between; align-items: flex-start;"><div class="fx-avatar" style="width: 64px; height: 64px; flex-basis: 64px;"></div><button data-testid="1-follow" class="css-175oi2r r-sdzlij" style="background-color: ${m.fill}; padding: 8px 16px; border: 0;"><span class="css-1jxf684 r-chirp r-b88u0q" style="color: ${m.onFill};">Follow</span></button></div>${T(m.text, 'Sample', ' r-b88u0q')}${T(m.muted, '@otheraccount')}${T(m.text, 'The social aquarium that lives while you’re away.')}<div class="css-175oi2r r-18u37iz" style="gap: 12px;">${T(m.text, '<b>30</b>')}${T(m.muted, 'Following')}${T(m.text, '<b>21</b>')}${T(m.muted, 'Followers')}</div></div></div>`,
'edit-profile': m => modal(m, 'Edit profile', `<div class="fx-media" style="margin: 0; border-radius: 0; height: 150px;"></div><div class="css-175oi2r" style="padding: 16px; gap: 16px;">${['Name', 'Bio', 'Location', 'Website'].map(label => `<label class="css-175oi2r r-1kqtdi0 r-card16" style="border-width: 1px; border-style: solid; padding: 8px 12px;">${T(m.muted, label)}<input class="r-chirp r-a023e6" style="border: 0; outline: 0; background: transparent; color: ${m.text}; padding: 4px 0;" value="${label === 'Name' ? 'Placeholder User' : ''}"></label>`).join('')}</div>`, 'Save'),
compose: m => modal(m, '', composer(m, { inline: false })),
// X's tooltip under a post's caret ("More"): X's grey tooltip fill with the label in X's text colour.

View file

@ -57,14 +57,14 @@ function fixture(mode) {
<main role="main" data-testid="primaryColumn">
<header>Home</header>
<button class="trigger" id="trigger">Open (focus return target)</button>
${tweet('playfishies', 'playfishies', 'A fresh post from today. Replies go straight through.', 2 * 3600e3, '2103816196386852995')}
${tweet('otheraccount', 'otheraccount', 'A fresh post from today. Replies go straight through.', 2 * 3600e3, '2103816196386852995')}
${tweet('jack', 'jack', 'just setting up my twttr', 3 * 365 * day, '20')}
${tweet('someone', 'Someone', 'A third post so the timeline has some depth to it.', 5 * 3600e3, '2103816196386852999')}
</main>
<div data-testid="sidebarColumn">
<section><h2 style="margin:0 0 8px;font-size:20px">What's happening</h2>
<div><div data-testid="trend">#wayBetter</div><div data-testid="trend">Tauri 3</div></div></section>
<aside><h2 style="margin:0 0 8px;font-size:20px">Who to follow</h2><div data-testid="UserCell">@playfishies</div></aside>
<aside><h2 style="margin:0 0 8px;font-size:20px">Who to follow</h2><div data-testid="UserCell">@otheraccount</div></aside>
</div>
</div>
<script>window.__replies = []; window.__xKeys = []; document.addEventListener('keydown', e => window.__xKeys.push(e.key));</script>

View file

@ -14,11 +14,11 @@ const root = fileURLToPath(new URL('../', import.meta.url));
const api = (await build({ stdin: { contents: "import { createPluginApi } from './src/inject/pluginApi.ts'; window.BetterTwttr = { api: createPluginApi(window) };", resolveDir: root, loader: 'ts' }, bundle: true, format: 'iife', write: false, target: 'chrome120' })).outputFiles[0].text;
const plugin = await readFile(join(root, 'src-tauri/bundled-plugins/user-notes.plugin.js'), 'utf8');
const PAGE = `<!doctype html><html><body><main role="main"><div data-testid="primaryColumn">
<div><div data-testid="UserName"><span>Fishies</span><span>@playfishies</span></div><div data-testid="UserDescription">bio</div></div>
<div><div data-testid="UserName"><span>Sample</span><span>@otheraccount</span></div><div data-testid="UserDescription">bio</div></div>
<button data-testid="userActions" id="more">···</button></div></main><div id="layers"></div>
<script>
document.getElementById('more').addEventListener('click', () => {
document.getElementById('layers').innerHTML = '<div data-testid="mask" id="mask" style="position:fixed;inset:0"></div><div role="menu" style="position:fixed;top:40px;left:40px;z-index:2;background:#fff"><div role="menuitem" data-testid="block"><svg><path d="M0 0"/></svg><span>Block @playfishies</span></div></div>';
document.getElementById('layers').innerHTML = '<div data-testid="mask" id="mask" style="position:fixed;inset:0"></div><div role="menu" style="position:fixed;top:40px;left:40px;z-index:2;background:#fff"><div role="menuitem" data-testid="block"><svg><path d="M0 0"/></svg><span>Block @otheraccount</span></div></div>';
document.getElementById('mask').addEventListener('click', () => { document.getElementById('layers').innerHTML = ''; });
});
</script></body></html>`;
@ -29,7 +29,7 @@ const browser = await chromium.launch();
const page = await browser.newPage();
await page.route('https://x.com/**', route => route.fulfill({ contentType: 'text/html', body: PAGE }));
const start = async () => {
await page.goto('https://x.com/playfishies');
await page.goto('https://x.com/otheraccount');
await page.addScriptTag({ content: api });
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; ${plugin}\n; window.notes = new module.exports(); window.notes.start(); })();` });
await page.waitForTimeout(200);

View file

@ -139,7 +139,7 @@ mod tests {
write(FEATURES_FILE, r#"{"hideViewCounts":true}"#);
write(SETTINGS_FILE, r#"{"homeUrl":"https://x.com/home","externalLinks":"pane","xLinks":"same","restoreTabs":true}"#);
write(ALERTS_FILE, r#"{"enabled":true,"keywords":["rust"]}"#);
write(PROFILES_FILE, r##"{"profiles":[{"id":"default","name":"Main","color":"#1d9bf0","account":{"handle":"playfishies","name":"P","avatar":null}},{"id":"work","name":"Work","color":"#f91880","authToken":"Bearer AAAA-TOKEN"}],"active":"work"}"##);
write(PROFILES_FILE, r##"{"profiles":[{"id":"default","name":"Main","color":"#1d9bf0","account":{"handle":"otheraccount","name":"P","avatar":null}},{"id":"work","name":"Work","color":"#f91880","authToken":"Bearer AAAA-TOKEN"}],"active":"work"}"##);
write(crate::workspaces::FILE, r#"{"version":1,"default":null,"items":[]}"#);
write("plugins/enabled.json", r#"["mine.plugin.js","bt-focus-mode.plugin.js"]"#);
write("plugins/mine.plugin.js", "/** @name Mine */ module.exports = class { start(){} };");

View file

@ -621,18 +621,18 @@ mod tests {
#[test]
fn reads_a_page_head() {
let base = Url::parse("https://fishies.lol/play/").unwrap();
let base = Url::parse("https://example.org/play/").unwrap();
let html = r#"<!doctype html><html><head><title>Fallback</title>
<meta property="og:title" content="Fishies &amp; friends">
<meta property="og:title" content="Sample &amp; friends">
<meta name=description content='The social aquarium'>
<meta property="og:site_name" content="Fishies">
<meta property="og:site_name" content="Sample">
<meta property="og:image" content="/img/card.png">
</head><body><meta property="og:title" content="not this"></body></html>"#;
let meta = parse_head(html, &base);
assert_eq!(meta.title, "Fishies & friends");
assert_eq!(meta.title, "Sample & friends");
assert_eq!(meta.description, "The social aquarium");
assert_eq!(meta.site, "Fishies");
assert_eq!(meta.image.unwrap().as_str(), "https://fishies.lol/img/card.png");
assert_eq!(meta.site, "Sample");
assert_eq!(meta.image.unwrap().as_str(), "https://example.org/img/card.png");
let bare = parse_head("<head><title>\n Only a title \u{202e}</title><meta property='og:image' content='http://insecure.example/a.png'></head>", &base);
assert_eq!(bare.title, "Only a title");
assert_eq!(bare.image, None, "an image off the link rules is dropped");

View file

@ -17,7 +17,7 @@ mod tests {
use crate::site::SITE;
fn account(handle: &str, avatar: &str) -> XAccount {
XAccount { handle: handle.into(), name: "Fishies".into(), avatar: Some(avatar.into()), signed_in: false }
XAccount { handle: handle.into(), name: "Sample".into(), avatar: Some(avatar.into()), signed_in: false }
}
#[test]
@ -26,10 +26,10 @@ mod tests {
for bad in ["", "has space", "waytoolonghandle_", "a/b", "a.b"] {
assert!(!settings.observe("default", AccountSeen::SignedIn(account(bad, "https://pbs.twimg.com/a.jpg")), &SITE.accounts), "{bad:?}");
}
assert!(settings.observe("default", AccountSeen::SignedIn(account("@play_fishies", "https://evil.test/x.png")), &SITE.accounts));
assert!(settings.observe("default", AccountSeen::SignedIn(account("@other_account", "https://evil.test/x.png")), &SITE.accounts));
let seen = settings.profiles[0].account.clone().unwrap();
assert_eq!((seen.handle.as_str(), seen.avatar, seen.signed_in), ("play_fishies", None, true));
assert!(settings.observe("default", AccountSeen::SignedIn(account("play_fishies", "https://pbs.twimg.com/profile_images/1/a_x96.jpg")), &SITE.accounts));
assert_eq!((seen.handle.as_str(), seen.avatar, seen.signed_in), ("other_account", None, true));
assert!(settings.observe("default", AccountSeen::SignedIn(account("other_account", "https://pbs.twimg.com/profile_images/1/a_x96.jpg")), &SITE.accounts));
assert!(settings.profiles[0].account.as_ref().unwrap().avatar.is_some());
}

View file

@ -39,7 +39,7 @@ const NEWS_ICON = svg('<path d="M9 10h14M9 15h14M9 20h9" stroke="#fff" stroke-wi
// Accounts as native code reports them: two signed in, one signed out on X, one never signed in.
const PROFILES = [
{ id: 'default', name: 'Main', color: '#1d9bf0', account: { handle: 'CRTkafa', name: 'CRT kafa', signedIn: true } },
{ id: 'pwork1', name: 'wayBetter for X', color: '#7856ff', account: { handle: 'playfishies', name: 'Fishies', signedIn: true } },
{ id: 'pwork1', name: 'wayBetter for X', color: '#7856ff', account: { handle: 'otheraccount', name: 'Sample', signedIn: true } },
{ id: 'pmemes1', name: 'Shitposts', color: '#f91880', account: { handle: 'oldshitposts', name: 'Shitposts', signedIn: false } },
{ id: 'pnews01', name: 'New account', color: '#00ba7c' },
].slice(0, Math.min(4, Math.max(1, Number(params.get('profiles') ?? 3))));
@ -185,8 +185,8 @@ const TABS: Array<{ url: string; title: string; favicon?: string }> = [
];
const DECK_SOURCES = [
{ kind: 'home' }, { kind: 'notifications' }, { kind: 'mentions' }, { kind: 'search', value: 'from:playfishies filter:media' },
{ kind: 'user', value: 'playfishies' }, { kind: 'list', value: '1234567890' }, { kind: 'bookmarks' }, { kind: 'following' },
{ kind: 'home' }, { kind: 'notifications' }, { kind: 'mentions' }, { kind: 'search', value: 'from:otheraccount filter:media' },
{ kind: 'user', value: 'otheraccount' }, { kind: 'list', value: '1234567890' }, { kind: 'bookmarks' }, { kind: 'following' },
];
export async function mountShellHarness(root: HTMLElement) {

View file

@ -16,7 +16,7 @@ const settings: ProfileSettings = normalizeProfiles({
active: 'default',
profiles: [
{ id: 'default', name: 'Main', color: '#1d9bf0', account: { handle: 'CRTkafa', name: 'CRT kafa', signedIn: true } },
{ id: 'pwork1', name: 'Work', color: '#7856ff', account: { handle: 'playfishies', name: 'Fishies', signedIn: true } },
{ id: 'pwork1', name: 'Work', color: '#7856ff', account: { handle: 'otheraccount', name: 'Sample', signedIn: true } },
{ id: 'pout01', name: 'Old', color: '#f91880', account: { handle: 'oldone', name: 'Old', signedIn: false } },
{ id: 'pnew01', name: 'New account', color: '#00ba7c' },
],

View file

@ -287,7 +287,7 @@ class El {
const EDITOR = { 'data-testid': 'tweetTextarea_0', role: 'textbox', contenteditable: 'true' };
/** X's page with the account button and one composer (editor, toolbar with Post). */
function composerPage(handle = 'fishies') {
function composerPage(handle = 'sample') {
const block = new El('div', { 'data-block': 'true' });
const editor = new El('div', EDITOR, new El('div', { 'data-contents': 'true' }, block));
const postButton = new El('button', { 'data-testid': 'tweetButtonInline' }, 'Post');
@ -366,16 +366,16 @@ test('length meter: counts the way X counts', () => {
});
test('"Posting as": one colour per account, whatever the case of the handle', () => {
assert.equal(accountHue('Fishies'), accountHue('fishies'));
assert.notEqual(accountHue('fishies'), accountHue('someone'));
for (const handle of ['a', 'fishies', 'someone_else', 'A_15_char_handl']) {
assert.equal(accountHue('Sample'), accountHue('sample'));
assert.notEqual(accountHue('sample'), accountHue('someone'));
for (const handle of ['a', 'sample', 'someone_else', 'A_15_char_handl']) {
const hue = accountHue(handle);
assert.ok(Number.isInteger(hue) && hue >= 0 && hue < 360, handle);
}
});
test('"Posting as" and the length meter: one line above the toolbar, kept current, gone without a trace', () => {
const page = composerPage('fishies');
const page = composerPage('sample');
const before = page.body.html();
const env = composeEnv(page);
const idle = env.listeners();
@ -385,10 +385,10 @@ test('"Posting as" and the length meter: one line above the toolbar, kept curren
const bar = page.toolbar.previousElementSibling!;
assert.ok(bar.hasAttribute('data-bt-compose-bar'), 'the line is right above X\'s toolbar');
const chip = bar.querySelector('[data-bt-compose-chip]')!;
assert.equal(chip.textContent, 'Posting as @fishies');
assert.equal(chip.getAttribute('data-bt-compose-chip'), 'fishies');
assert.equal(chip.style.props['--bt-compose-hue'], String(accountHue('fishies')));
assert.match(chip.title, /@fishies/);
assert.equal(chip.textContent, 'Posting as @sample');
assert.equal(chip.getAttribute('data-bt-compose-chip'), 'sample');
assert.equal(chip.style.props['--bt-compose-hue'], String(accountHue('sample')));
assert.match(chip.title, /@sample/);
assert.equal(bar.querySelector('[data-bt-compose-meter]'), null, 'an empty composer has nothing to measure');
// Typing: the meter follows, in the same line.
page.block.textContent = 'hello https://example.com/a/long/link';
@ -442,7 +442,7 @@ test('"Posting as" and the length meter: one line above the toolbar, kept curren
env.registry.setFlags(only({}));
second.remove();
page.block.textContent = '';
page.avatar.setAttribute('data-testid', 'UserAvatar-Container-fishies');
page.avatar.setAttribute('data-testid', 'UserAvatar-Container-sample');
assert.equal(page.body.html(), before);
assert.deepEqual(env.live(), []);
assert.equal(env.listeners(), idle);

View file

@ -8,7 +8,7 @@ import { asPreview, isBareCard, previewable } from '../src/areas/chat/linkPrevie
test('link previews: only plain https links to hosts that are not X\'s own', () => {
assert.equal(previewable('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), 'https://www.youtube.com/watch?v=dQw4w9WgXcQ');
assert.equal(previewable('https://fishies.lol/'), 'https://fishies.lol/');
assert.equal(previewable('https://example.org/'), 'https://example.org/');
for (const bad of [
'http://example.com/', 'https://user:pw@example.com/', 'https://example.com:8443/', 'https://192.168.0.1/', 'https://[::1]/',
'https://intranet/', 'https://x.com/a/status/1', 'https://t.co/abc', 'https://pbs.twimg.com/media/a.jpg', 'javascript:alert(1)', '/i/chat/1-2', '', null,
@ -16,7 +16,7 @@ test('link previews: only plain https links to hosts that are not X\'s own', ()
});
test('link previews: a native answer is checked again: plain fields, capped, data: images of four types, an 11-character video id', () => {
const good = asPreview({ url: 'https://fishies.lol/', site: 'Fishies', title: 'The social aquarium', description: 'x'.repeat(400), image: 'data:image/png;base64,iVBORw0KGgo=' });
const good = asPreview({ url: 'https://example.org/', site: 'Sample', title: 'The social aquarium', description: 'x'.repeat(400), image: 'data:image/png;base64,iVBORw0KGgo=' });
assert.ok(good);
assert.equal(good.description.length, 300);
assert.equal(good.image, 'data:image/png;base64,iVBORw0KGgo=');
@ -37,7 +37,7 @@ test('link previews: a native answer is checked again: plain fields, capped, dat
test('link previews: X\'s bare card (a host name and an icon) is replaced; a card with its own picture or a sentence is not', () => {
const anchor = (text: string, media = false) => ({ textContent: text, querySelector: () => (media ? {} : null) }) as unknown as Element;
assert.equal(isBareCard(anchor('www.youtube.com')), true);
assert.equal(isBareCard(anchor('fishies.lol')), true);
assert.equal(isBareCard(anchor('example.org')), true);
assert.equal(isBareCard(anchor('www.youtube.com', true)), false);
assert.equal(isBareCard(anchor('watch this video')), false);
assert.equal(isBareCard(anchor('www.youtube.com/watch?v=jNQXAC9IVRw')), false, 'a link in the text (live 2026-10-10) stays');

View file

@ -10,7 +10,7 @@ import { CLICK_GUARD_MS, QUEUE_MAX_MS, installClickGuard, installMutationGate }
// Clicks reach the window capture listener first (the guard), then X's handler, like in the page.
const POST = '2103816196386852995', USER = '2086263555364691968';
const BASE = 'https://x.com/playfishies/status/' + POST;
const BASE = 'https://x.com/otheraccount/status/' + POST;
function clock() {
let now = 0, seq = 0;

View file

@ -11,7 +11,7 @@ test('deck columns: every kind loads an x.com page', () => {
assert.equal(xColumnUrl({ kind: 'mentions' }), 'https://x.com/notifications/mentions');
assert.equal(xColumnUrl({ kind: 'bookmarks' }), 'https://x.com/i/bookmarks');
assert.equal(xColumnUrl({ kind: 'list', value: '1234' }), 'https://x.com/i/lists/1234');
assert.equal(xColumnUrl({ kind: 'user', value: 'playfishies' }), 'https://x.com/playfishies');
assert.equal(xColumnUrl({ kind: 'user', value: 'otheraccount' }), 'https://x.com/otheraccount');
assert.equal(xColumnUrl({ kind: 'search', value: 'from:me filter:media' }), 'https://x.com/search?q=from%3Ame%20filter%3Amedia&f=live');
for (const bad of [{ kind: 'list', value: 'abc' }, { kind: 'user', value: 'not a handle' }, { kind: 'search' }, { kind: 'rocket' }]) {
assert.equal(xColumnUrl(bad), null, JSON.stringify(bad));
@ -23,20 +23,20 @@ test('deck columns: every kind loads an x.com page', () => {
});
test('deck columns: typed values are parsed per kind', () => {
assert.equal(parseColumnValue('user', '@playfishies'), 'playfishies');
assert.equal(parseColumnValue('user', '@otheraccount'), 'otheraccount');
assert.equal(parseColumnValue('user', 'https://x.com/CRTkafa/status/1'), 'CRTkafa');
assert.equal(parseColumnValue('user', 'twitter.com/nope nope'), null);
assert.equal(parseColumnValue('list', 'https://x.com/i/lists/1590000000000000001'), '1590000000000000001');
assert.equal(parseColumnValue('list', '42'), '42');
assert.equal(parseColumnValue('list', 'https://x.com/i/lists/abc'), null);
assert.equal(parseColumnValue('search', ' wayBetter from:@playfishies '), 'wayBetter from:playfishies');
assert.equal(parseColumnValue('search', ' wayBetter from:@otheraccount '), 'wayBetter from:otheraccount');
assert.equal(parseColumnValue('search', ' '), null);
assert.equal(parseColumnValue('home', 'ignored'), '');
});
test('deck columns: header titles', () => {
assert.deepEqual(columnTitle({ kind: 'notifications' }), { title: 'Notifications' });
assert.deepEqual(columnTitle({ kind: 'user', value: 'playfishies' }), { title: '@playfishies', detail: 'Profile' });
assert.deepEqual(columnTitle({ kind: 'user', value: 'otheraccount' }), { title: '@otheraccount', detail: 'Profile' });
assert.deepEqual(columnTitle({ kind: 'search', value: 'gm' }), { title: 'gm', detail: 'Search · Latest' });
assert.deepEqual(columnTitle({ kind: 'home' }, 'Main feed'), { title: 'Main feed', detail: 'Home' });
});

View file

@ -5,8 +5,8 @@ import { besideGroup, type GroupBox } from '../src/shell/paneModel.ts';
test('posts, profiles, hashtags, searches, lists and communities are routable', () => {
for (const [path, search] of [
['/playfishies/status/1840000000000000000', ''],
['/playfishies/status/1840000000000000000/quotes', ''],
['/otheraccount/status/1840000000000000000', ''],
['/otheraccount/status/1840000000000000000/quotes', ''],
['/CRTkafa', ''],
['/CRTkafa/', ''],
['/CRTkafa/media', ''],
@ -20,9 +20,9 @@ test('posts, profiles, hashtags, searches, lists and communities are routable',
test('media viewers, X navigation pages and actions stay in place', () => {
for (const [path, search] of [
['/playfishies/status/1840000000000000000/photo/1', ''],
['/playfishies/status/1840000000000000000/video/1', ''],
['/playfishies/status/1840000000000000000/analytics', ''],
['/otheraccount/status/1840000000000000000/photo/1', ''],
['/otheraccount/status/1840000000000000000/video/1', ''],
['/otheraccount/status/1840000000000000000/analytics', ''],
['/home', ''], ['/explore', ''], ['/notifications', ''], ['/messages', ''], ['/settings', ''], ['/i/bookmarks', ''],
['/compose/post', ''], ['/i/chat/123', ''], ['/search', ''], ['/logout', ''], ['/', ''],
['/waytoolonghandlename_x', ''], ['/i/spaces/1', ''],

View file

@ -98,7 +98,7 @@ function env() {
const fetch = (async (input: unknown) => String(input).includes('/live_pipeline/events')
? new Response(stream, { status: 200, headers: { 'content-type': 'text/event-stream' } })
: new Response('{}')) as typeof globalThis.fetch;
const win = { fetch, location: { href: `https://x.com/playfishies/status/${POST}` }, XMLHttpRequest: FakeXHR as unknown as typeof XMLHttpRequest };
const win = { fetch, location: { href: `https://x.com/otheraccount/status/${POST}` }, XMLHttpRequest: FakeXHR as unknown as typeof XMLHttpRequest };
const send = (op: 'FavoriteTweet' | 'UnfavoriteTweet', id = POST) => {
const xhr = new win.XMLHttpRequest();
xhr.open('POST', `/i/api/graphql/q/${op}`);

View file

@ -52,15 +52,15 @@ test('an account seen natively survives normalization; forged or broken ones do
const settings = normalizeProfiles({
active: 'default',
profiles: [
{ id: 'default', name: 'Fishies', color: '#1d9bf0', account: { handle: 'playfishies', name: 'Fishies', avatar: 'https://pbs.twimg.com/profile_images/1/a_x96.jpg', signedIn: true } },
{ id: 'default', name: 'Sample', color: '#1d9bf0', account: { handle: 'otheraccount', name: 'Sample', avatar: 'https://pbs.twimg.com/profile_images/1/a_x96.jpg', signedIn: true } },
{ id: 'p1', name: 'x', color: '#f91880', account: { handle: 'bad handle!', name: 'x', signedIn: true } },
{ id: 'p2', name: 'y', color: '#00ba7c', account: { handle: 'CRTkafa', name: 'CRT', avatar: 'https://evil.test/a.png', signedIn: false } },
],
});
assert.equal(settings.profiles[0]!.account?.handle, 'playfishies');
assert.equal(settings.profiles[0]!.account?.handle, 'otheraccount');
assert.equal(settings.profiles[1]!.account, undefined);
assert.equal(settings.profiles[2]!.account?.avatar, undefined);
assert.deepEqual(accountLabel(settings.profiles[0]!), { title: 'Fishies', detail: '@playfishies', state: 'signed-in' });
assert.deepEqual(accountLabel(settings.profiles[0]!), { title: 'Sample', detail: '@otheraccount', state: 'signed-in' });
assert.deepEqual(accountLabel(settings.profiles[2]!), { title: 'CRT', detail: '@CRTkafa · signed out', state: 'signed-out' });
assert.equal(accountLabel(settings.profiles[1]!).state, 'new');
});

View file

@ -7,13 +7,13 @@ import {
const U = '2086263555364691968';
const OTHER = '1790000000000000001';
const BASE = 'https://x.com/playfishies';
const BASE = 'https://x.com/otheraccount';
// Shapes as X sends them (live 2026-09-28): GraphQL user result (UserByScreenName, URT timelines)
// and a REST 1.1 user object (friendships/create.json, friends/following/list.json).
const gqlUser = (id: string, following: boolean, followers = 21) => ({
__typename: 'User', rest_id: id, follow_request_sent: false, privacy: { protected: false },
core: { name: 'Fishies', screen_name: 'playfishies' },
core: { name: 'Sample', screen_name: 'otheraccount' },
relationship_counts: { followers, following: 31 },
relationship_perspectives: { followed_by: true, following, live_following: false, blocking: false, muting: false, blocked_by: false },
});
@ -93,11 +93,11 @@ test('strip: GraphQL and REST user objects of the listed users only', () => {
assert.equal('relationship_perspectives' in mine, false);
assert.equal('follow_request_sent' in mine, false);
assert.deepEqual(mine.relationship_counts, { following: 31 });
assert.deepEqual(mine.core, { name: 'Fishies', screen_name: 'playfishies' }, 'other fields untouched');
assert.deepEqual(mine.core, { name: 'Sample', screen_name: 'otheraccount' }, 'other fields untouched');
assert.deepEqual(other, gqlUser(OTHER, false), 'other users untouched');
const rest = { id: 1, id_str: U, screen_name: 'playfishies', following: false, followers_count: 21, follow_request_sent: false, followed_by: true, legacy: { following: false, followers_count: 21, name: 'x' } };
const rest = { id: 1, id_str: U, screen_name: 'otheraccount', following: false, followers_count: 21, follow_request_sent: false, followed_by: true, legacy: { following: false, followers_count: 21, name: 'x' } };
assert.equal(stripRelationship([rest], new Set([`follow:${U}`])), 1);
assert.deepEqual(rest, { id: 1, id_str: U, screen_name: 'playfishies', followed_by: true, legacy: { name: 'x' } });
assert.deepEqual(rest, { id: 1, id_str: U, screen_name: 'otheraccount', followed_by: true, legacy: { name: 'x' } });
assert.equal(stripRelationship({ id_str: U, name: 'no relationship fields' }, new Set([`follow:${U}`])), 0);
});