import assert from 'node:assert/strict'; import test from 'node:test'; import { navigationCss } from '../src/areas/navigation/css.ts'; import { profileCss } from '../src/areas/profile/css.ts'; import { READER_CSS, readerCss } from '../src/areas/reader/css.ts'; import { EXPLORE_CSS } from '../src/areas/search/hideExploreContent.ts'; import { FEATURE_DEFAULTS, buildFeatureCss, installFeatures, normalizeFeatures } from '../src/inject/features.ts'; import type { FeatureConfig } from '../src/inject/features.ts'; const ALL_FALSE: FeatureConfig = Object.fromEntries( Object.keys(FEATURE_DEFAULTS).map(key => [key, false]), ) as unknown as FeatureConfig; function withOnly(key: keyof FeatureConfig): FeatureConfig { return { ...ALL_FALSE, [key]: true }; } /** Every rule a config turns on: the runtime's own, Navigation+'s (src/areas/navigation/css.ts) and Reader+'s (src/areas/reader/css.ts). */ const allCss = (config: FeatureConfig) => [buildFeatureCss(config), navigationCss(config), readerCss(config), profileCss(config), config.hideExploreContent ? EXPLORE_CSS : ''].filter(Boolean).join('\n\n'); test('FEATURE_DEFAULTS matches the specified defaults exactly', () => { assert.deepEqual(FEATURE_DEFAULTS, { logoBrush: true, replaceLogo: false, twitterWording: true, hidePromotedPosts: true, hidePremiumUpsells: false, hideGrok: false, hideSidebarTrends: false, hideWhoToFollow: false, hideLiveOnX: false, hideTodaysNews: false, hideViewCounts: false, hideEngagementCounts: false, hideBookmarkButton: false, hideExploreContent: false, followingTimelineDefault: false, hideForYouTab: false, hidePremiumNav: false, hideChatNav: false, hideCommunitiesNav: false, hideGrokNav: false, singleSplitNav: true, disableTweetTextFormatting: false, mediaDownload: true, composerDrafts: true, searchOperators: true, fixLikeFollow: true, fixScrollRestore: true, fixTimelineJump: true, fixHomeTab: true, fixNotificationBadge: true, fixNewPostsPill: true, fixMediaUpload: true, followingChronological: true, composerRestorePrompt: true, fixChatButton: true, rememberVideoVolume: true, disableVideoAutoplay: false, stopVideoLoop: false, sortRepliesRecent: false, fullNumbers: false, hideVerifiedBadges: false, hideSubscribeButtons: false, fullWidthTimeline: false, hideSidebar: false, hideRelevantPeople: false, hideProfileWhoToFollow: false, }); }); test('normalizeFeatures drops unknown keys and falls back to defaults for non-boolean values', () => { const input = { replaceLogo: false, hidePromotedPosts: 'yes', hideGrok: 1, hideBookmarkButton: null, unknownFeature: true, __proto__: { polluted: true }, }; const result = normalizeFeatures(input); assert.equal(result.replaceLogo, false); assert.equal(result.hidePromotedPosts, FEATURE_DEFAULTS.hidePromotedPosts); assert.equal(result.hideGrok, FEATURE_DEFAULTS.hideGrok); assert.equal(result.hideBookmarkButton, FEATURE_DEFAULTS.hideBookmarkButton); assert.equal(Object.keys(result).length, Object.keys(FEATURE_DEFAULTS).length); assert.equal((result as Record).unknownFeature, undefined); assert.equal(Object.hasOwn(Object.prototype, 'polluted'), false); }); test('normalizeFeatures returns full defaults for non-object input', () => { for (const input of [null, undefined, 'config', 42, true, []]) { assert.deepEqual(normalizeFeatures(input), FEATURE_DEFAULTS); } }); test('normalizeFeatures preserves every valid boolean value untouched', () => { const allTrue = Object.fromEntries(Object.keys(FEATURE_DEFAULTS).map(key => [key, true])); assert.deepEqual(normalizeFeatures(allTrue), Object.fromEntries(Object.keys(FEATURE_DEFAULTS).map(key => [key, true]))); assert.deepEqual(normalizeFeatures(ALL_FALSE), ALL_FALSE); }); test('buildFeatureCss emits nothing for an all-false config', () => { assert.equal(buildFeatureCss(ALL_FALSE), ''); }); test('buildFeatureCss is pure and includes only the selectors for enabled flags', () => { const markers: Record = { logoBrush: 'background: url("data:image/png', replaceLogo: 'bt-logo-patched', twitterWording: '', hidePromotedPosts: 'placementTracking', hidePremiumUpsells: 'premium_sign_up', hideGrok: 'grokImgGen', hideSidebarTrends: '[data-testid="trend"]', hideWhoToFollow: 'UserCell', hideLiveOnX: 'Live on X', hideTodaysNews: 'news_sidebar', hideViewCounts: '/analytics', hideEngagementCounts: 'visibility: hidden', hideBookmarkButton: 'ookmark', hideExploreContent: 'primaryColumn', followingTimelineDefault: '', hideForYouTab: 'data-bt-home', hidePremiumNav: '/i/verified', hideChatNav: '/i/chat', hideCommunitiesNav: '/communities', hideGrokNav: 'a[href="/i/grok"]', singleSplitNav: '', disableTweetTextFormatting: 'tweetText', mediaDownload: '', composerDrafts: '', searchOperators: '', fixLikeFollow: '', fixScrollRestore: '', fixTimelineJump: '', fixHomeTab: '', fixNotificationBadge: '', fixNewPostsPill: '', fixMediaUpload: '', followingChronological: '', composerRestorePrompt: '', fixChatButton: '', rememberVideoVolume: '', disableVideoAutoplay: '', stopVideoLoop: '', sortRepliesRecent: '', fullNumbers: '', hideVerifiedBadges: 'icon-verified', hideSubscribeButtons: 'creator-subscriptions', fullWidthTimeline: 'flex-grow: 1', hideSidebar: '[data-testid="sidebarColumn"] {', hideRelevantPeople: 'data-bt-route="status"', hideProfileWhoToFollow: 'connect_people', }; for (const key of Object.keys(FEATURE_DEFAULTS) as (keyof FeatureConfig)[]) { const marker = markers[key]; if (!marker) continue; // flags with no marker are DOM/JS-only, not CSS const cssWithFlag = allCss(withOnly(key)); assert.ok(cssWithFlag.includes(marker), `expected the rules of ${key}=true to include ${JSON.stringify(marker)}`); assert.equal(allCss(ALL_FALSE).includes(marker), false, `expected the rules of all-false to exclude ${JSON.stringify(marker)}`); } }); test('hidePromotedPosts hides ad posts, never the placementTracking wrapper of an ordinary video', () => { const css = READER_CSS.hidePromotedPosts(); assert.ok(css.includes('[data-testid="placementTracking"]:has(article)')); // A bare wrapper selector would also hide X's video players inside normal posts (live, 2026-09-27). assert.doesNotMatch(css, /\[data-testid="placementTracking"\](?!:has\(article\)| article)/); }); test('buildFeatureCss keeps hideGrok and hideGrokNav selectors disjoint', () => { const grokContentOnly = allCss(withOnly('hideGrok')); const grokNavOnly = allCss(withOnly('hideGrokNav')); assert.ok(grokContentOnly.includes('grokImgGen')); assert.equal(grokContentOnly.includes('header nav a[href="/i/grok"]'), false); assert.ok(grokNavOnly.includes('header nav a[href="/i/grok"]')); assert.equal(grokNavOnly.includes('grokImgGen'), false); }); test('buildFeatureCss default config hides promoted posts and shows the brush logo, nothing else', () => { const css = allCss(FEATURE_DEFAULTS); assert.ok(css.includes('placementTracking')); assert.ok(css.includes('data:image/png')); assert.equal(css.includes('bt-logo-patched'), false, 'brush logo wins over the Twitter bird'); assert.equal(css.includes('news_sidebar'), false); assert.equal(css.includes('UserCell'), false); assert.equal(css.includes('grokImgGen'), false); }); function fakeWindow(overrides: { top?: unknown; origin?: string; pathname?: string } = {}): Window { const elements = new Map void; setAttribute?: (n: string, v: string) => void; getAttribute?: (n: string) => string | null }>(); const documentElement = { attrs: new Map(), setAttribute(n: string, v: string) { this.attrs.set(n, v); }, removeAttribute(n: string) { this.attrs.delete(n); }, getAttribute(n: string) { return this.attrs.get(n) ?? null; } }; const head = { appendChild(el: { id: string }) { elements.set(el.id, el as never); } }; const doc = { title: 'Home / X', head, documentElement, body: {}, createElement(_tag: string) { const el = { id: '', textContent: '', remove() { elements.delete(el.id); } }; return el; }, getElementById(id: string) { return elements.get(id) ?? null; }, querySelector(_sel: string) { return null; }, querySelectorAll(_sel: string) { return [] as unknown[]; }, addEventListener() {}, removeEventListener() {}, }; class FakeObserver { disconnected = false; constructor(_cb: () => void) {} observe() {} disconnect() { this.disconnected = true; } } const win: Record = { document: doc, location: { origin: overrides.origin ?? 'https://x.com', pathname: overrides.pathname ?? '/home' }, MutationObserver: FakeObserver, requestAnimationFrame(cb: () => void) { cb(); return 0; }, addEventListener() {}, removeEventListener() {}, }; win.top = overrides.top ?? win; return win as unknown as Window; } test('installFeatures is a no-op outside the top-level https://x.com page', () => { const framed = installFeatures(fakeWindow({ top: {} })); const wrongOrigin = installFeatures(fakeWindow({ origin: 'https://example.com' })); assert.doesNotThrow(() => framed.apply({ replaceLogo: false })); assert.doesNotThrow(() => framed.dispose()); assert.doesNotThrow(() => wrongOrigin.apply({})); assert.doesNotThrow(() => wrongOrigin.dispose()); }); test('installFeatures is idempotent per window and dispose cleans up the style tag', () => { const win = fakeWindow(); const first = installFeatures(win); const second = installFeatures(win); assert.equal(first, second); first.apply({ logoBrush: true }); const style = (win.document as unknown as { getElementById(id: string): { textContent: string } | null }).getElementById('bt-features'); assert.ok(style); assert.ok(style!.textContent.includes('background: url("data:image/png')); first.dispose(); const afterDispose = (win.document as unknown as { getElementById(id: string): unknown }).getElementById('bt-features'); assert.equal(afterDispose, null); }); test('logo choice is exclusive: brush wins, bird needs brush off, both off keeps X', () => { const bird = buildFeatureCss({ ...ALL_FALSE, replaceLogo: true }); assert.ok(bird.includes('bt-logo-patched')); assert.equal(bird.includes('data:image/png'), false); const both = buildFeatureCss({ ...ALL_FALSE, replaceLogo: true, logoBrush: true }); assert.ok(both.includes('data:image/png')); assert.equal(both.includes('bt-logo-patched'), false); assert.equal(buildFeatureCss(ALL_FALSE).includes('path[d='), false); }); // Minimal DOM stand-ins for the runtime patches (no jsdom in this suite). type FakeNode = { nodeType: number; nodeValue: string | null }; class FakeEl { attrs = new Map(); classes = new Set(); childNodes: FakeNode[] = []; children: FakeEl[] = []; tag: string; constructor(tag: string, text?: string) { this.tag = tag; if (text != null) this.childNodes.push({ nodeType: 3, nodeValue: text }); } get classList() { return { add: (c: string) => this.classes.add(c), remove: (c: string) => this.classes.delete(c) }; } add(...kids: FakeEl[]) { this.children.push(...kids); return this; } all(): FakeEl[] { return this.children.flatMap(kid => [kid, ...kid.all()]); } setAttribute(n: string, v: string) { this.attrs.set(n, v); } getAttribute(n: string) { return this.attrs.get(n) ?? null; } hasAttribute(n: string) { return this.attrs.has(n); } removeAttribute(n: string) { this.attrs.delete(n); } matches(sel: string): boolean { const attr = /^(\w+)?\[([\w-]+)(?:~?="([^"]*)")?\]$/.exec(sel); if (attr) return (!attr[1] || attr[1] === this.tag) && this.attrs.has(attr[2]!) && (attr[3] == null || (this.attrs.get(attr[2]!) ?? '').split(' ').includes(attr[3])); return sel === this.tag; } querySelectorAll(sel: string) { return this.all().filter(el => el.matches(sel)); } querySelector(sel: string) { return this.querySelectorAll(sel)[0] ?? null; } } function domWindow(root: FakeEl, byTestId: Record): Window { const win = fakeWindow() as unknown as Record; const doc = win.document as Record; doc.querySelector = (sel: string) => { const id = /data-testid(?:\^)?="([^"]+)"/.exec(sel)?.[1]; return id != null ? byTestId[id] ?? null : root.querySelector(sel); }; doc.querySelectorAll = (sel: string) => sel === 'h1 svg path' ? root.querySelectorAll('path') : sel.includes('data-testid') ? [] : root.querySelectorAll(sel); return win as unknown as Window; } test('twitterWording no longer retexts labels in the page: the wording is a built-in string pack', () => { const label = new FakeEl('span', 'Post'); const composer = new FakeEl('button').add(label); const root = new FakeEl('body').add(composer); const win = domWindow(root, { tweetButton: composer }); const handle = installFeatures(win); handle.apply({ ...ALL_FALSE, twitterWording: true }); assert.equal(label.childNodes[0]!.nodeValue, 'Post', 'the label node is left as the catalog gave it'); assert.equal(label.getAttribute('data-bt-retext'), null); handle.dispose(); }); test('logo choices apply live and revert both ways: brush, bird and X', () => { const X_D = 'M21.742 21.75l-7.563-11.179 7.056-8.321h-2.456l-5.691 6.714-4.54-6.714H2.359l7.29 10.776L2.25 21.75h2.456l6.035-7.118 4.818 7.118h6.191-.008zM7.739 3.818L18.81 20.182h-2.447L5.29 3.818h2.447z'; const path = new FakeEl('path'); path.setAttribute('d', X_D); const icon = new FakeEl('link'); icon.setAttribute('rel', 'shortcut icon'); icon.setAttribute('href', '//abs.twimg.com/favicons/twitter.3.ico'); const root = new FakeEl('html').add(new FakeEl('svg').add(path), icon); const handle = installFeatures(domWindow(root, {})); handle.apply({ ...ALL_FALSE, logoBrush: true }); assert.equal(path.getAttribute('d'), X_D, 'brush is CSS-only and needs X\'s glyph in place'); assert.match(icon.getAttribute('href')!, /^data:image\/png/); handle.apply({ logoBrush: false, replaceLogo: true }); assert.notEqual(path.getAttribute('d'), X_D); assert.match(icon.getAttribute('href')!, /^data:image\/svg\+xml/); handle.apply({ replaceLogo: false, logoBrush: true }); assert.equal(path.getAttribute('d'), X_D, 'bird -> brush restores X\'s glyph for the brush CSS'); assert.match(icon.getAttribute('href')!, /^data:image\/png/); handle.apply({ logoBrush: false, replaceLogo: true }); handle.apply({ replaceLogo: false }); assert.equal(path.getAttribute('d'), X_D, 'bird -> X restores X\'s logo'); assert.equal(icon.getAttribute('href'), '//abs.twimg.com/favicons/twitter.3.ico', 'X gets its own favicon back'); handle.dispose(); });