wayBetter for X

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

View file

@ -0,0 +1,423 @@
// 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<typeof areaPage>;
/** The flags with every other area switched off: what Profile+ alone does to the page. */
const only = (patch: Record<string, boolean>) => 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<string, unknown> = {}) {
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<string, unknown>;
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 data-testid="UserDescription">.*?<\/div><div class="bt-note" data-handle="maple_harbor" role="button" aria-label="Your private note about @maple_harbor. Edit"><span class="bt-note-label">Your private note<\/span><span class="bt-note-text">met at a jam\nplays bass<\/span><\/div>/);
assert.match(now.seen[1]!, /<div role="menu"><div role="menuitem" tabindex="0" class="bt-note-item" data-bt-profile-item="note"><svg><path d="M14\.23[^"]*"><\/path><\/svg><div><span>Edit private note<\/span><\/div><\/div><div role="menuitem" data-testid="block"/);
assert.match(now.seen[2]!, /<div class="bt-note-hover" data-prefix="Your note: ">sells synths<\/div>/);
assert.equal(now.seen[2]!.match(/bt-note-hover/g)!.length, 1, 'a card of an account without a note gets nothing');
assert.doesNotMatch(now.seen[3]!, /bt-note"|bt-note-hover/, 'off: the notes are out of the page');
});
test('User Notes: a display name that looks like a handle never shows the note of another account', () => {
const { env, body } = profilePage({ ...ROBOT, name: '@maple_harbor' }, { maple_harbor: 'met at a jam' });
env.registry.setFlags(only({ 'profile.userNotes': true }));
assert.equal(body.querySelectorAll('.bt-note').length, 0, 'the note is about @maple_harbor, this is @tin_robot');
env.registry.dispose();
});
test('User Notes: nothing without a note, the header follows the profile, and off leaves nothing', () => {
const { env, body, layers } = profilePage(ROBOT, { maple_harbor: 'met at a jam' });
env.registry.setFlags(only({ 'profile.userNotes': true }));
assert.equal(body.querySelectorAll('.bt-note').length, 0, 'no note about this account');
openMenu(env, body, layers, ROBOT.handle);
assert.equal(body.querySelector('.bt-note-item')!.textContent, 'Add private note');
// A menu X opens for anything else (a post's ···) is left alone.
layers.textContent = '';
env.doc.emit('pointerdown', { target: body });
layers.append(...profileMenu('someone'));
env.mutate();
assert.equal(body.querySelectorAll('.bt-note-item').length, 0);
// X keeps the header mounted from profile to profile and only changes its text.
const handle = body.querySelectorAll('[data-testid="UserName"] span').find(span => span.textContent === '@tin_robot')!;
handle.textContent = `@${MAPLE.handle}`;
env.go(`/${MAPLE.handle}`);
assert.equal(body.querySelector('.bt-note .bt-note-text')!.textContent, 'met at a jam');
handle.textContent = '@tin_robot';
env.go('/tin_robot');
assert.equal(body.querySelectorAll('.bt-note').length, 0, 'the other profile\'s note is gone');
// Every word it shows is one of the plugin's own keys.
for (const key of ['hoverPrefix', 'boxLabel', 'label', 'placeholder', 'areaLabel', 'title', 'message', 'delete', 'deleteTitle', 'deleteMessage', 'deleted', 'cancel', 'save', 'saveFailed', 'edit', 'add']) {
assert.ok(messages[`plugin.user-notes.${key}`], key);
}
env.registry.setFlags(only({}));
assert.deepEqual([env.live(), body.querySelectorAll('.bt-note, .bt-note-hover, .bt-note-item').length], [[], 0]);
assert.deepEqual([...env.store.keys()], ['bt-plugin:user-notes:maple_harbor'], 'it wrote nothing of its own');
env.registry.dispose();
});
test('whose block: the "@handle" span, lowercase; nothing when there is none', () => {
assert.equal(handleSpanOf(node(profile(MAPLE).querySelector('[data-testid="UserName"]')))!.handle, 'maple_harbor');
assert.equal(handleSpanOf(node(post(ROBOT)))!.handle, 'tin_robot');
assert.equal(handleSpanOf(node(h('div', null, h('span', null, 'Follows you'), h('span', null, '@way_too_long_for_a_handle')))), null);
assert.equal(handleSpanOf(null), null);
});
// ---- New with the area: private labels, account age, hover card peek ---------------------------------
const node = (el: El | null) => el as unknown as NameNode;
const element = (el: El) => el as unknown as Element;
/** This area's word `key` of feature `name`, in English. */
const word = (name: string) => (key: string, params: Record<string, unknown> = {}) =>
(messages[`plugin.profile-plus.${name}.${key}`] ?? key).replace(/\{(\w+)\}/g, (_, param: string) => String(params[param]));
/** The user cache as modules see it (`api.users`), filled by hand: what X "already sent". */
function knownUsers(initial: Record<string, Partial<UserInfo>> = {}) {
const map = new Map<string, UserInfo>();
const listeners = new Set<(handles: string[]) => void>();
const put = (handle: string, info: Partial<UserInfo>) => map.set(handle, { xUserId: '1', handle, blue: null, verifiedType: null, following: null, followedBy: null, createdAt: null, ...map.get(handle), ...info });
for (const [handle, info] of Object.entries(initial)) put(handle, info);
const api: UsersApi = { get: handle => map.get(handle.toLowerCase()) ?? null, onUpdate: callback => { listeners.add(callback); return () => { listeners.delete(callback); }; } };
return { api, listeners, /** A response X fetched adds to what is known. */ learn(handle: string, info: Partial<UserInfo>) { put(handle, info); for (const listener of [...listeners]) listener([handle]); } };
}
/** Profile+'s modules over the page's API with `extra` in place (a user cache, a signed-in account). */
function profileRegistry(env: Env, extra: Partial<PluginApi>) {
const api = Object.freeze({ ...env.api, ...extra }) as PluginApi;
const location = (env.win as unknown as { location: { pathname: string } }).location;
const registry = createFeatureRegistry<PluginApi>({ win: env.win as unknown as Window & typeof globalThis, table: X_TABLE, api, label: 'wayBetter for X', surfaces: () => surfacesOf(location.pathname) });
for (const module of MODULES) registry.define(module);
return registry;
}
test('Profile+: the new features are flags of their own, off until chosen, with their words and schemas', () => {
const added = ['profile.privateLabels', 'profile.accountAge', 'profile.hoverPeek'];
for (const id of added) {
const feature = FEATURES.find(item => item.id === id)!;
assert.deepEqual([feature.backing, feature.default, feature.supersedes], ['module', false, undefined], id);
assert.ok(messages[feature.label!] && messages[feature.description!], id);
assert.equal(!!feature.settings, !!MODULES.find(module => module.id === id)!.settings?.length, id);
}
assert.deepEqual([...POWER].sort(), [...added].sort(), 'Power User turns on what only adds information');
assert.deepEqual(FEATURES.find(feature => feature.id === 'profile.accountAge')!.surfaces, ['profile']);
for (const [name, schema] of [['privateLabels', LABELS_SCHEMA], ['hoverPeek', PEEK_SCHEMA]] as const) {
for (const field of schema) if (field.type !== 'divider') {
assert.ok(messages[`plugin.profile-plus.${name}.${field.label}`], field.label);
if (field.hint) assert.ok(messages[`plugin.profile-plus.${name}.${field.hint}`], field.hint);
}
}
// Notes and labels have one home: the namespace User Notes always used.
assert.deepEqual(MODULES.filter(module => module.storage).map(module => [module.id, module.storage]), [['profile.userNotes', 'user-notes']]);
});
test('labels: stored beside the note, one line of plain text, and a note is never touched', () => {
const env = areaPage();
const store = env.api.storage('user-notes');
store.set('maple_harbor', 'met at a jam');
assert.equal(writeLabel(store, 'Maple_Harbor', { text: ' close\n friend ', color: 'green' }), true);
assert.deepEqual(readLabel(store, 'MAPLE_HARBOR'), { text: 'close friend', color: 'green' });
assert.deepEqual(JSON.parse(env.store.get('bt-plugin:user-notes:label:maple_harbor')!), { text: 'close friend', color: 'green' });
assert.equal(readNote(store, 'Maple_Harbor'), 'met at a jam', 'the note is the plugin\'s string, unchanged');
assert.equal(labelKey('Tin_Robot'), 'label:tin_robot');
assert.doesNotMatch(labelKey('x'), /^[A-Za-z0-9_]{1,15}$/, 'a label\'s key can never be a handle');
// Removing: an empty text, or null.
writeLabel(store, 'maple_harbor', { text: ' ', color: 'green' });
assert.deepEqual([readLabel(store, 'maple_harbor'), env.store.has('bt-plugin:user-notes:label:maple_harbor'), readNote(store, 'maple_harbor')], [null, false, 'met at a jam']);
// Whatever is stored, a label is short plain text in a known color.
assert.equal(Array.from(labelText('x'.repeat(80))).length, LABEL_MAX);
assert.equal(labelText('a‮b\u0007c'), 'a b c', 'no direction override, no control character');
assert.deepEqual(normalizeLabel({ text: 'press', color: 'url(x)' }), { text: 'press', color: 'gray' });
for (const odd of [null, 'press', 7, {}, { text: 5 }, { text: '' }, []]) assert.equal(normalizeLabel(odd), null, JSON.stringify(odd));
assert.equal(readNote(store, 'nobody'), '');
env.registry.dispose();
});
test('whose name: a display name that looks like a handle never borrows another account\'s label', () => {
const spoof = { name: '@maple_harbor', handle: 'tin_robot' };
// In a post and a hover card the name and the handle are links to the account: only the real one counts.
assert.equal(handleSpanOf(node(post(spoof)))!.handle, 'tin_robot');
assert.equal(handleSpanOf(node(hoverCard({ ...spoof, bio: '@maple_harbor' })))!.handle, 'tin_robot');
// A profile header has no links: the handle follows the name.
assert.equal(handleSpanOf(node(profile(spoof).querySelector('[data-testid="UserName"]')))!.handle, 'tin_robot');
// A bio is never read, so a block whose handle cannot be told names nobody.
assert.equal(handleSpanOf(node(h('div', null, h('div', { 'data-testid': 'UserDescription' }, h('span', null, '@maple_harbor'))))), null);
assert.equal(handleSpanOf(null), null);
// The name row is the row just before the handle's row, in each shape X uses.
for (const block of [post(MAPLE).querySelector('[data-testid="User-Name"]')!, post(ROBOT, 'text', MAPLE).querySelectorAll('[data-testid="User-Name"]')[1]!, profile(MAPLE).querySelector('[data-testid="UserName"]')!, hoverCard(MAPLE)]) {
const found = handleSpanOf(node(block))!;
assert.equal(found.handle, 'maple_harbor');
assert.equal((nameRowOf(node(block), found.span) as unknown as El).className, 'name-row');
}
const flat = h('div', null, h('span', null, '@tin_robot'));
assert.equal(nameRowOf(node(flat), handleSpanOf(node(flat))!.span), node(flat), 'an unknown shape: the block itself');
});
test('Private labels: beside the name on the profile, on posts, on a quoted post and in the hover card; off leaves X\'s markup as it was', () => {
const { env, body, layers } = profilePage(MAPLE, { 'label:maple_harbor': { text: 'friend', color: 'green' }, 'label:tin_robot': { text: 'press', color: 'blue' }, maple_harbor: 'met at a jam' });
const column = body.querySelector('[data-testid="primaryColumn"]')!;
column.append(post(MAPLE), post(ROBOT, 'Quoting.', MAPLE), post({ name: '@maple_harbor', handle: 'someone_else' }), post({ name: 'No Label', handle: 'no_label' }));
const before = body.outerHTML;
env.registry.setFlags(only({ 'profile.privateLabels': true }));
const labelled = () => body.querySelectorAll('[data-bt-profile-label]').map(row => [row.className, row.getAttribute('data-bt-profile-label'), row.getAttribute('data-bt-profile-label-color'), row.textContent]);
assert.deepEqual(labelled(), [
['name-row', 'friend', 'green', 'Maple Harbor'], // the profile header
['name-row', 'friend', 'green', 'Maple Harbor'], // their post
['name-row', 'press', 'blue', 'Tin Robot'], // a post by the other account
['name-row', 'friend', 'green', 'Maple Harbor'], // quoted in it
], 'and nothing on the impostor or the account without a label');
assert.deepEqual(env.live(), ['wbf-profile.privateLabels']);
assert.match(LABELS_CSS, /\[data-bt-profile-label\]::after \{ content: attr\(data-bt-profile-label\)/, 'the chip is the stylesheet\'s: the text is never markup');
// Posts X renders later, and its hover card once the account is in it.
column.append(post(ROBOT));
layers.append(hoverCard(ROBOT));
env.mutate();
assert.equal(labelled().filter(row => row[1] === 'press').length, 3);
// The ··· menu of the profile offers the label, next to X's own items.
layers.textContent = '';
openMenu(env, body, layers, MAPLE.handle);
assert.deepEqual(body.querySelectorAll('[role="menuitem"]').map(item => item.textContent), ['Edit private label', 'Block @Maple_Harbor']);
assert.deepEqual(env.api.commands.list().map(command => [command.id, command.title]), [
['bt-profile-plus.plugin.js:privateLabels.edit', 'Set a private label for this profile'], ['bt-profile-plus.plugin.js:settings', 'Profile+ settings'],
]);
// "Show labels on posts" off: only the header (and hover cards) keep theirs. Saved where the area's settings go.
layers.textContent = '';
column.querySelectorAll('[data-testid="cellInnerDiv"]').at(-1)!.remove();
env.store.set('bt-plugin:profile-plus.privateLabels:settings', JSON.stringify({ posts: false }));
env.registry.setFlags(only({}));
assert.deepEqual([env.live(), env.api.commands.list(), body.outerHTML], [[], [], before], 'off: attributes, item, command and rules are gone');
env.registry.setFlags(only({ 'profile.privateLabels': true }));
assert.deepEqual(labelled().map(row => row[1]), ['friend']);
// X reuses the header for the next profile: the label follows the account, not the element.
const handle = body.querySelectorAll('[data-testid="UserName"] span').find(span => span.textContent === '@Maple_Harbor')!;
handle.textContent = '@no_label';
env.go('/no_label');
assert.deepEqual(labelled(), []);
handle.textContent = '@tin_robot';
env.go('/tin_robot');
assert.deepEqual(labelled().map(row => row[1]), ['press']);
assert.equal(readNote(env.api.storage('user-notes'), 'maple_harbor'), 'met at a jam');
env.registry.dispose();
});
test('Private labels and User Notes together: two items in the profile\'s menu, each its own', () => {
const { env, body, layers } = profilePage(MAPLE, { maple_harbor: 'met at a jam' });
env.registry.setFlags(only({ 'profile.userNotes': true, 'profile.privateLabels': true }));
openMenu(env, body, layers, MAPLE.handle);
assert.deepEqual(body.querySelectorAll('[role="menuitem"]').map(item => `${item.getAttribute('data-bt-profile-item')}: ${item.textContent}`).sort(), ['label: Add private label', 'note: Edit private note', 'null: Block @Maple_Harbor']);
assert.equal(body.querySelectorAll('.bt-note-item').length, 1, 'the label\'s item is not a copy of the note\'s');
// markLabel takes a label off a block X reused for an account without one.
const block = post(MAPLE).querySelector('[data-testid="User-Name"]')!;
const store = env.api.storage('user-notes');
writeLabel(store, 'maple_harbor', { text: 'friend', color: 'pink' });
markLabel(element(block), store);
assert.equal(block.querySelector('.name-row')!.getAttribute('data-bt-profile-label'), 'friend');
writeLabel(store, 'maple_harbor', null);
markLabel(element(block), store);
assert.equal(block.querySelectorAll('[data-bt-profile-label], [data-bt-profile-label-color]').length, 0);
env.registry.dispose();
});
test('account age: whole years and months from the creation time, never from a guess', () => {
const at = (y: number, m: number, d: number) => Date.UTC(y, m - 1, d, 12);
const now = at(2026, 9, 30);
const text = word('accountAge');
const cases: Array<[number, string]> = [
[at(2017, 7, 12), '9 y 2 mo'], [at(2017, 9, 30), '9 y'], [at(2017, 10, 1), '8 y 11 mo'], [at(2026, 7, 31), '1 mo'],
[at(2026, 8, 31), '30 d'], [at(2026, 9, 18), '12 d'], [now, 'today'], [now - 1000, 'today'],
];
for (const [created, expected] of cases) assert.equal(ageText(ageOf(created, now)!, text), expected, new Date(created).toISOString());
assert.equal(ageOf(now + 1, now), null, 'a creation time ahead of now is not an age');
assert.equal(ageOf(NaN, now), null);
// On the page: only where X already sent the account's creation time.
const body = h('body', null, profile(MAPLE));
const joined = body.querySelector('[data-testid="UserJoinDate"]')!;
const users = knownUsers({ maple_harbor: { createdAt: at(2017, 7, 12) }, tin_robot: {} });
applyAge(body as unknown as ParentNode, users.api, text, now);
assert.equal(joined.getAttribute('data-bt-profile-age'), '9 y 2 mo');
assert.equal(joined.textContent, 'Joined June 2015', 'X\'s own line is not rewritten (and its month is not read)');
const handle = body.querySelectorAll('[data-testid="UserName"] span').find(span => span.textContent === '@Maple_Harbor')!;
for (const unknown of ['@tin_robot', '@never_seen']) {
handle.textContent = unknown;
applyAge(body as unknown as ParentNode, users.api, text, now);
assert.equal(joined.hasAttribute('data-bt-profile-age'), false, `${unknown}: absent is unknown`);
}
});
test('Account age: on profiles only, follows the header and the cache, and off takes it away', () => {
const { env, body } = profilePage(MAPLE);
const created = Date.UTC(2015, 5, 6);
const users = knownUsers({ maple_harbor: { createdAt: created } });
const registry = profileRegistry(env, { users: users.api });
const joined = body.querySelector('[data-testid="UserJoinDate"]')!;
const expected = () => ageText(ageOf(created, Date.now())!, word('accountAge'));
registry.setFlags(only({ 'profile.accountAge': true }));
assert.deepEqual([registry.active(), env.live()], [['profile.accountAge'], ['wbf-profile.accountAge']]);
assert.equal(joined.getAttribute('data-bt-profile-age'), expected());
assert.match(expected(), /^\d+ y( \d+ mo)?$/);
// The next profile in the same header: unknown until X's response for it is seen.
const handle = body.querySelectorAll('[data-testid="UserName"] span').find(span => span.textContent === '@Maple_Harbor')!;
handle.textContent = '@tin_robot';
env.mutate();
assert.equal(joined.hasAttribute('data-bt-profile-age'), false);
users.learn('tin_robot', { createdAt: created });
assert.equal(joined.getAttribute('data-bt-profile-age'), expected());
env.go('/home');
assert.deepEqual([registry.active(), joined.hasAttribute('data-bt-profile-age')], [[], false], 'not a profile: not running');
env.go('/tin_robot');
assert.equal(joined.getAttribute('data-bt-profile-age'), expected());
registry.setFlags(only({}));
assert.deepEqual([env.live(), joined.hasAttribute('data-bt-profile-age'), users.listeners.size], [[], false, 0]);
registry.dispose();
env.registry.dispose();
});
test('peek facts: what is known is shown, what is not is left out (absent is unknown, not "no")', () => {
const all = normalizeSettings(PEEK_SCHEMA, null) as PeekSettings;
assert.deepEqual(all, { age: true, relationship: true, note: true });
const now = Date.UTC(2026, 8, 30);
const base = { self: false, label: null, note: '', settings: all, now };
const user = (info: Partial<UserInfo>) => ({ createdAt: null, following: null, followedBy: null, ...info });
assert.deepEqual(peekFacts({ ...base, user: null }), { age: null, relation: null, label: null, note: '' }, 'an account X has not described');
assert.deepEqual(peekFacts({ ...base, user: user({}) }), { age: null, relation: null, label: null, note: '' }, 'described, but without these fields');
assert.equal(peekFacts({ ...base, user: user({ followedBy: true }) }).relation, 'followsYou');
assert.equal(peekFacts({ ...base, user: user({ followedBy: true, following: false }) }).relation, 'followsYou');
assert.equal(peekFacts({ ...base, user: user({ followedBy: true, following: true }) }).relation, 'mutual');
assert.equal(peekFacts({ ...base, user: user({ followedBy: false, following: true }) }).relation, 'notFollowingYou', 'only when X said so');
assert.equal(peekFacts({ ...base, user: user({ following: true }) }).relation, null, 'your own follow says nothing about theirs');
assert.equal(peekFacts({ ...base, self: true, user: user({ followedBy: false }) }).relation, null, 'your own card');
assert.deepEqual(peekFacts({ ...base, user: user({ createdAt: Date.UTC(2017, 6, 12) }) }).age, { years: 9, months: 2, days: 3367 });
const mine = { label: { text: 'press', color: 'blue' as const }, note: 'sells synths' };
assert.deepEqual(peekFacts({ ...base, ...mine, user: null }), { age: null, relation: null, ...mine });
const none: PeekSettings = { age: false, relationship: false, note: false };
assert.deepEqual(peekFacts({ ...base, ...mine, settings: none, user: user({ createdAt: Date.UTC(2017, 6, 12), followedBy: true }) }), { age: null, relation: null, label: null, note: '' });
});
test('Hover card peek: adds what is in memory to X\'s card, more when X sends more, and off removes it', () => {
const { env, body, layers } = profilePage(MAPLE, { tin_robot: 'sells synths', 'label:tin_robot': { text: 'press', color: 'blue' } });
const created = Date.UTC(2015, 5, 6);
const users = knownUsers({ tin_robot: { createdAt: created } });
const registry = profileRegistry(env, { users: users.api, account: () => ({ handle: 'Maple_Harbor', name: 'Maple Harbor', avatar: null }) });
const age = () => `Account age: ${ageText(ageOf(created, Date.now())!, word('accountAge'))}`;
registry.setFlags(only({ 'profile.hoverPeek': true }));
assert.deepEqual([registry.active(), body.querySelectorAll('.bt-profile-peek').length], [['profile.hoverPeek'], 0], 'no card, nothing added');
const peek = () => body.querySelectorAll('.bt-profile-peek').map(block => [block.getAttribute('data-handle'), ...block.children.map(part => `${part.className.replace('bt-profile-peek-', '')}: ${part.getAttribute('data-prefix') ?? ''}${part.textContent}`)]);
layers.append(hoverCard(ROBOT));
env.mutate();
assert.deepEqual(peek(), [['tin_robot', `age: ${age()}`, 'label: press', 'note: Your note: sells synths']]);
assert.equal(body.querySelector('.bt-profile-peek-label')!.getAttribute('data-bt-profile-peek-color'), 'blue');
assert.equal(body.querySelector('[data-testid="HoverCard"] [data-testid="UserDescription"]')!.nextElementSibling!.className, 'bt-profile-peek', 'under the bio, inside X\'s card');
// X fetches the account while the card is open: the relationship it sent is added, once.
users.learn('tin_robot', { followedBy: true, following: true });
assert.deepEqual(peek(), [['tin_robot', `age: ${age()}`, 'relation: You follow each other', 'label: press', 'note: Your note: sells synths']]);
users.learn('tin_robot', { followedBy: false });
assert.equal(peek()[0]![2], 'relation: Does not follow you');
// A card about an account nothing is known of gets no block at all; your own card says nothing about following.
layers.textContent = '';
layers.append(hoverCard({ name: 'No Label', handle: 'no_label' }), hoverCard(MAPLE));
env.mutate();
users.learn('maple_harbor', { followedBy: false });
assert.deepEqual(peek(), []);
// Its settings choose the facts.
layers.textContent = '';
layers.append(hoverCard(ROBOT));
env.mutate();
env.store.set('bt-plugin:profile-plus.hoverPeek:settings', JSON.stringify({ age: false, relationship: true, note: false }));
users.learn('tin_robot', {});
assert.deepEqual(peek(), [['tin_robot', 'relation: Does not follow you']]);
registry.setFlags(only({}));
assert.deepEqual([env.live(), body.querySelectorAll('.bt-profile-peek').length, users.listeners.size], [[], 0, 0]);
assert.deepEqual([...env.store.keys()].sort(), ['bt-plugin:profile-plus.hoverPeek:settings', 'bt-plugin:user-notes:label:tin_robot', 'bt-plugin:user-notes:tin_robot'], 'it reads the notes and writes none');
registry.dispose();
env.registry.dispose();
});