// Profile+ (src/areas/profile): User Notes as a feature module, compared with the plugin it // replaces, and the features that are new with the area. The table-level checks (every module // starts and stops clean, one observer, the budget) are in areas.test.ts. Markup: fixtures/x/profile // (X-shaped, invented accounts); nothing here is a live X test. import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; import { ageOf, ageText, applyAge } from '../src/areas/profile/accountAge.ts'; import { FEATURES, POWER } from '../src/areas/profile/features.ts'; import { PEEK_SCHEMA, peekFacts, type PeekSettings } from '../src/areas/profile/hoverPeek.ts'; import { MODULES } from '../src/areas/profile/index.ts'; import { LABEL_MAX, handleSpanOf, labelKey, labelText, nameRowOf, normalizeLabel, readLabel, readNote, writeLabel, type NameNode } from '../src/areas/profile/notes.ts'; import { LABELS_CSS, LABELS_SCHEMA, markLabel } from '../src/areas/profile/privateLabels.ts'; import { USER_NOTES_CSS } from '../src/areas/profile/userNotes.ts'; import { surfacesOf } from '../src/areas/surfaces.ts'; import { X_TABLE } from '../src/areas/table.ts'; import type { PluginApi } from '../src/inject/pluginApi.ts'; import type { UserInfo, UsersApi } from '../src/inject/userInfo.ts'; import { createFeatureRegistry } from '../../../waybetter/packages/runtime/src/featureModules.ts'; import { normalizeSettings } from '../../../waybetter/packages/runtime/src/settingsForm.ts'; import { areaPage, flagsWith, messages } from './helpers/areaPage.ts'; import { h, mount, type El } from './fixtures/x/profile/miniDom.ts'; import { hoverCard, post, profile, profileMenu } from './fixtures/x/profile/x.ts'; const BUNDLED = new URL('../src-tauri/bundled-plugins/', import.meta.url); const MAPLE = { name: 'Maple Harbor', handle: 'Maple_Harbor' }; const ROBOT = { name: 'Tin Robot', handle: 'tin_robot' }; type Env = ReturnType; /** The flags with every other area switched off: what Profile+ alone does to the page. */ const only = (patch: Record) => flagsWith({ ...Object.fromEntries(X_TABLE.areas.filter(area => area.id !== 'profile').map(area => [`area.${area.id}`, false])), ...patch }); /** A profile page of `account` in an area page: the primary column and X's `#layers`. */ function profilePage(account = MAPLE, stored: Record = {}) { const env = areaPage(`/${account.handle}`); for (const [key, value] of Object.entries(stored)) env.store.set(`bt-plugin:user-notes:${key}`, JSON.stringify(value)); const layers = h('div', { id: 'layers' }); const body = mount(env.doc, h('main', { role: 'main' }, profile(account)), layers); return { env, body, layers }; } /** The profile's ··· button is pressed and X opens its menu. */ function openMenu(env: Env, body: El, layers: El, handle: string) { env.doc.emit('pointerdown', { target: body.querySelector('[data-testid="userActions"]') }); layers.append(...profileMenu(handle)); env.mutate(); } /** The old plugin, started the way native code starts it (its globals are this page's). */ function startOldPlugin(file: string, env: Env): { stop(): void } { const module = { exports: {} as unknown }; new Function('module', 'exports', readFileSync(new URL(file, BUNDLED), 'utf8'))(module, module.exports); const scope = globalThis as Record; const before = { window: scope.window, document: scope.document }; scope.window = { wayBetter: { api: { ...env.api, i18n: env.api.i18n.namespace(file.replace('.plugin.js', '')) } } }; scope.document = env.doc; const plugin = new (module.exports as new () => { start(): void; stop(): void })(); plugin.start(); return { stop() { plugin.stop(); scope.window = before.window; scope.document = before.document; } }; } test('Profile+: the rows of its card and one module per module row', () => { assert.deepEqual(FEATURES.filter(feature => feature.backing !== 'module'), [], 'the two rules that were Tweaks are modules too'); assert.deepEqual(FEATURES.slice(0, 2).map(feature => [feature.id, feature.default]), [['hideProfileWhoToFollow', false], ['hideSubscribeButtons', false]]); assert.deepEqual(MODULES.map(module => module.id).sort(), FEATURES.filter(feature => feature.backing === 'module').map(feature => feature.id).sort()); const notes = FEATURES.find(feature => feature.id === 'profile.userNotes')!; assert.deepEqual([notes.default, notes.supersedes, notes.label], [false, 'bt-user-notes.plugin.js', 'plugin.user-notes.name']); for (const feature of FEATURES) assert.ok(!feature.surfaces?.includes('chat'), `${feature.id} stays out of X's chat`); }); test('User Notes: the same page as the plugin it replaces, from the notes the plugin stored', () => { const stored = { maple_harbor: 'met at a jam\nplays bass', tin_robot: 'sells synths' }; const run = (start: (env: Env) => () => void) => { const { env, body, layers } = profilePage(MAPLE, stored); const stop = start(env); const seen: string[] = [body.outerHTML]; openMenu(env, body, layers, MAPLE.handle); seen.push(body.outerHTML); layers.textContent = ''; layers.append(hoverCard(ROBOT), hoverCard({ name: 'No Note', handle: 'no_note' })); env.mutate(); seen.push(body.outerHTML); const css = env.live(); stop(); seen.push(body.outerHTML); env.registry.dispose(); return { seen, css }; }; // (A template literal's line ends are line feeds whatever the file's are.) const oldCss = /scope\.css\('user-notes', `([^`]*)`\)/.exec(readFileSync(new URL('user-notes.plugin.js', BUNDLED), 'utf8'))![1]!.replace(/\r\n/g, '\n'); const old = run(env => { const plugin = startOldPlugin('user-notes.plugin.js', env); return () => plugin.stop(); }); const now = run(env => { env.registry.setFlags(only({ 'profile.userNotes': true })); return () => env.registry.setFlags(only({})); }); assert.deepEqual(old.css, ['user-notes']); assert.deepEqual(now.css, ['wbf-profile.userNotes']); assert.equal(USER_NOTES_CSS, oldCss, 'the same rules'); // The module's menu item carries one more attribute (so another Profile+ item never copies it). assert.deepEqual(now.seen.map(html => html.replace(' data-bt-profile-item="note"', '')), old.seen); // And that page is: the note under the bio, "Edit private note" first in the menu, the note in the hover card. assert.match(now.seen[0]!, /
.*?<\/div>
Your private note<\/span>met at a jam\nplays bass<\/span><\/div>/); assert.match(now.seen[1]!, /