// 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}`);
});