// Every palette command and keyboard shortcut of the eight area plugins, in one table // (docs/AREA-PLUGINS.md, section 4). A new command or shortcut is added here on purpose: the test // fails on a shortcut two features share, on one the app itself uses, and on an area that could // exceed its palette budget with everything on. import assert from 'node:assert/strict'; import { readdirSync, readFileSync } from 'node:fs'; import test from 'node:test'; import { X_TABLE } from '../src/areas/table.ts'; import { AREA_COMMAND_BUDGET } from '../../../waybetter/packages/runtime/src/featureModules.ts'; import { areaFeatures } from '../../../waybetter/packages/runtime/src/featureTable.ts'; import { parseShortcut } from '../../../waybetter/packages/runtime/src/commands.ts'; import { areaPage, flagsWith } from './helpers/areaPage.ts'; const AREAS = new URL('../src/areas/', import.meta.url); /** `: [shortcut]`, in the order of the table's areas. "settings" is the registry's own entry of an area. */ const COMMANDS: Record> = { compose: [['replyGuard.settings', 'Alt+Shift+R'], ['snippets.insert']], reader: [['mutedRegex.edit', 'Alt+Shift+M'], ['quickMute.add', 'Alt+Shift+U'], ['readMarker.jump'], ['threadReader.open']], profile: [['privateLabels.edit']], chat: [['bookmarks.open'], ['personFilter.pick'], ['pins.edit'], ['searchLoaded.open', 'Ctrl+Shift+F']], media: [['blurMedia.toggle', 'Alt+Shift+B'], ['videoSpeed.faster', 'Alt+Shift+ArrowUp'], ['videoSpeed.slower', 'Alt+Shift+ArrowDown']], search: [['advancedSearch.open'], ['savedSearches.open'], ['savedSearches.pin']], notifications: [], navigation: [['focusMode.toggle', 'Alt+Shift+F']], }; /** Keys outside the palette (a key listener of the module), kept from the plugin the feature was. */ const KEYS: Array<[area: string, file: string, shortcut: string]> = [['reader', 'collapseVerified.ts', 'Alt+Shift+H']]; /** * Keys the app takes before a page sees them (waybetter shell `shortcutFor` / `deckShortcutFor`, * zoom, the app menu, find in page) and the ones X itself uses with a modifier. A feature never * takes one of these: the tab's bridge forwards them to the shell before the page sees the key, so * a feature's shortcut on one of them would simply never fire. (Chat+'s "Find in conversation" is * Ctrl+Shift+F for that reason: Ctrl+F is the app's find bar.) */ const RESERVED = [ 'Ctrl+T', 'Ctrl+Shift+T', 'Ctrl+W', 'Ctrl+Shift+W', 'Ctrl+L', 'Ctrl+Shift+G', 'Ctrl+K', 'Ctrl+R', 'Ctrl+Shift+R', 'Ctrl+\\', 'Ctrl+Shift+\\', 'Alt+ArrowLeft', 'Alt+ArrowRight', 'Ctrl+Shift+D', 'Ctrl+Alt+N', 'Alt+R', 'Alt+U', ...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(digit => `Alt+${digit}`), 'Ctrl+=', 'Ctrl+-', 'Ctrl+0', 'Ctrl+Enter', 'Ctrl+F', 'Alt+F', 'F3', 'F10', ]; const canonical = (shortcut: string) => { const parsed = parseShortcut(shortcut); assert.ok(parsed, `${shortcut} is a shortcut the command registry accepts`); return [parsed.ctrl && 'ctrl', parsed.alt && 'alt', parsed.shift && 'shift', parsed.meta && 'meta', parsed.key].filter(Boolean).join('+'); }; /** What the sources register: every `ctx.command({ id, ..., shortcut? })` of an area's modules. */ function registered(area: string): Array<[string, string?]> { const found: Array<[string, string?]> = []; for (const file of readdirSync(new URL(`${area}/`, AREAS)).filter(name => name.endsWith('.ts')).sort()) { const source = readFileSync(new URL(`${area}/${file}`, AREAS), 'utf8'); for (const match of source.matchAll(/ctx\.command\(\{\s*id: '([^']+)'([\s\S]*?)\brun\b/g)) { const shortcut = /shortcut: '([^']+)'/.exec(match[2]!)?.[1]; found.push(shortcut ? [match[1]!, shortcut] : [match[1]!]); } } return found.sort(([a], [b]) => a.localeCompare(b)); } test('area commands: the table is what the modules register, and a command is named after its feature', () => { assert.deepEqual(Object.keys(COMMANDS), X_TABLE.areas.map(area => area.id)); for (const area of X_TABLE.areas) { assert.deepEqual(registered(area.id), COMMANDS[area.id], area.name); const features = areaFeatures(X_TABLE, area.id).map(feature => feature.id.replace(/^[a-z]+\./, '')); for (const [id] of COMMANDS[area.id]!) assert.ok(features.includes(id.split('.')[0]!), `${area.name}: ${id} is .`); } }); test('area commands: no shortcut is shared by two features, and none is one the app or X uses', () => { const owners = new Map(); const reserved = new Set(RESERVED.map(canonical)); for (const [area, list] of Object.entries(COMMANDS)) { for (const [id, shortcut] of list) { if (!shortcut) continue; const key = canonical(shortcut); assert.ok(!owners.has(key), `${shortcut}: ${area} ${id} and ${owners.get(key)}`); assert.ok(!reserved.has(key), `${shortcut} (${area} ${id}) is taken by the app`); owners.set(key, `${area} ${id}`); } } // A key a module listens for itself counts too, and every such listener is in the list. for (const [area, file, shortcut] of KEYS) { const key = canonical(shortcut); assert.ok(!owners.has(key), `${shortcut}: ${area} ${file} and ${owners.get(key)}`); assert.ok(!reserved.has(key), `${shortcut} (${area} ${file}) is taken by the app`); owners.set(key, `${area} ${file}`); } const listening: string[] = []; for (const area of X_TABLE.areas) { for (const file of readdirSync(new URL(`${area.id}/`, AREAS)).filter(name => name.endsWith('.ts'))) { if (/\.altKey && \w+\.shiftKey/.test(readFileSync(new URL(`${area.id}/${file}`, AREAS), 'utf8'))) listening.push(`${area.id}/${file}`); } } assert.deepEqual(listening, KEYS.map(([area, file]) => `${area}/${file}`), 'a new Alt+Shift key listener belongs in KEYS (or, better, in a command)'); // All of them with a modifier: a bare key would fire while reading. for (const key of owners.keys()) assert.match(key, /^(?:ctrl|alt)\+/, key); }); test(`area commands: no area can exceed ${AREA_COMMAND_BUDGET} palette entries, its settings entry included; in the page they are what the table says`, () => { for (const area of X_TABLE.areas) { const settings = areaFeatures(X_TABLE, area.id).some(feature => feature.settings) ? 1 : 0; assert.ok(COMMANDS[area.id]!.length + settings <= AREA_COMMAND_BUDGET, `${area.name}: ${COMMANDS[area.id]!.length} commands + ${settings}`); } // Every module on, on the pages the areas live on: nothing is refused for the budget, and each // command shows under its area's owner. const all = Object.fromEntries(X_TABLE.features.filter(feature => feature.backing === 'module').map(feature => [feature.id, true])); const env = areaPage('/home'); const errors: string[] = []; const original = console.error; console.error = (...args: unknown[]) => { errors.push(args.join(' ')); }; const seen = new Set(); try { env.registry.setFlags(flagsWith(all)); for (const path of ['/home', '/someone', '/someone/status/1', '/notifications', '/search', '/explore', '/i/bookmarks', '/i/chat', '/compose/post']) { env.go(path); const ids = env.api.commands.list().map(command => command.id); for (const id of ids) seen.add(id); for (const area of X_TABLE.areas) assert.ok(ids.filter(id => id.startsWith(`${area.owner}:`)).length <= AREA_COMMAND_BUDGET, `${area.name} on ${path}`); } } finally { console.error = original; } assert.deepEqual(errors.filter(line => /palette commands per area/.test(line)), []); const expected = X_TABLE.areas.flatMap(area => [ ...COMMANDS[area.id]!.map(([id]) => `${area.owner}:${id}`), ...(areaFeatures(X_TABLE, area.id).some(feature => feature.settings) ? [`${area.owner}:settings`] : []), ]); assert.deepEqual([...seen].sort(), expected.sort()); env.registry.dispose(); });