wayBetter for X

This commit is contained in:
CRTkafa 2026-10-01 06:01:13 +07:00
commit 2f1e35ba20
720 changed files with 147465 additions and 0 deletions

View file

@ -0,0 +1,506 @@
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import test from 'node:test';
const DIR = new URL('../src-tauri/bundled-plugins/', import.meta.url);
const files = readdirSync(DIR).filter(file => file.endsWith('.plugin.js'));
type PluginClass = { new(): { start(): void; stop(): void }; [key: string]: unknown };
/** Loads a plugin the way native code does: CommonJS `module.exports` inside a function scope. */
function load(file: string): PluginClass {
const module = { exports: {} as unknown };
new Function('module', 'exports', readFileSync(new URL(file, DIR), 'utf8'))(module, module.exports);
return module.exports as PluginClass;
}
test('every bundled plugin is a CommonJS class with a complete header and start/stop', () => {
assert.ok(files.length >= 10, `${files.length} plugins`);
for (const file of files) {
const source = readFileSync(new URL(file, DIR), 'utf8');
for (const tag of ['@name', '@description', '@version', '@author']) assert.match(source, new RegExp(`^ \\* ${tag} \\S`, 'm'), `${file} ${tag}`);
const Plugin = load(file);
assert.equal(typeof Plugin, 'function', file);
assert.equal(typeof Plugin.prototype.start, 'function', `${file} start`);
assert.equal(typeof Plugin.prototype.stop, 'function', `${file} stop`);
// First-party plugins never act on the account or make requests of their own.
assert.doesNotMatch(source, /\bfetch\(|XMLHttpRequest|sendBeacon|WebSocket|__TAURI/, `${file} makes no requests`);
assert.doesNotMatch(source, /innerHTML|insertAdjacentHTML|eval\(/, `${file} builds DOM from text only`);
}
});
test('Muted Regex parses plain, literal and invalid patterns', () => {
const MutedRegex = load('muted-regex.plugin.js') as unknown as { parsePatterns(s: string): { patterns: { source: string; pattern: RegExp }[]; invalid: string[] } };
const { patterns, invalid } = MutedRegex.parsePatterns('# comment\n\ncrypto\n/^@spam_bot$/m\n/Exact/g\n(unclosed\n');
assert.deepEqual(patterns.map(p => p.source), ['crypto', '/^@spam_bot$/m', '/Exact/g']);
assert.deepEqual(invalid, ['(unclosed']);
const [plain, literal, exact] = patterns.map(p => p.pattern);
assert.ok(plain!.test('Buy CRYPTO now'), 'plain lines are case-insensitive');
assert.ok(literal!.test('@someone\n@spam_bot\ntext'));
assert.ok(!exact!.test('exact'), 'literal flags are respected');
assert.ok(exact!.test('Exact') && exact!.test('Exact'), 'g is dropped so .test() is not stateful');
});
test('Reading Time only labels long text', () => {
const ReadingTime = load('reading-time.plugin.js') as unknown as { minutes(s: string): number };
assert.equal(ReadingTime.minutes('short post'), 0);
assert.equal(ReadingTime.minutes('word '.repeat(119)), 0);
assert.equal(ReadingTime.minutes('word '.repeat(120)), 1);
assert.equal(ReadingTime.minutes('word '.repeat(1150)), 5);
assert.equal(ReadingTime.minutes('字'.repeat(460)), 2, 'CJK characters count individually');
});
test('Hide Blue Checks decides purely from settings and observed attributes, never on unknowns', () => {
const HideBlueChecks = load('hide-blue-checks.plugin.js') as unknown as {
DEFAULTS: Record<string, unknown>;
shouldHide(settings: Record<string, unknown>, ctx: Record<string, unknown>): boolean;
parseAllowlist(source: string): string[];
};
const ctx = (over: Record<string, unknown> = {}) => ({ handle: 'someone', blue: true, verifiedType: 'None', following: false, viewer: 'me', ...over });
const settings = (over: Record<string, unknown> = {}) => ({ ...HideBlueChecks.DEFAULTS, ...over });
assert.ok(HideBlueChecks.shouldHide(settings(), ctx()), 'default settings hide a blue-check post');
assert.ok(!HideBlueChecks.shouldHide(settings(), ctx({ viewer: 'SomeOne' })), 'never hides the signed-in account');
assert.ok(!HideBlueChecks.shouldHide(settings(), ctx({ viewer: null })), 'hides nothing while the signed-in account is unknown');
assert.ok(!HideBlueChecks.shouldHide(settings({ keepFollowed: false }), ctx({ viewer: null })), 'unknown viewer wins over every setting');
assert.ok(!HideBlueChecks.shouldHide(settings(), ctx({ following: null })), 'keepFollowed on: unknown follow status is not hidden');
assert.ok(HideBlueChecks.shouldHide(settings({ keepFollowed: false }), ctx({ following: null })), 'keepFollowed off: follow status does not matter');
assert.ok(!HideBlueChecks.shouldHide(settings({ allowlist: ['Someone'] }), ctx()), 'allowlisted handle is kept (case-insensitive)');
assert.ok(HideBlueChecks.shouldHide(settings({ allowlist: ['other'] }), ctx()), 'allowlist entries for other handles do not protect this one');
assert.ok(!HideBlueChecks.shouldHide(settings(), ctx({ following: true })), 'followed accounts are kept by default');
assert.ok(HideBlueChecks.shouldHide(settings({ keepFollowed: false }), ctx({ following: true })), 'keepFollowed off hides a followed blue account');
assert.ok(!HideBlueChecks.shouldHide(settings(), ctx({ blue: null })), 'unknown blue status is never hidden');
assert.ok(!HideBlueChecks.shouldHide(settings(), ctx({ blue: false, verifiedType: 'Business' })), 'Business is kept unless hideBusiness is on');
assert.ok(HideBlueChecks.shouldHide(settings({ hideBusiness: true }), ctx({ blue: false, verifiedType: 'Business' })), 'hideBusiness hides a Business account');
assert.ok(HideBlueChecks.shouldHide(settings({ hideGovernment: true }), ctx({ blue: false, verifiedType: 'Government' })), 'hideGovernment hides a Government account');
assert.ok(!HideBlueChecks.shouldHide(settings({ hideBlue: false }), ctx()), 'hideBlue off keeps a blue account');
assert.deepEqual(HideBlueChecks.parseAllowlist('@Jack\n\ntoo-many-symbols!\njack\n bad handle \n'), ['jack'], 'lowercased, deduped, invalid handles dropped');
});
test('Hide Blue Checks re-checks unknown posts once per frame when their author is observed, and drops orphaned stubs', () => {
const HideBlueChecks = load('hide-blue-checks.plugin.js') as unknown as {
new(): Record<string, any>;
DEFAULTS: Record<string, unknown>;
isUnknown(settings: Record<string, unknown>, user: Record<string, unknown> | null): boolean;
};
const settings = HideBlueChecks.DEFAULTS;
const known = { blue: true, verifiedType: 'None', following: false };
assert.ok(HideBlueChecks.isUnknown(settings, null), 'never seen');
assert.ok(HideBlueChecks.isUnknown(settings, { ...known, following: null }), 'follow status pending while keepFollowed is on');
assert.ok(!HideBlueChecks.isUnknown({ ...settings, keepFollowed: false }, { ...known, following: null }), 'follow status irrelevant with keepFollowed off');
assert.ok(!HideBlueChecks.isUnknown(settings, known));
const frames: (() => void)[] = [];
const g = globalThis as Record<string, unknown>;
const saved = g.requestAnimationFrame;
g.requestAnimationFrame = (fn: () => void) => { frames.push(fn); return frames.length; };
try {
const plugin = new HideBlueChecks();
const checked: string[] = [];
const article = (name: string, connected = true) => ({ name, isConnected: connected });
const a = article('a'), b = article('b'), gone = article('gone', false), anyUpdate = article('any');
plugin.pending = new Map<unknown, string[]>([[a, ['alice']], [b, ['bob']], [gone, ['alice']], [anyUpdate, []]]);
plugin.dirty = new Set();
plugin.recheckFrame = 0;
plugin.check = (el: { name: string }) => { checked.push(el.name); };
plugin.onUsersUpdate(['alice']);
plugin.onUsersUpdate(['carol']);
assert.equal(frames.length, 1, 'updates are coalesced into one frame');
assert.deepEqual(checked, [], 'nothing re-checked synchronously');
frames[0]!();
assert.deepEqual(checked.sort(), ['a', 'any'], 'only posts depending on an updated handle (or on any update) are re-checked');
assert.ok(plugin.pending.has(b) && !plugin.pending.has(gone), 'unrelated posts stay pending; detached ones are dropped');
const removed: string[] = [];
const stub = (name: string) => ({ isConnected: true, remove() { removed.push(name); } });
plugin.stubs = new Map([['1', { article: article('live'), stub: stub('s1') }], ['2', { article: article('dead', false), stub: stub('s2') }]]);
plugin.sweepStubs();
assert.deepEqual(removed, ['s2'], 'a stub whose post left the page is removed');
assert.deepEqual([...plugin.stubs.keys()], ['1']);
} finally {
g.requestAnimationFrame = saved;
}
});
test('Media Grid Focus is scoped to a profile media tab', () => {
const MediaGridFocus = load('media-grid-focus.plugin.js') as unknown as { isMediaTab(p: string): boolean };
assert.ok(MediaGridFocus.isMediaTab('/jack/media'));
assert.ok(MediaGridFocus.isMediaTab('/jack/media/'));
for (const path of ['/jack', '/jack/media/extra', '/i/bookmarks', '/search', '/jack/status/1/media']) assert.ok(!MediaGridFocus.isMediaTab(path), path);
});
type EmojiEntry = { emoji: string; names: string[]; tags: string[]; cat: number; skin: boolean };
type EmojiPickerClass = {
entries(): EmojiEntry[];
parseTrigger(text: string): { start: number; query: string } | null;
parseExact(text: string): { start: number; end: number; code: string } | null;
findExact(code: string): EmojiEntry | null;
search(query: string, limit?: number): EmojiEntry[];
withSkinTone(emoji: string, tone: number): string;
replaceLength(text: string, start: number | null): number | null;
};
test('Emoji Picker: shortcode dataset is complete and reasonably small', () => {
const source = readFileSync(new URL('emoji-picker.plugin.js', DIR), 'utf8');
assert.ok(Buffer.byteLength(source) < 200_000, `${Buffer.byteLength(source)} bytes`);
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
const entries = EmojiPicker.entries();
assert.ok(entries.length > 1000, `${entries.length} emoji`);
assert.ok(entries.some(e => e.emoji === '🔥' && e.names[0] === 'fire'));
});
test('Emoji Picker: parseTrigger opens on 2+ chars after a boundary colon, not mid-word', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
assert.deepEqual(EmojiPicker.parseTrigger('hello :fir'), { start: 6, query: 'fir' });
assert.deepEqual(EmojiPicker.parseTrigger('(:ok'), { start: 1, query: 'ok' });
assert.equal(EmojiPicker.parseTrigger('hello :f'), null, 'needs 2+ chars');
assert.equal(EmojiPicker.parseTrigger('abc:de'), null, 'no boundary before the colon');
assert.equal(EmojiPicker.parseTrigger('hello :fire '), null, 'trailing space closes the query');
});
test('Emoji Picker: parseExact fires only on a just-closed :shortcode:', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
assert.deepEqual(EmojiPicker.parseExact('hello :fire:'), { start: 6, end: 12, code: 'fire' });
assert.equal(EmojiPicker.parseExact('hello :fire'), null, 'not closed yet');
assert.equal(EmojiPicker.parseExact('http://x'), null);
});
test('Emoji Picker: findExact matches any alias case-insensitively', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
assert.equal(EmojiPicker.findExact('fire')!.emoji, '🔥');
assert.equal(EmojiPicker.findExact('THUMBSUP')!.emoji, '👍');
assert.equal(EmojiPicker.findExact('+1')!.emoji, '👍');
assert.equal(EmojiPicker.findExact('not-a-real-shortcode'), null);
});
test('Emoji Picker: search ranks exact and prefix shortcode matches above tag matches', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
const fire = EmojiPicker.search('fire', 5);
assert.equal(fire[0]!.names[0], 'fire', 'exact shortcode wins');
assert.ok(fire.every(e => e.names.some(n => n.startsWith('fire')) || e.tags.includes('fire')));
assert.equal(EmojiPicker.search('', 5).length, 0, 'empty query has no matches');
assert.equal(EmojiPicker.search('not-a-real-shortcode', 5).length, 0);
});
test('Emoji Picker: withSkinTone inserts the Fitzpatrick modifier right after the base character', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
assert.equal(EmojiPicker.withSkinTone('👍', 0), '👍', 'tone 0 is unchanged');
assert.equal(EmojiPicker.withSkinTone('👍', 3), '👍🏽');
assert.equal([...EmojiPicker.withSkinTone('👍', 5)].length, 2, 'base + modifier, two code points');
});
test('Emoji Picker: replaceLength re-reads the trigger at the caret and refuses a moved caret', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
assert.equal(EmojiPicker.replaceLength('hello :fire', 6), 5, 'replaces ":fire"');
assert.equal(EmojiPicker.replaceLength('hello :fi', 6), 3, 'still typing');
assert.equal(EmojiPicker.replaceLength('hello :f', 6), null, 'caret moved left out of a 2+ char trigger');
assert.equal(EmojiPicker.replaceLength('hel', 6), null, 'caret moved before the trigger');
assert.equal(EmojiPicker.replaceLength('other :fire', 6), 5, 'same start');
assert.equal(EmojiPicker.replaceLength('x :fire', 6), null, 'a different trigger');
});
test('Emoji Picker: a skin tone replaces the VS16 after the base character', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
assert.equal(EmojiPicker.withSkinTone('✌️', 1), '✌\u{1F3FB}');
assert.equal(EmojiPicker.withSkinTone('\u{1F575}️‍♂️', 2), '\u{1F575}\u{1F3FC}‍♂️');
});
type Pos = { section: number; index: number };
type Rect = { left: number; right: number; top: number; bottom: number };
type LayoutRow = { kind: 'header' | 'row'; section: number; top: number; height: number; start?: number; end?: number; index?: number };
type EmojiPickerLayout = EmojiPickerClass & {
layoutRows(counts: number[], cols?: number, rowHeight?: number, headerHeight?: number): { rows: LayoutRow[]; sectionTops: number[]; sectionFirstRow: number[]; height: number; gridRows: number };
visibleRange(rows: LayoutRow[], scrollTop: number, viewport: number, overscan?: number): { start: number; end: number };
sectionAt(sectionTops: number[], scrollTop: number): number;
moveInGrid(counts: number[], pos: Pos | null, key: string, cols?: number, ctrl?: boolean): Pos | null;
placeCard(anchor: Rect, avoid: Rect | null, width: number, height: number, viewWidth: number, viewHeight: number, minHeight?: number): { left: number; top: number; width: number; height: number; side: string };
regionCode(emoji: string): string | null;
displayName(entry: EmojiEntry): string;
};
test('Emoji Picker: layoutRows lays sections out as a header plus rows of 8', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerLayout;
const layout = EmojiPicker.layoutRows([10, 0, 8], 8, 36, 28);
assert.deepEqual(layout.rows.map(r => r.kind), ['header', 'row', 'row', 'header', 'header', 'row']);
assert.deepEqual(layout.sectionTops, [0, 100, 128]);
assert.deepEqual(layout.sectionFirstRow, [1, 4, 5]);
assert.equal(layout.height, 28 + 72 + 28 + 28 + 36);
assert.equal(layout.gridRows, 3, 'only emoji rows count as grid rows');
assert.deepEqual([layout.rows[2]!.start, layout.rows[2]!.end, layout.rows[2]!.index], [8, 10, 1]);
// The real dataset: ~1850 emoji become a few hundred rows.
const full = EmojiPicker.layoutRows(Array.from({ length: 9 }, (_, i) => EmojiPicker.entries().filter(e => e.cat === i).length));
assert.ok(full.gridRows > 200 && full.gridRows < 260, `${full.gridRows} rows`);
});
test('Emoji Picker: visibleRange windows the rows in view (binary search), with overscan', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerLayout;
const { rows } = EmojiPicker.layoutRows(Array.from({ length: 9 }, () => 200));
const top = EmojiPicker.visibleRange(rows, 0, 300);
assert.equal(top.start, 0);
assert.ok(top.end - top.start <= 10, `${top.end - top.start} rows for a 300 px viewport`);
const mid = EmojiPicker.visibleRange(rows, 5000, 300, 0);
assert.ok(rows[mid.start]!.top <= 5000 && rows[mid.start]!.top + rows[mid.start]!.height > 5000, 'first row covers scrollTop');
assert.ok(rows[mid.end - 1]!.top < 5300 && (mid.end === rows.length || rows[mid.end]!.top >= 5300), 'last row reaches the bottom edge');
const over = EmojiPicker.visibleRange(rows, 5000, 300, 160);
assert.ok(over.start < mid.start && over.end > mid.end, 'overscan renders a few rows either side');
const end = EmojiPicker.visibleRange(rows, 1e9, 300);
assert.equal(end.start, rows.length, 'past the end: nothing');
});
test('Emoji Picker: sectionAt names the section under the sticky header', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerLayout;
assert.equal(EmojiPicker.sectionAt([0, 100, 400], 0), 0);
assert.equal(EmojiPicker.sectionAt([0, 100, 400], 99), 0);
assert.equal(EmojiPicker.sectionAt([0, 100, 400], 100), 1);
assert.equal(EmojiPicker.sectionAt([0, 100, 400], 9999), 2);
});
test('Emoji Picker: moveInGrid moves in 2D and flows across sections keeping the column', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerLayout;
const counts = [10, 0, 20, 3];
const at = (section: number, index: number) => ({ section, index });
const move = (pos: Pos | null, key: string, ctrl = false) => EmojiPicker.moveInGrid(counts, pos, key, 8, ctrl);
assert.deepEqual(move(null, 'ArrowDown'), at(0, 0), 'no selection: the first cell');
assert.deepEqual(move(at(0, 2), 'ArrowRight'), at(0, 3));
assert.deepEqual(move(at(0, 9), 'ArrowRight'), at(2, 0), 'right from the last cell: next non-empty section');
assert.deepEqual(move(at(2, 0), 'ArrowLeft'), at(0, 9), 'left from the first cell: previous section, last cell');
assert.deepEqual(move(at(0, 0), 'ArrowLeft'), at(0, 0), 'nowhere to go: stays');
assert.deepEqual(move(at(0, 1), 'ArrowDown'), at(0, 9), 'down into a short last row lands on its last cell');
assert.deepEqual(move(at(0, 3), 'ArrowDown'), at(0, 9), 'down from col 3 of row 1 (last row has 2): last cell');
assert.deepEqual(move(at(0, 9), 'ArrowDown'), at(2, 1), 'down from the last row: next section, same column');
assert.deepEqual(move(at(2, 5), 'ArrowUp'), at(0, 9), 'up from the first row: previous section last row, column clamped');
assert.deepEqual(move(at(2, 0), 'ArrowUp'), at(0, 8));
assert.deepEqual(move(at(2, 17), 'ArrowDown'), at(3, 1), 'column clamped to a short section');
assert.deepEqual(move(at(2, 12), 'ArrowUp'), at(2, 4));
assert.deepEqual(move(at(0, 4), 'PageDown'), at(2, 4), 'PageDown: next section, same column');
assert.deepEqual(move(at(2, 12), 'PageUp'), at(2, 4), 'PageUp inside a section: its first row');
assert.deepEqual(move(at(2, 4), 'PageUp'), at(0, 4), 'PageUp from a first row: previous section');
assert.deepEqual(move(at(2, 12), 'Home'), at(2, 8));
assert.deepEqual(move(at(2, 12), 'End'), at(2, 15));
assert.deepEqual(move(at(2, 17), 'End'), at(2, 19), 'End on a short row: its last cell');
assert.deepEqual(move(at(2, 12), 'Home', true), at(0, 0), 'Ctrl+Home: first cell');
assert.deepEqual(move(at(0, 3), 'End', true), at(3, 2), 'Ctrl+End: last cell');
assert.equal(EmojiPicker.moveInGrid([0, 0], null, 'ArrowDown'), null, 'no cells: null');
});
test('Emoji Picker: placeCard flips above, clamps into narrow panes and never covers the text', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerLayout;
const rect = (left: number, top: number, w: number, h: number) => ({ left, right: left + w, top, bottom: top + h });
const editorTop = rect(60, 60, 500, 60);
const buttonTop = rect(560, 140, 34, 34);
const below = EmojiPicker.placeCard(buttonTop, editorTop, 320, 440, 1180, 760, 260);
assert.equal(below.side, 'below');
assert.equal(below.top, 174 + 8);
assert.equal(below.left, 560);
// A reply box at the bottom of the pane: above the editor, not over it.
const editorBottom = rect(60, 630, 500, 60);
const above = EmojiPicker.placeCard(rect(560, 710, 34, 34), editorBottom, 320, 440, 1180, 760, 260);
assert.equal(above.side, 'above');
assert.ok(above.top + above.height <= 630 - 8, 'bottom edge above the editor');
// A 420 px pane: clamped inside with the margin.
const narrow = EmojiPicker.placeCard(rect(290, 140, 34, 34), editorTop, 320, 440, 420, 760, 260);
assert.equal(narrow.left, 420 - 8 - 320);
// Narrower than the card: the card shrinks to fit.
const tiny = EmojiPicker.placeCard(rect(10, 140, 34, 34), editorTop, 320, 440, 300, 760, 260);
assert.deepEqual([tiny.left, tiny.width], [8, 284]);
// Not enough room either side for the full card: the roomier side, shrunk.
const short = EmojiPicker.placeCard(rect(100, 300, 34, 34), rect(60, 260, 500, 40), 320, 440, 1180, 640, 260);
assert.equal(short.side, 'below');
assert.equal(short.height, 640 - 8 - (334 + 8));
// A pane too small for even the minimum: overlaps, but stays inside.
const over = EmojiPicker.placeCard(rect(100, 150, 34, 34), rect(60, 100, 500, 60), 320, 440, 1180, 300, 260);
assert.equal(over.side, 'over');
assert.ok(over.top >= 8 && over.top + over.height <= 292);
// An anchor scrolled out of view never puts the card off screen.
const gone = EmojiPicker.placeCard(rect(100, -400, 34, 34), rect(60, -480, 500, 60), 320, 440, 1180, 760, 260);
assert.ok(gone.top >= 8 && gone.top + gone.height <= 752, JSON.stringify(gone));
});
test('Emoji Picker: flag chips and readable names', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerLayout;
assert.equal(EmojiPicker.regionCode('🇺🇸'), 'US');
assert.equal(EmojiPicker.regionCode(EmojiPicker.findExact('england')!.emoji), 'ENG');
assert.equal(EmojiPicker.regionCode('🏁'), null);
assert.equal(EmojiPicker.regionCode('🔥'), null);
assert.equal(EmojiPicker.displayName(EmojiPicker.findExact('heart_eyes')!), 'Heart eyes');
assert.equal(EmojiPicker.displayName(EmojiPicker.findExact('+1')!), 'Thumbsup', 'first name with letters');
assert.equal(EmojiPicker.displayName(EmojiPicker.findExact('us')!), 'Flag: US');
});
type EmojiPickerV3 = EmojiPickerLayout & {
columnsFor(width: number, cell?: number, min?: number): number;
isXEmojiButton(el: unknown): boolean;
};
test('Emoji Picker 3: the 432 px card fits 9 columns of 40 px; narrow panes lose columns, not the layout', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerV3;
const source = readFileSync(new URL('emoji-picker.plugin.js', DIR), 'utf8');
assert.match(source, /^ \* @version 3\.\d+\.\d+$/m);
// Grid width = card 432 - 2 border - 48 rail - 8 scrollbar = 374.
assert.equal(EmojiPicker.columnsFor(374), 9);
assert.equal(EmojiPicker.columnsFor(404 - 58), 8, 'a 420 px pane: 404 px card');
assert.equal(EmojiPicker.columnsFor(344 - 58), 6, 'a 360 px pane: 344 px card');
assert.equal(EmojiPicker.columnsFor(40), 5, 'never fewer than 5');
// The full dataset in 9 columns stays a few hundred rows.
const full = EmojiPicker.layoutRows(Array.from({ length: 9 }, (_, i) => EmojiPicker.entries().filter(e => e.cat === i).length), 9, 40, 32);
assert.ok(full.gridRows > 180 && full.gridRows < 240, `${full.gridRows} rows`);
assert.equal(full.rows[1]!.top, 32, 'first emoji row under a 32 px header');
});
test("Emoji Picker 3: X's emoji button is recognised by its label in X's toolbar, and nothing else is", () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerV3;
const el = (label: string, opts: { tag?: string; role?: string; inToolbar?: boolean; ours?: boolean } = {}) => ({
nodeType: 1,
tagName: opts.tag ?? 'BUTTON',
classList: { contains: (name: string) => !!opts.ours && name === 'bt-ep-button' },
getAttribute: (name: string) => (name === 'aria-label' ? label : name === 'role' ? opts.role ?? 'button' : null),
closest: (selector: string) => (selector === '[data-testid="toolBar"]' && opts.inToolbar !== false ? {} : null),
});
assert.equal(EmojiPicker.isXEmojiButton(el('Add emoji')), true);
assert.equal(EmojiPicker.isXEmojiButton(el('Emoji ekle')), true, 'Turkish');
assert.equal(EmojiPicker.isXEmojiButton(el('絵文字を追加')), true, 'Japanese');
assert.equal(EmojiPicker.isXEmojiButton(el('Add emoji', { tag: 'DIV', role: 'button' })), true, 'a role=button div');
assert.equal(EmojiPicker.isXEmojiButton(el('Add poll')), false);
assert.equal(EmojiPicker.isXEmojiButton(el('Add emoji', { inToolbar: false })), false, 'outside a composer toolbar');
assert.equal(EmojiPicker.isXEmojiButton(el('Emoji picker', { ours: true })), false, 'our own button');
assert.equal(EmojiPicker.isXEmojiButton(el('Add emoji', { tag: 'DIV', role: 'presentation' })), false, 'not a button');
assert.equal(EmojiPicker.isXEmojiButton(null), false);
});
test('Emoji Picker: search finds a word inside a shortcode below prefix matches', () => {
const EmojiPicker = load('emoji-picker.plugin.js') as unknown as EmojiPickerClass;
const names = EmojiPicker.search('eyes').map(e => e.names[0]);
assert.equal(names[0], 'eyes');
assert.ok(names.includes('heart_eyes'), 'word inside a shortcode');
assert.ok(names.indexOf('heart_eyes') > names.indexOf('eyes'));
});
test('Focus Mode: settings normalize, CSS hides only what is chosen, off means no CSS', () => {
const FocusMode = load('focus-mode.plugin.js') as unknown as {
DEFAULTS: Record<string, boolean>;
normalize(saved: unknown): Record<string, boolean>;
css(settings: Record<string, boolean>): string;
};
assert.deepEqual(FocusMode.normalize(null), FocusMode.DEFAULTS);
assert.deepEqual(FocusMode.normalize({ metrics: false, trends: 'yes', extra: true }), { ...FocusMode.DEFAULTS, metrics: false }, 'non-booleans and unknown keys are ignored');
const all = (over: Record<string, boolean>) => ({ enabled: true, metrics: false, trends: false, whoToFollow: false, followCounts: false, sidebar: false, ...over });
assert.equal(FocusMode.css({ ...FocusMode.DEFAULTS, enabled: false }), '', 'off: nothing hidden');
assert.equal(FocusMode.css(all({})), '', 'nothing chosen: nothing hidden');
assert.match(FocusMode.css(all({ metrics: true })), /\[role="group"\] \[data-testid="app-text-transition-container"\]/);
assert.doesNotMatch(FocusMode.css(all({ metrics: true })), /sidebarColumn/);
assert.match(FocusMode.css(all({ trends: true })), /sidebarColumn"\] section\[role="region"\]:has\(\[data-testid="trend"\]\)/);
assert.match(FocusMode.css(all({ whoToFollow: true })), /aside\[role="complementary"\]:has\(\[data-testid="UserCell"\]\)/);
const sidebar = FocusMode.css(all({ sidebar: true, trends: true, whoToFollow: true }));
assert.match(sidebar, /\[data-testid="sidebarColumn"\] \{ visibility: hidden/);
assert.doesNotMatch(sidebar, /trend|UserCell/, 'the whole column replaces the finer rules');
const counts = FocusMode.css(all({ followCounts: true }));
assert.match(counts, /a\[href\$="\/followers"\]:not\(\[href="\/followers"\]\):not\(\[role="tab"\]\) > span:first-child/, 'follower links only, not @followers mentions or tabs');
for (const css of [FocusMode.css(all({ metrics: true, trends: true, whoToFollow: true, followCounts: true }))]) {
assert.doesNotMatch(css, /display: none[^}]*article|\bmain\b/, 'posts themselves are never hidden');
}
});
test('Reply Guard: asks only for old posts or known strangers, never on unknowns or your own posts', () => {
const ReplyGuard = load('reply-guard.plugin.js') as unknown as {
DEFAULTS: { oldDays: number; strangers: boolean };
normalize(saved: unknown): { oldDays: number; strangers: boolean };
formatAge(days: number): string;
reason(settings: { oldDays: number; strangers: boolean }, ctx: Record<string, unknown>): string | null;
};
const now = Date.parse('2026-09-28T12:00:00Z');
const daysAgo = (days: number) => new Date(now - days * 86_400_000).toISOString();
const ctx = (over: Record<string, unknown> = {}) => ({ time: daysAgo(1), now, handle: 'someone', following: null, own: false, ...over });
const settings = ReplyGuard.DEFAULTS;
assert.equal(ReplyGuard.reason(settings, ctx()), null, 'a fresh post is not guarded');
assert.equal(ReplyGuard.reason(settings, ctx({ time: daysAgo(30) })), 'This post is 30 days old.');
assert.equal(ReplyGuard.reason(settings, ctx({ time: daysAgo(800) })), 'This post is 2 years old.');
assert.equal(ReplyGuard.reason(settings, ctx({ time: daysAgo(800), own: true })), null, 'your own posts never ask');
assert.equal(ReplyGuard.reason(settings, ctx({ time: null })), null, 'unknown date never asks');
assert.equal(ReplyGuard.reason({ ...settings, oldDays: 0 }, ctx({ time: daysAgo(800) })), null, '0 days turns the age check off');
assert.equal(ReplyGuard.reason(settings, ctx({ following: false })), null, 'strangers check is off by default');
const strict = { ...settings, strangers: true };
assert.equal(ReplyGuard.reason(strict, ctx({ following: false })), "You don't follow @someone.");
assert.equal(ReplyGuard.reason(strict, ctx({ following: null })), null, 'unknown follow status never asks');
assert.equal(ReplyGuard.reason(strict, ctx({ following: true })), null);
assert.equal(ReplyGuard.reason(strict, ctx({ following: false, time: daysAgo(90) })), "This post is 3 months old. You don't follow @someone.");
assert.equal(ReplyGuard.formatAge(1.5), '1 day');
assert.equal(ReplyGuard.formatAge(59), '59 days');
assert.equal(ReplyGuard.formatAge(61), '2 months');
assert.equal(ReplyGuard.formatAge(365), '1 year');
assert.deepEqual(ReplyGuard.normalize({ oldDays: '7', strangers: true }), { oldDays: 7, strangers: true });
assert.deepEqual(ReplyGuard.normalize({ oldDays: -3 }), { oldDays: 0, strangers: false });
assert.deepEqual(ReplyGuard.normalize({ oldDays: 'soon' }), ReplyGuard.DEFAULTS);
assert.deepEqual(ReplyGuard.normalize({ oldDays: 99999 }), { oldDays: 3650, strangers: false });
});
test('Focus Mode and Reply Guard: stop() removes their CSS, listeners and dialogs', () => {
const g = globalThis as Record<string, unknown>;
const saved = { window: g.window, document: g.document };
const css = new Map<string, string>();
const listeners = new Set<string>();
let openDialogs = 0;
const fakeApi = {
storage: () => ({ get: (_k: string, fallback: unknown) => fallback, set: () => true, remove() {}, keys: () => [] }),
injectCSS: (id: string, text: string) => { css.set(id, text); },
clearCSS: (id: string) => { css.delete(id); },
isTypingTarget: () => false,
scope() {
const disposers: (() => void)[] = [];
return {
on(target: { name: string }, type: string) { const key = `${target.name}:${type}:${Math.random()}`; listeners.add(key); disposers.push(() => listeners.delete(key)); },
css(id: string, text: string) { css.set(id, text); disposers.push(() => css.delete(id)); },
defer(fn: () => void) { disposers.push(fn); },
observe(selector: string) { const key = `observe:${selector}:${Math.random()}`; listeners.add(key); disposers.push(() => listeners.delete(key)); },
ui: { dialog: () => { openDialogs += 1; disposers.push(() => { openDialogs -= 1; }); return { open: true }; }, toast() { return { close() {} }; }, confirm: async () => false, h() { return {}; } },
dispose() { for (const fn of disposers.splice(0).reverse()) fn(); },
};
},
};
g.window = { name: 'window', wayBetter: { api: fakeApi } };
g.document = { name: 'document', querySelectorAll: () => [] };
try {
for (const file of ['focus-mode.plugin.js', 'reply-guard.plugin.js']) {
const Plugin = load(file) as unknown as new () => { start(): void; stop(): void; openSettings(): void };
const plugin = new Plugin();
plugin.start();
plugin.openSettings();
assert.ok(listeners.size > 0, `${file} listens`);
assert.equal(openDialogs, 1, `${file} settings dialog opened through scope.ui`);
plugin.stop();
assert.equal(listeners.size, 0, `${file} removed its listeners`);
assert.equal(css.size, 0, `${file} removed its CSS`);
assert.equal(openDialogs, 0, `${file} dialogs closed by the scope`);
}
} finally {
g.window = saved.window;
g.document = saved.document;
}
});
test('Thread Reader: the button belongs to plain post pages, not the photo/video viewer or lists', () => {
const ThreadReader = load('thread-reader.plugin.js') as unknown as { focalIdOf(pathname: string): string | null };
assert.equal(ThreadReader.focalIdOf('/jack/status/123'), '123');
assert.equal(ThreadReader.focalIdOf('/jack/status/123/'), '123');
for (const path of ['/jack/status/123/photo/1', '/jack/status/123/video/1', '/jack/status/123/quotes', '/home', '/jack']) {
assert.equal(ThreadReader.focalIdOf(path), null, path);
}
});
test('every bundled plugin creates its scope before start() first uses it', () => {
// i18n regression (live): Copy as Markdown, Hide Blue Checks and User Notes called
// `this.scope.defer(...)` before `this.scope = api.scope()`, so start() threw in the app and
// the three plugins never ran. Their unit tests never called start().
for (const file of files) {
const source = readFileSync(new URL(file, DIR), 'utf8');
const start = source.indexOf(' start() {');
if (start < 0) continue;
const end = source.indexOf('\n }', start);
const body = source.slice(start, end);
const use = body.search(/this\.scope\.(?!\s*=)/);
const made = body.indexOf('this.scope = ');
if (use >= 0) assert.ok(made >= 0 && made < use, `${file}: this.scope used before it is created in start()`);
}
});