// The "Make" section's examples are what people copy into their plugins folder, so each one is run // here through a full lifecycle against the real X plugin API surface (members of createPluginApi, // its scope and UI kit), its header is read with the plugin store's rules, the theme example goes // through the app's own importer and the schema, and the bundled docs parse with working links. // The section itself, the header rules and the Markdown reader are the framework's // (waybetter/packages/shell/src/panel, tested in waybetter/packages/shell/tests/panel.test.ts). import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; import vm from 'node:vm'; import { themeFromColors, themeVars } from '../../../waybetter/packages/runtime/src/themeApi.ts'; import { createPluginApi } from '../src/inject/pluginApi.ts'; import { parseThemeJson } from '../src/inject/themeSpec.ts'; import { anchorOf, inlineTokens, parseMarkdown, resolveLink } from '../../../waybetter/packages/shell/src/panel/markdown.ts'; import { KIT_DEMOS, cssVarOf, parsePluginHeader, pluginFileFor, pluginWarnings, validPluginFileName } from '../../../waybetter/packages/shell/src/panel/makeModel.ts'; import { PLUGIN_EXAMPLES, THEME_EXAMPLE } from '../src/ui/makeExamples.ts'; // @ts-expect-error: a plain .mjs helper of the framework's create-waybetter tests (no types). import { validate } from '../../../waybetter/packages/create-waybetter/tests/jsonSchema.mjs'; const fakeWindow = { document: { documentElement: {} }, location: { href: 'https://x.com/home' } } as unknown as Window & typeof globalThis; const real = createPluginApi(fakeWindow); const API_KEYS = new Set(Object.keys(real)); const SCOPE_KEYS = new Set(Object.keys(real.scope())); const UI_KEYS = new Set(Object.keys(real.ui)); type Any = Record; /** Throws on any member the real API does not have (a typo in an example fails here). */ function strict(label: string, keys: Set, impl: T): T { return new Proxy(impl, { get(target, key) { if (typeof key === 'symbol' || key === 'then') return (target as Any)[key as string]; if (!keys.has(key)) throw new Error(`${label}.${key} is not part of the wayBetter for X plugin API`); if (!(key in target)) throw new Error(`test harness: ${label}.${key} is not stubbed`); return (target as Any)[key]; }, }); } function element(tag: string): Any { return { tag, className: '', textContent: '', children: [] as unknown[], attrs: new Map(), append(...nodes: unknown[]) { this.children.push(...nodes); }, replaceChildren(...nodes: unknown[]) { this.children = nodes; }, toggleAttribute(name: string, on: boolean) { if (on) this.attrs.set(name, ''); else this.attrs.delete(name); }, removeAttribute(name: string) { this.attrs.delete(name); }, }; } function load(source: string) { const calls: Array<[string, ...unknown[]]> = []; const store = new Map(); const keyHandlers: Array<(event: Any, item: unknown) => unknown> = []; const themeHandlers: Array<(theme: unknown) => void> = []; let disposed = false; const ui = strict('ui', UI_KEYS, { toast: (message: string, options?: Any) => { calls.push(['toast', message, options]); return { element: {}, close() {}, closed: Promise.resolve() }; }, dialog: (options: Any) => { calls.push(['dialog', options]); return { element: {}, body: element('div'), open: true, closed: Promise.resolve(), close() {}, setError() {}, setBusy() {} }; }, h: (tag: string, props: Any | null, ...children: unknown[]) => ({ ...element(tag), props: props ?? {}, checked: props?.checked, children: children.flat() }), }); const scope = strict('scope', SCOPE_KEYS, { css: (id: string, css: string) => calls.push(['css', id, css]), onKey: (fn: (event: Any, item: unknown) => unknown) => keyHandlers.push(fn), onThemeChange: (fn: (theme: unknown) => void) => themeHandlers.push(fn), add: (node: unknown) => { calls.push(['add', node]); return node; }, defer: (fn: () => void) => calls.push(['defer', fn]), ui, dispose: () => { disposed = true; }, }); const theme = themeFromColors('#000000', '#e7e9ea', '#1d9bf0'); const api = strict('api', API_KEYS, { injectCSS: (id: string, css: string) => calls.push(['injectCSS', id, css]), clearCSS: (id: string) => calls.push(['clearCSS', id]), scope: () => scope, storage: (namespace: string) => ({ get: (key: string, fallback: unknown) => (store.has(`${namespace}:${key}`) ? store.get(`${namespace}:${key}`) : fallback), set: (key: string, value: unknown) => { store.set(`${namespace}:${key}`, JSON.parse(JSON.stringify(value))); return true; }, remove: (key: string) => store.delete(`${namespace}:${key}`), keys: () => [], }), tweet: () => ({ url: 'https://x.com/someone/status/1' }), copyText: async (text: string) => { calls.push(['copyText', text]); return true; }, theme: { current: () => theme, onChange: () => () => {} }, ui, }); const document = { documentElement: element('html'), body: element('body'), createElement: element }; const context = vm.createContext({ wayBetter: { api }, document, console }); context.window = context; const module = { exports: {} as unknown }; vm.runInContext(`(function (module, exports) {\n${source}\n})`, context)(module, module.exports); // As waybetter-host `start_script` instantiates it. const P = (module.exports as Any)?.default ?? module.exports; const instance = typeof P === 'function' ? (P.prototype && (P.prototype.start || P.prototype.stop) ? new P() : P()) : P; return { instance, calls, store, keyHandlers, themeHandlers, document, theme, disposed: () => disposed }; } test('every example plugin has a complete header and a file name the plugins folder accepts', () => { assert.equal(PLUGIN_EXAMPLES.length, 4); for (const example of PLUGIN_EXAMPLES) { const meta = parsePluginHeader(example.file, example.source); assert.ok(meta.header && meta.name && meta.description && meta.version && meta.author, example.id); assert.ok(validPluginFileName(example.file), example.file); assert.equal(pluginFileFor(meta.name), example.file); assert.deepEqual(pluginWarnings(meta, example.source), [], example.id); assert.doesNotMatch(example.source, /\bfetch\(|XMLHttpRequest|innerHTML|eval\(/, `${example.id}: no requests, no HTML strings`); } }); test('the smallest example injects and clears its CSS', () => { const run = load(PLUGIN_EXAMPLES.find(e => e.id === 'smallest')!.source); run.instance.start(); run.instance.stop(); assert.deepEqual(run.calls.map(c => c[0]), ['injectCSS', 'clearCSS']); }); test('the scope example copies the focused post link on c and toasts', async () => { const run = load(PLUGIN_EXAMPLES.find(e => e.id === 'scope')!.source); run.instance.start(); await run.keyHandlers[0]!({ key: 'x' }, {}); await run.keyHandlers[0]!({ key: 'c' }, null); assert.equal(run.calls.filter(c => c[0] === 'copyText').length, 0, 'nothing without a focused post'); await run.keyHandlers[0]!({ key: 'c' }, {}); assert.deepEqual(run.calls.find(c => c[0] === 'copyText'), ['copyText', 'https://x.com/someone/status/1']); assert.equal(run.calls.find(c => c[0] === 'toast')?.[1], 'Link copied'); run.instance.stop(); assert.ok(run.disposed()); }); test('the settings example saves through a dialog and undoes from the toast', () => { const run = load(PLUGIN_EXAMPLES.find(e => e.id === 'settings')!.source); run.instance.start(); assert.ok(run.document.documentElement.attrs.has('data-hide-promoted')); run.keyHandlers[0]!({ key: 'P' }, null); const dialog = run.calls.find(c => c[0] === 'dialog')?.[1] as Any; const body = element('div'); dialog.content(body); const checkbox = body.children[0].children[0]; assert.equal(checkbox.checked, true); checkbox.checked = false; const save = dialog.actions.find((action: Any) => action.default); assert.notEqual(save.onClick({ setError() {} }), false); assert.equal(JSON.stringify(run.store.get('hide-promoted:settings')), '{"hide":false}'); assert.equal(run.document.documentElement.attrs.has('data-hide-promoted'), false); const toast = run.calls.find(c => c[0] === 'toast') as [string, string, Any]; toast[2].action.onClick(); assert.equal(JSON.stringify(run.store.get('hide-promoted:settings')), '{"hide":true}'); assert.ok(run.document.documentElement.attrs.has('data-hide-promoted')); run.instance.stop(); assert.ok(run.disposed()); }); test('the theme example draws the active theme and follows changes', () => { const run = load(PLUGIN_EXAMPLES.find(e => e.id === 'theme')!.source); run.instance.start(); const css = run.calls.find(c => c[0] === 'css')?.[2] as string; assert.match(css, /var\(--wb-surface\)/); const badge = run.document.body.children[0]; assert.equal(badge.className, 'theme-badge'); assert.equal(badge.children[0].textContent, run.theme.name); run.themeHandlers[0]!({ ...run.theme, name: 'Midnight Mint', mode: 'dark', siteDefault: false }); assert.equal(badge.children[0].textContent, 'Midnight Mint'); assert.equal(badge.children[1], ' ยท dark'); run.instance.stop(); assert.ok(run.disposed()); }); test('the theme example imports cleanly and validates against theme.schema.json', () => { const parsed = parseThemeJson(THEME_EXAMPLE); assert.ok('theme' in parsed, 'error' in parsed ? parsed.error : ''); assert.deepEqual(parsed.warnings, []); assert.equal(parsed.theme.name, 'Midnight Mint'); const schema = JSON.parse(readFileSync(new URL('../../../waybetter/packages/waybetter/theme.schema.json', import.meta.url), 'utf8')); assert.deepEqual(validate(schema, JSON.parse(THEME_EXAMPLE)), []); }); test('UI kit demos call real kit methods; color roles map to the --wb-* variables', () => { for (const demo of KIT_DEMOS) { assert.ok(UI_KEYS.has(demo.kind), demo.kind); assert.ok(demo.code.includes(`ui.${demo.kind}(`), demo.id); } const theme = themeFromColors('#101010', '#f0f0f0', '#3ddc97'); const vars = themeVars(theme); for (const role of Object.keys(theme.colors)) assert.ok(cssVarOf(role) in vars, role); }); test('the bundled docs parse into headings, tables and code, and every link resolves', () => { const docs = ['PLUGINS.md', 'THEMES.md', 'PLUGIN-IDEAS.md']; for (const name of docs) { const source = readFileSync(new URL(`../../../docs/${name}`, import.meta.url), 'utf8'); const blocks = parseMarkdown(source); assert.ok(blocks.some(block => block.kind === 'heading'), name); if (name !== 'PLUGIN-IDEAS.md') assert.ok(blocks.some(block => block.kind === 'code'), name); for (const block of blocks) if (block.kind === 'table') for (const row of block.rows) assert.equal(row.length, block.head.length, `${name}: ${row.join(' | ')}`); const anchors = new Set(blocks.filter(block => block.kind === 'heading').map(block => anchorOf((block as { text: string }).text))); const texts = blocks.flatMap(block => block.kind === 'list' ? block.items : block.kind === 'table' ? [...block.head, ...block.rows.flat()] : 'text' in block && block.kind !== 'code' ? [block.text] : []); for (const token of texts.flatMap(inlineTokens)) { if (token.kind !== 'link') continue; const link = resolveLink(token.href, docs); assert.ok(link, `${name}: ${token.href} resolves`); if (link.anchor && !link.doc) assert.ok(anchors.has(link.anchor), `${name}: #${link.anchor}`); } } assert.equal(anchorOf('Matching the theme: `api.theme`'), 'matching-the-theme-apitheme'); assert.deepEqual(resolveLink('./PLUGINS.md#matching-the-theme-apitheme', docs), { doc: 'PLUGINS.md', anchor: 'matching-the-theme-apitheme' }); });