wayBetter for X
This commit is contained in:
commit
2f1e35ba20
720 changed files with 147465 additions and 0 deletions
223
apps/desktop/tests/areas.test.ts
Normal file
223
apps/desktop/tests/areas.test.ts
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
// 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-<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<string, string> = {
|
||||
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 `<its area>.<feature>`.
|
||||
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<string, string>)), `${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',
|
||||
];
|
||||
/** 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<string, boolean>) => 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.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}`);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue