// The feature table (src/areas/table.ts) as a whole: every flag has exactly one owner, the // defaults and modes keep what an existing setup has, every area, feature, group and mode has its // words, the native list generated from the table is current, surfaces are classified from the // path, and every module of every area starts and stops clean. docs/AREA-PLUGINS.md is the // contract these tests hold; each area's own behaviour is in tests/area-.test.ts. import assert from 'node:assert/strict'; import { readdirSync, readFileSync } from 'node:fs'; import test from 'node:test'; import { AREAS_RS, renderAreas } from '../scripts/build-areas.ts'; import { AREA_MESSAGES } from '../src/areas/messages.ts'; import { ALL_MODULES } from '../src/areas/modules.ts'; import { X_SURFACES, surfacesOf } from '../src/areas/surfaces.ts'; import { X_ADVANCED, X_ELSEWHERE, X_FEATURES, X_POWER, X_TABLE } from '../src/areas/table.ts'; import { FEATURE_DEFAULTS } from '../src/inject/features.ts'; import en from '../locales/en.json' with { type: 'json' }; import { AREA_COMMAND_BUDGET } from '../../../waybetter/packages/runtime/src/featureModules.ts'; import { areaFeatures, coreFeatures, effectiveFlags, featureWords, flagFeatures, modeFlags, supersededFiles, switchMode, tableDefaults, validateTable, } from '../../../waybetter/packages/runtime/src/featureTable.ts'; import { areaPage, flagsWith, messages } from './helpers/areaPage.ts'; const BUNDLED = new URL('../src-tauri/bundled-plugins/', import.meta.url); const AREA_IDS = ['compose', 'reader', 'profile', 'chat', 'media', 'search', 'notifications', 'navigation']; /** A page each surface is on, to start a surface-bound module. */ const PAGE_OF: Record = { status: '/someone/status/1', profile: '/someone', notifications: '/notifications', search: '/search', explore: '/explore', bookmarks: '/i/bookmarks', chat: '/i/chat', home: '/home', timeline: '/home', viewer: '/someone/status/1/photo/1', composer: '/compose/post', settings: '/settings', }; test('the table is valid: eight areas, fourteen core flags, every flag owned once, no flag renamed', () => { assert.deepEqual(validateTable(X_TABLE), []); assert.deepEqual(X_TABLE.areas.map(area => area.id), AREA_IDS); assert.deepEqual(coreFeatures(X_TABLE).map(feature => feature.id).sort(), [ 'composerRestorePrompt', 'fixChatButton', 'fixHomeTab', 'fixLikeFollow', 'fixMediaUpload', 'fixNewPostsPill', 'fixNotificationBadge', 'fixScrollRestore', 'fixTimelineJump', 'followingChronological', 'logoBrush', 'replaceLogo', 'singleSplitNav', 'twitterWording', ], 'branding 3, split view 1, fixes 10: no plugin switch turns these off'); for (const flag of X_ELSEWHERE) assert.ok(coreFeatures(X_TABLE).some(feature => feature.id === flag), flag); // Flag names never change: every flag that was a Tweak is in the table with the default it had. const defaults = tableDefaults(X_TABLE); for (const [flag, value] of Object.entries(FEATURE_DEFAULTS)) assert.equal(defaults[flag], value, flag); assert.equal(flagFeatures(X_TABLE).filter(feature => feature.id in FEATURE_DEFAULTS).length, 46); // A flag that is new with the area plugins is `.`. for (const feature of X_FEATURES) { if (feature.id in FEATURE_DEFAULTS) continue; assert.ok(feature.area && feature.id.startsWith(`${feature.area}.`), `${feature.id} is named after its area`); } }); test('every area, feature, group and mode has its words in English', () => { for (const key of Object.keys(AREA_MESSAGES)) { assert.ok(!(key in (en.messages as Record)), `${key} is in locales/en.json already: drop it from the area's messages.en.json`); assert.match(key, new RegExp(`^(?:plugin\\.(?:${AREA_IDS.join('|')})-plus\\.|features\\.(?:${AREA_IDS.join('|')})\\.)`), `${key} is under its area's prefix`); } for (const area of X_TABLE.areas) { assert.ok(messages[area.description], area.description); assert.match(area.name, /^[A-Z][a-z]+\+$/, 'a product name, the same in every language'); assert.equal(area.owner, `bt-${area.id}-plus.plugin.js`); } for (const feature of X_FEATURES) { const words = featureWords(feature); assert.ok(messages[words.label], `${feature.id}: ${words.label}`); assert.ok(messages[words.description], `${feature.id}: ${words.description}`); if (feature.group) assert.ok(messages[`features.group.${feature.group}`], `group ${feature.group}`); } for (const mode of X_TABLE.modes) assert.ok(messages[mode.name] && messages[mode.description], mode.id); }); test('every bundled plugin is a feature of an area: still its file, or ported and superseded', () => { const sources = readdirSync(BUNDLED).filter(file => file.endsWith('.plugin.js')).map(file => `bt-${file}`).sort(); const backed = X_FEATURES.filter(feature => feature.backing === 'plugin').map(feature => feature.file!); const superseded = supersededFiles(X_TABLE).map(row => row.file); assert.deepEqual([...backed, ...superseded].sort(), sources); // The migration map and the flags native code knows are generated from the table. assert.equal(readFileSync(AREAS_RS, 'utf8'), renderAreas(), 'src-tauri/src/areas.rs is current (node scripts/build-areas.ts)'); // A ported plugin keeps its words and its page storage, so an upgrade changes nothing for its user. for (const row of supersededFiles(X_TABLE)) { const module = ALL_MODULES.find(item => item.id === row.flag); const name = row.file.replace(/^bt-|\.plugin\.js$/g, ''); assert.ok(module, `${row.flag} has its module`); assert.equal(module!.words, name, `${row.flag} reads the words of ${name}`); const source = readFileSync(new URL(`${name}.plugin.js`, BUNDLED), 'utf8'); const namespace = /\.storage\('([^']+)'\)/.exec(source)?.[1]; if (namespace) assert.equal(module!.storage, namespace, `${row.flag} keeps the storage namespace of ${name}`); } }); test('modules: one per module-backed row; each starts on its page and stops without a trace', () => { const rows = X_FEATURES.filter(feature => feature.backing === 'module').map(feature => feature.id).sort(); assert.deepEqual(ALL_MODULES.map(module => module.id).sort(), rows); // A module-backed row may be on by default: every one is forced off, then one at a time on. const none = Object.fromEntries(rows.map(id => [id, false])); const env = areaPage('/home'); const idle = env.listeners(); const errors: unknown[] = []; const original = console.error; console.error = (...args: unknown[]) => { errors.push(args.join(' ')); }; try { for (const area of X_TABLE.areas) { for (const feature of areaFeatures(X_TABLE, area.id).filter(item => item.backing === 'module')) { env.go(feature.surfaces ? PAGE_OF[feature.surfaces[0]!] ?? '/home' : '/home'); env.registry.setFlags(flagsWith({ ...none, [feature.id]: true })); assert.deepEqual(env.registry.active(), [feature.id], `${feature.id} on`); const commands = env.api.commands.list().filter(command => command.id.startsWith(`${area.owner}:`)); assert.ok(commands.length <= AREA_COMMAND_BUDGET, `${feature.id}: ${commands.length} commands`); env.registry.setFlags(flagsWith(none)); assert.deepEqual(env.registry.active(), [], `${feature.id} off`); assert.deepEqual(env.live(), [], `${feature.id}: no style left`); assert.deepEqual(env.api.commands.list(), [], `${feature.id}: no command left`); assert.equal(env.listeners(), idle, `${feature.id}: no listener left`); assert.equal(env.observers.live, 0, `${feature.id}: no observer left`); } } assert.deepEqual(errors, [], 'no module fails to start'); } finally { console.error = original; } // All of them on at once: still one MutationObserver; an area's switch off stops all it owns. env.go('/someone/status/1'); const all = Object.fromEntries(rows.map(id => [id, true])); env.registry.setFlags(flagsWith(all)); assert.ok(env.observers.live <= 1, `${env.observers.live} observers`); for (const area of X_TABLE.areas) { env.registry.setFlags(flagsWith({ ...all, [`area.${area.id}`]: false })); assert.ok(!env.registry.active().some(id => areaFeatures(X_TABLE, area.id).some(feature => feature.id === id)), `${area.name} off`); } // X's chat is a heavy page: only a feature that asks for it (`heavy`) runs there. env.registry.setFlags(flagsWith(all)); env.go('/i/chat'); for (const id of env.registry.active()) assert.ok(X_FEATURES.find(feature => feature.id === id)!.heavy, `${id} runs on chat pages`); env.registry.dispose(); }); /** On in a setup that never stored a flag (what ran before the area plugins, and still runs there). */ const DEFAULT_ON = [ 'logoBrush', 'twitterWording', 'singleSplitNav', 'fixLikeFollow', 'fixScrollRestore', 'fixTimelineJump', 'fixHomeTab', 'fixNotificationBadge', 'fixNewPostsPill', 'fixMediaUpload', 'followingChronological', 'fixChatButton', 'composerRestorePrompt', 'composerDrafts', 'hidePromotedPosts', 'mediaDownload', 'rememberVideoVolume', 'searchOperators', 'notifications.keywordAlerts', // On by default by the owner's choice (2026-10-10): Chat+ link previews. 'chat.linkPreviews', ]; /** What Advanced adds: the calm features that came with the area plugins (they add something; none hides or changes X). */ const ADVANCED_ADDS = [ 'compose.postingAs', 'compose.lengthMeter', 'reader.readMarker', 'reader.cleanLinks', 'reader.copyAsMarkdown', 'reader.exactTimestamps', 'reader.readingTime', 'reader.threadReader', 'profile.accountAge', 'profile.hoverPeek', 'chat.bookmarks', 'chat.searchLoaded', 'chat.pins', 'media.videoSpeed', 'media.pictureInPicture', 'media.altText', 'search.savedSearches', 'search.advancedSearch', 'notifications.keywordHighlight', ]; /** What Power User adds to Advanced: the rest of each area's POWER list. */ const POWER_ADDS = ['compose.altTextReminder', 'compose.snippets', 'profile.privateLabels', 'chat.personFilter', 'chat.keywords', 'navigation.keyboardPlus']; /** Hides content or changes what X does: on only when the owner turns it on, in no mode. */ const NEVER_BY_MODE = [ 'media.blurMedia', 'notifications.kindFilters', 'notifications.openMentions', 'reader.collapseVerified', 'reader.mutedRegex', 'reader.quickMute', 'reader.languageFilter', 'search.latestDefault', 'compose.replyGuard', 'navigation.focusMode', 'hideEngagementCounts', 'hideViewCounts', 'hideVerifiedBadges', 'hideSidebar', 'fullWidthTimeline', 'hideForYouTab', 'followingTimelineDefault', 'hideGrok', 'hideBookmarkButton', 'disableVideoAutoplay', 'stopVideoLoop', 'sortRepliesRecent', 'hideExploreContent', ]; test('modes: the exact flags of Basic, Advanced and Power User; an existing setup keeps what it ran', () => { const defaults = tableDefaults(X_TABLE); /** The feature flags that are on (the area switches aside), as the runtime applies them. */ const on = (flags: Record) => Object.keys(flags).filter(flag => flags[flag] && !flag.startsWith('area.')).sort(); const running = (mode: string | null) => on(effectiveFlags(X_TABLE, modeFlags(X_TABLE, mode))); const areasOn = (mode: string | null) => X_TABLE.areas.filter(area => modeFlags(X_TABLE, mode)[`area.${area.id}`]).map(area => area.id); assert.deepEqual(X_TABLE.modes.map(mode => [mode.id, !!mode.recommended]), [['basic', false], ['advanced', true], ['power', false]]); // Never chosen (every setup from before the modes, until its owner picks one): the defaults, // which are exactly what ran before. Nothing that is new with the area plugins is a default. assert.deepEqual(on(defaults), [...DEFAULT_ON].sort()); assert.deepEqual(running(null), [...DEFAULT_ON].sort()); for (const [flag, value] of Object.entries(FEATURE_DEFAULTS)) assert.equal(defaults[flag], value, flag); for (const feature of X_FEATURES) if (!(feature.id in FEATURE_DEFAULTS) && feature.id !== 'notifications.keywordAlerts' && feature.id !== 'chat.linkPreviews' && feature.backing !== 'plugin') assert.equal(defaults[feature.id], false, `${feature.id} is new: off by default`); for (const row of supersededFiles(X_TABLE)) assert.equal(defaults[row.flag], false, `${row.flag}: what came from an opt-in plugin stays off`); // Basic: every area off (so everything an area owns is off), no logo or wording; the fixes and split-view navigation stay. assert.deepEqual(areasOn('basic'), []); assert.deepEqual(running('basic'), DEFAULT_ON.filter(flag => flag.startsWith('fix') || ['singleSplitNav', 'followingChronological', 'composerRestorePrompt'].includes(flag)).sort()); // Advanced (recommended on a first run): the defaults and the calm new features. assert.deepEqual(areasOn('advanced'), AREA_IDS); assert.deepEqual(running('advanced'), [...DEFAULT_ON, ...ADVANCED_ADDS].sort()); assert.deepEqual([...X_ADVANCED].sort(), [...ADVANCED_ADDS].sort()); // Power User: Advanced and the rest of each area's POWER list. It only adds. assert.deepEqual(areasOn('power'), AREA_IDS); assert.deepEqual(running('power'), [...DEFAULT_ON, ...ADVANCED_ADDS, ...POWER_ADDS].sort()); assert.deepEqual([...X_POWER].sort(), [...ADVANCED_ADDS, ...POWER_ADDS].sort()); // What hides content or changes X's behaviour is in no mode; with the lists above that is every flag. for (const mode of X_TABLE.modes) for (const flag of NEVER_BY_MODE) assert.equal(modeFlags(X_TABLE, mode.id)[flag], false, `${mode.id}: ${flag}`); const listed = new Set([...DEFAULT_ON, ...ADVANCED_ADDS, ...POWER_ADDS, ...NEVER_BY_MODE]); const rest = flagFeatures(X_TABLE).map(feature => feature.id).filter(flag => !listed.has(flag)); assert.deepEqual(rest.sort(), ['hideChatNav', 'hideCommunitiesNav', 'hideGrokNav', 'hideLiveOnX', 'hidePremiumNav', 'hidePremiumUpsells', 'hideProfileWhoToFollow', 'hideRelevantPeople', 'hideSidebarTrends', 'hideSubscribeButtons', 'hideTodaysNews', 'hideWhoToFollow', 'disableTweetTextFormatting', 'fullNumbers', 'replaceLogo', 'media.gridFocus', 'profile.userNotes'].sort(), 'the remaining opt-in rows (hide rules of the rail and the right column, a few tools): off in every mode'); for (const mode of X_TABLE.modes) for (const flag of rest) assert.equal(modeFlags(X_TABLE, mode.id)[flag], false, `${mode.id}: ${flag}`); // Someone on their own setup who picks a mode and keeps their changes keeps them: the mode's // features join what they had. const mine = { ...defaults, hideSidebar: true, mediaDownload: false }; const kept = switchMode(X_TABLE, mine, null, 'basic', true); assert.deepEqual([kept.hideSidebar, kept.mediaDownload, kept['area.navigation']], [true, false, false]); const advanced = switchMode(X_TABLE, mine, null, 'advanced', true); assert.deepEqual([advanced.hideSidebar, advanced.mediaDownload, advanced['reader.readMarker']], [true, false, true]); }); test('surfaces: the path says which parts of X a page is', () => { const cases: Array<[string, string[]]> = [ ['/home', ['home', 'timeline']], ['/someone', ['profile', 'timeline']], ['/someone/with_replies', ['profile', 'timeline']], ['/someone/status/123', ['status', 'timeline']], ['/someone/status/123/photo/1', ['status', 'viewer', 'timeline']], ['/notifications/mentions', ['notifications']], ['/search', ['search', 'timeline']], ['/explore/tabs/trending', ['explore']], ['/i/bookmarks', ['bookmarks', 'timeline']], ['/messages/123-456', ['chat']], ['/i/chat', ['chat']], ['/compose/post', ['composer']], ['/settings/account', ['settings']], ['/i/lists/42', ['timeline']], ['/i/grok', ['other']], ]; for (const [path, expected] of cases) assert.deepEqual(surfacesOf(path), expected, path); for (const feature of X_FEATURES) for (const surface of feature.surfaces ?? []) assert.ok((X_SURFACES as readonly string[]).includes(surface), `${feature.id}: ${surface}`); });