// Regression tests for the visual-bug audit (scripts/visual-bugs.mjs): what gets hidden with a // hidden count or sidebar module, and when X's Chat button steps aside. Layout itself (overlaps, // gaps, empty boxes) is checked in a browser by the audit script. import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; import { navigationCss } from '../src/areas/navigation/css.ts'; import { profileCss } from '../src/areas/profile/css.ts'; import { readerCss } from '../src/areas/reader/css.ts'; import { FEATURE_DEFAULTS, VIEWS_ATTR, buildFeatureCss, focalViewsParts, sidebarModuleSelectors, type FeatureConfig, type ShapeNode } from '../src/inject/features.ts'; import { LAUNCHER_MAX_HEIGHT, overlaps, shouldTuck, unionRect, type Rect } from '../src/fixes/chatButton.ts'; type PluginClass = { new(): { start(): void; stop(): void }; [key: string]: unknown }; function load(file: string): PluginClass { const module = { exports: {} as unknown }; new Function('module', 'exports', readFileSync(new URL(`../src-tauri/bundled-plugins/${file}`, import.meta.url), 'utf8'))(module, module.exports); return module.exports as PluginClass; } const FocusMode = load('focus-mode.plugin.js') as PluginClass & { css(settings: Record): string; sidebarModule(base: string, inside?: string): string[]; viewsParts(time: ShapeNode): { unit: ShapeNode[]; separators: ShapeNode[] } | null; }; // -- a DOM-shaped tree without a DOM: tag, attributes, text, parent/sibling links -- type Fake = ShapeNode & { children: Fake[]; name?: string }; function el(tag: string, attrs: Record = {}, ...kids: (Fake | string)[]): Fake { const children = kids.filter((kid): kid is Fake => typeof kid !== 'string'); const own = kids.filter(kid => typeof kid === 'string').join(''); const node: Fake = { tagName: tag.toUpperCase(), parentElement: null, nextElementSibling: null, children, get textContent() { return own + children.map(child => child.textContent).join(''); }, getAttribute: (name: string) => (Object.hasOwn(attrs, name) ? attrs[name]! : null), name: attrs['data-name'], }; children.forEach((child, i) => { child.parentElement = node; child.nextElementSibling = children[i + 1] ?? null; }); return node; } const find = (root: Fake, name: string): Fake => { if (root.name === name) return root; for (const child of root.children) { const hit = find(child, name) as Fake | null; if (hit) return hit; } return null as unknown as Fake; }; /** X's opened-post date line: [date link box] [·] [views], views either a link (own post) or text. */ function dateLine({ own, split = false }: { own: boolean; split?: boolean }) { const count = el('span', { 'data-testid': 'app-text-transition-container' }, el('span', {}, '1,234')); const words = [el('span', {}, ' '), el('span', { 'data-name': 'word' }, 'Views')]; const unit = own ? el('a', { href: '/CRTkafa/status/1/analytics', 'data-name': 'views' }, count, ...words) : el('div', { 'data-name': 'views' }, count, ...words); const row = el('div', {}, el('div', {}, el('a', { href: '/CRTkafa/status/1' }, el('time', { datetime: '2026-09-28T19:06:00.000Z', 'data-name': 'time' }, '10:06 PM · Sep 28, 2026'))), el('div', { 'aria-hidden': 'true', 'data-name': 'sep' }, el('span', {}, '·')), ...(split ? [el('div', { 'data-name': 'views' }, count), ...words] : [unit]), ); return el('article', { 'data-testid': 'tweet' }, el('div', {}, row)); } for (const [label, parts] of [['features.ts', focalViewsParts], ['Focus Mode', (t: ShapeNode) => FocusMode.viewsParts(t)]] as const) { test(`${label}: an opened post's views unit is found with its separator, for own and others' posts`, () => { for (const own of [true, false]) { const tree = dateLine({ own }); const result = parts(find(tree, 'time')); assert.ok(result, `own=${own}`); assert.deepEqual(result.unit, [find(tree, 'views')]); assert.deepEqual(result.separators, [find(tree, 'sep')]); } }); test(`${label}: a count and "Views" that are separate siblings go together`, () => { const tree = dateLine({ own: false, split: true }); const result = parts(find(tree, 'time')); assert.ok(result); assert.equal(result.unit.length, 3, 'count, space and word'); assert.ok(result.unit.includes(find(tree, 'word'))); }); test(`${label}: a date line without a count (or a timeline header) marks nothing`, () => { const header = el('article', {}, el('div', {}, el('div', {}, el('a', { href: '/jack' }, '@jack')), el('div', {}, '·'), el('div', {}, el('a', { href: '/jack/status/20' }, el('time', { datetime: 'x', 'data-name': 'time' }, '2h'))), )); assert.equal(parts(find(header, 'time')), null); }); } test('sidebar modules hide with their bordered card, and only a card that holds nothing else', () => { const [module, card, outer] = sidebarModuleSelectors('section[role="region"]', '[data-testid="trend"]'); assert.equal(module, '[data-testid="sidebarColumn"] section[role="region"]:has([data-testid="trend"])'); assert.match(card!, /div:has\(> section\[role="region"\] \[data-testid="trend"\]\):not\(:has\(> :not\(section\[role="region"\]\)\)\)/); assert.match(outer!, /:not\(:has\(> :not\(div\)\)\)/); // No :has() inside :has() (invalid CSS: the whole rule would be dropped). for (const selector of [module, card, outer]) assert.doesNotMatch(selector!, /:has\([^)]*:has\(/); }); test('every sidebar-module tweak also hides the module card; Trends stay scoped to the right column', () => { // The right-column modules are Navigation+'s (src/areas/navigation/css.ts), the view counts Reader+'s, the profile one Profile+'s. const only = (key: keyof FeatureConfig) => { const config = { ...Object.fromEntries(Object.keys(FEATURE_DEFAULTS).map(k => [k, false])), [key]: true } as FeatureConfig; return [buildFeatureCss(config), navigationCss(config), readerCss(config), profileCss(config)].filter(Boolean).join('\n\n'); }; for (const key of ['hideSidebarTrends', 'hideWhoToFollow', 'hideLiveOnX', 'hideTodaysNews', 'hidePremiumUpsells', 'hideRelevantPeople', 'hideProfileWhoToFollow'] as const) { assert.match(only(key), /\[data-testid="sidebarColumn"\] div:has\(> /, key); } // Explore's own trends are hideExploreContent's business, not the sidebar tweak's. assert.doesNotMatch(only('hideSidebarTrends'), /(^|,\n)section\[role="region"\]/); const views = only('hideViewCounts'); assert.ok(views.includes(`[${VIEWS_ATTR}]`), 'the opened post views unit'); assert.ok(views.includes('a[href$="/analytics"]:not([role="group"] a)')); }); test('Focus Mode: counts go without leaving words, separators, gaps or empty cards', () => { const css = FocusMode.css({ enabled: true, metrics: true, trends: true, whoToFollow: true, followCounts: true, sidebar: false }); assert.ok(css.includes('[data-bt-fm-views]') && css.includes('[data-bt-fm-views-sep]'), 'views unit and its separator'); // Follower numbers: display none (no blank gap before "Followers"). assert.match(css, /a\[href\$="\/followers"\][^{]*> span:first-child[^{]*\{ display: none !important; \}/); // Sidebar cards of hidden trends / suggestions go too. assert.ok(css.includes('[data-testid="sidebarColumn"] div:has(> section[role="region"] [data-testid="trend"])')); assert.ok(css.includes('[data-testid="sidebarColumn"] div:has(> aside[role="complementary"] [data-testid="UserCell"])')); for (const rule of css.split('\n')) assert.doesNotMatch(rule, /:has\([^)]*:has\(/, 'no nested :has()'); assert.equal(FocusMode.css({ enabled: false, metrics: true, trends: true, whoToFollow: true, followCounts: true, sidebar: false }), ''); }); const R = (left: number, top: number, right: number, bottom: number): Rect => ({ left, top, right, bottom }); const VIEW = { width: 600, height: 800 }; test('Chat button: steps aside only while its collapsed button overlaps a composer on screen', () => { const button = R(524, 724, 580, 780); assert.equal(shouldTuck(button, [R(0, 700, 600, 790)], VIEW), true, 'reply box under the button'); assert.equal(shouldTuck(button, [R(0, 100, 600, 180)], VIEW), false, 'composer elsewhere'); assert.equal(shouldTuck(button, [R(0, 900, 600, 980)], VIEW), false, 'composer below the fold'); assert.equal(shouldTuck(button, [R(0, 700, 600, 790)], VIEW, true), false, 'the button has the focus'); assert.equal(shouldTuck(null, [R(0, 700, 600, 790)], VIEW), false, 'no chat button'); const drawer = R(200, 300, 580, 780); assert.ok(drawer.bottom - drawer.top > LAUNCHER_MAX_HEIGHT); assert.equal(shouldTuck(drawer, [R(0, 700, 600, 790)], VIEW), false, 'an open chat drawer is never hidden'); }); test('Chat button: the launcher is what it paints (zero-size fixed wrappers add nothing)', () => { assert.deepEqual(unionRect([R(600, 800, 600, 800), R(524, 724, 580, 780)]), R(524, 724, 580, 780)); assert.equal(unionRect([R(0, 0, 0, 0)]), null); assert.equal(overlaps(R(0, 0, 10, 10), R(20, 0, 30, 10), 8), false); assert.equal(overlaps(R(0, 0, 10, 10), R(15, 0, 30, 10), 8), true, 'within the 8px breathing room'); });