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,252 @@
// A small DOM for tests/area-chat.test.ts: enough of Element (attributes, children, text, `closest`,
// `querySelectorAll` over the selectors Chat+ uses, listeners with bubbling, focus, style) to run
// the Chat+ modules over X-shaped chat markup without a browser (./x.ts builds that markup and plays
// X's part). Not a DOM library: it implements only what these tests use, and a selector it does not
// understand throws. The selector engine is the one of ../profile/miniDom.ts.
type Simple = { tag?: string; id?: string; classes: string[]; attrs: { name: string; op?: string; value?: string }[]; nots: Simple[] };
type Step = { simple: Simple; child: boolean };
/** Splits at top-level separators (not inside brackets, parentheses or quotes). */
function split(source: string, separators: string): { part: string; by: string }[] {
const out: { part: string; by: string }[] = [];
let depth = 0;
let quote = '';
let part = '';
let by = '';
for (const char of source) {
if (quote) { part += char; if (char === quote) quote = ''; continue; }
if (char === '"' || char === "'") { quote = char; part += char; continue; }
if (char === '[' || char === '(') depth += 1;
if (char === ']' || char === ')') depth -= 1;
if (depth === 0 && separators.includes(char)) {
if (part) { out.push({ part, by }); part = ''; by = ''; }
if (char !== ' ') by = char;
continue;
}
part += char;
}
if (part) out.push({ part, by });
return out;
}
function parseSimple(source: string): Simple {
const simple: Simple = { classes: [], attrs: [], nots: [] };
let rest = source;
const take = (pattern: RegExp) => { const match = pattern.exec(rest); if (match) rest = rest.slice(match[0].length); return match; };
let match = take(/^[a-zA-Z][\w-]*/);
if (match) simple.tag = match[0].toLowerCase();
while (rest) {
if ((match = take(/^#([\w-]+)/))) simple.id = match[1];
else if ((match = take(/^\.([\w-]+)/))) simple.classes.push(match[1]!);
else if ((match = take(/^\[([\w-]+)(?:([\^$*]?=)"([^"]*)")?\]/))) simple.attrs.push({ name: match[1]!, op: match[2], value: match[3] });
else if ((match = take(/^:not\(([^()]*)\)/))) simple.nots.push(parseSimple(match[1]!));
else throw new Error(`miniDom: selector not supported: ${source}`);
}
return simple;
}
const parsed = new Map<string, Step[][]>();
function parse(selector: string): Step[][] {
let list = parsed.get(selector);
if (!list) {
list = split(selector, ',').map(({ part }) => split(part.trim(), ' >').map(step => ({ simple: parseSimple(step.part), child: step.by === '>' })));
parsed.set(selector, list);
}
return list;
}
function matchesSimple(el: El, simple: Simple): boolean {
if (simple.tag && el.tagName.toLowerCase() !== simple.tag) return false;
if (simple.id && el.getAttribute('id') !== simple.id) return false;
for (const name of simple.classes) if (!el.classList.contains(name)) return false;
for (const { name, op, value } of simple.attrs) {
const actual = el.getAttribute(name);
if (actual === null) return false;
if (op === '=' && actual !== value) return false;
if (op === '^=' && !actual.startsWith(value!)) return false;
if (op === '$=' && !actual.endsWith(value!)) return false;
if (op === '*=' && !actual.includes(value!)) return false;
}
return !simple.nots.some(not => matchesSimple(el, not));
}
function matchesSteps(el: El, steps: Step[], index = steps.length - 1): boolean {
const step = steps[index]!;
if (!matchesSimple(el, step.simple)) return false;
if (index === 0) return true;
if (step.child) return !!el.parentElement && matchesSteps(el.parentElement, steps, index - 1);
for (let up = el.parentElement; up; up = up.parentElement) if (matchesSteps(up, steps, index - 1)) return true;
return false;
}
export class Txt {
text: string;
parentElement: El | null = null;
constructor(text: string) { this.text = text; }
}
/** An event as the code under test makes them (`new win.MouseEvent(type, init)`) and as X's part fires them. */
export class Ev {
type: string;
target: El | null = null;
currentTarget: unknown = null;
defaultPrevented = false;
stopped = false;
[key: string]: unknown;
constructor(type: string, init: Record<string, unknown> = {}) { this.type = type; Object.assign(this, init); }
preventDefault() { this.defaultPrevented = true; }
stopPropagation() { this.stopped = true; }
}
type Listener = (event: Ev) => void;
type DocLike = { activeElement: unknown; emit(type: string, event: unknown): void };
class Style {
[key: string]: unknown;
setProperty(name: string, value: string) { this[name] = value; }
getPropertyValue(name: string) { return String(this[name] ?? ''); }
}
export class El {
tagName: string;
parentElement: El | null = null;
childNodes: (El | Txt)[] = [];
style = new Style();
value = '';
title = '';
type = '';
disabled = false;
scrollTop = 0;
scrollHeight = 0;
clientHeight = 0;
/** Set by `mount` on `<html>`: where an event goes after the tree, and who knows the focus. */
doc: DocLike | null = null;
rect = { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 };
private attributes = new Map<string, string>();
private listeners = new Map<string, Listener[]>();
constructor(tag: string) { this.tagName = tag.toUpperCase(); }
get nodeName() { return this.tagName; }
get children(): El[] { return this.childNodes.filter((node): node is El => node instanceof El); }
get firstElementChild(): El | null { return this.children[0] ?? null; }
get previousElementSibling(): El | null { const all = this.parentElement?.children ?? []; return all[all.indexOf(this) - 1] ?? null; }
get nextElementSibling(): El | null { const all = this.parentElement?.children ?? []; return all[all.indexOf(this) + 1] ?? null; }
private get root(): El { let up: El = this; while (up.parentElement) up = up.parentElement; return up; }
get isConnected(): boolean { return this.root.tagName === 'HTML'; }
getAttribute(name: string) { return this.attributes.get(name) ?? null; }
hasAttribute(name: string) { return this.attributes.has(name); }
setAttribute(name: string, value: string) { this.attributes.set(name, String(value)); }
removeAttribute(name: string) { this.attributes.delete(name); }
toggleAttribute(name: string, on = !this.attributes.has(name)) { if (on) this.attributes.set(name, ''); else this.attributes.delete(name); return on; }
get id() { return this.getAttribute('id') ?? ''; }
get className() { return this.getAttribute('class') ?? ''; }
set className(value: string) { this.setAttribute('class', value); }
get classList() {
const names = () => this.className.split(/\s+/).filter(Boolean);
return { contains: (name: string) => names().includes(name) };
}
get textContent(): string { return this.childNodes.map(node => (node instanceof El ? node.textContent : node.text)).join(''); }
set textContent(value: string) {
for (const node of this.childNodes) node.parentElement = null;
this.childNodes = [];
if (value !== '') this.append(String(value));
}
private adopt(nodes: (El | Txt | string)[]): (El | Txt)[] {
return nodes.map(node => {
const own = typeof node === 'string' ? new Txt(node) : node;
if (own instanceof El) own.remove();
return own;
});
}
private insert(at: number, nodes: (El | Txt)[]) {
this.childNodes.splice(at, 0, ...nodes);
for (const node of nodes) node.parentElement = this;
}
append(...nodes: (El | Txt | string)[]) { this.insert(this.childNodes.length, this.adopt(nodes)); }
appendChild<T extends El>(node: T): T { this.append(node); return node; }
insertBefore<T extends El>(node: T, before: El | null): T {
const own = this.adopt([node]);
this.insert(before ? this.childNodes.indexOf(before) : this.childNodes.length, own);
return node;
}
replaceChildren(...nodes: (El | Txt | string)[]) { this.textContent = ''; this.append(...nodes); }
remove() {
const parent = this.parentElement;
if (!parent) return;
parent.childNodes.splice(parent.childNodes.indexOf(this), 1);
this.parentElement = null;
}
contains(other: El | null): boolean { for (let up = other; up; up = up.parentElement) if (up === this) return true; return false; }
matches(selector: string): boolean { return parse(selector).some(steps => matchesSteps(this, steps)); }
closest(selector: string): El | null {
for (let up: El | null = this; up; up = up.parentElement) if (up.matches(selector)) return up;
return null;
}
querySelectorAll(selector: string): El[] {
const out: El[] = [];
const walk = (el: El) => { for (const child of el.children) { if (child.matches(selector)) out.push(child); walk(child); } };
walk(this);
return out;
}
querySelector(selector: string): El | null { return this.querySelectorAll(selector)[0] ?? null; }
addEventListener(type: string, listener: Listener) { (this.listeners.get(type) ?? this.listeners.set(type, []).get(type)!).push(listener); }
removeEventListener(type: string, listener: Listener) { this.listeners.set(type, (this.listeners.get(type) ?? []).filter(item => item !== listener)); }
/** Runs the listeners of this element and, for an event that bubbles, of its ancestors and the document. */
dispatchEvent(event: Ev): boolean {
event.target ??= this;
const root = this.root;
for (let at: El | null = this; at && !event.stopped; at = event.bubbles === false ? null : at.parentElement) {
event.currentTarget = at;
for (const listener of [...(at.listeners.get(event.type) ?? [])]) listener(event);
}
if (!event.stopped && event.bubbles !== false) root.doc?.emit(event.type, event);
return !event.defaultPrevented;
}
click() { this.dispatchEvent(new Ev('click', { bubbles: true })); }
focus() { const doc = this.root.doc; if (doc) doc.activeElement = this; }
select() {}
scrollIntoView() {}
scrollBy(options: { top: number }) { this.scrollTop = Math.max(0, this.scrollTop + options.top); }
getBoundingClientRect() { return this.rect; }
}
type Child = El | string | null | false | undefined;
/** `h('div', { 'data-testid': 'dm-container' }, h('span', null, 'Name'))`. */
export function h(tag: string, attrs?: Record<string, string> | null, ...children: Child[]): El {
const el = new El(tag);
for (const [name, value] of Object.entries(attrs ?? {})) el.setAttribute(name, value);
el.append(...children.filter((child): child is El | string => !!child));
return el;
}
/** Shorthand for X's test ids: `t('dm-inbox-panel', ...)` is `<div data-testid="dm-inbox-panel">`. */
export const t = (testid: string, ...children: Child[]) => h('div', { 'data-testid': testid }, ...children);
/**
* Puts `<html><body>…children</body></html>` behind an `areaPage` document: its queries and the
* elements it creates are this DOM's (a `<style>` stays the page's own, so `env.live()` still
* lists the stylesheets), and an event that bubbles out of the tree reaches the document's
* listeners. Returns the body.
*/
export function mount(doc: Record<string, unknown>, ...children: Child[]): El {
const body = h('body', null, ...children);
const html = h('html', { lang: 'en' }, h('head'), body);
html.doc = doc as unknown as DocLike;
const createStyle = (doc.createElement as (tag: string) => unknown).bind(doc);
Object.assign(doc, {
body,
querySelectorAll: (selector: string) => html.querySelectorAll(selector),
querySelector: (selector: string) => html.querySelector(selector),
createElement: (tag: string) => (tag === 'style' ? createStyle(tag) : new El(tag)),
createElementNS: (_: string, tag: string) => new El(tag),
});
return body;
}

351
apps/desktop/tests/fixtures/x/chat/x.ts vendored Normal file
View file

@ -0,0 +1,351 @@
// X's chat for tests/area-chat.test.ts, built from the live survey of /i/chat (structure only): the
// test ids, the virtualized message list (a window of rows in the DOM out of a longer loaded
// history), the record X's page keeps per row (obfuscated field names, a sequence object with a
// bigint `num` whose class has `fromString`, a sender with `userId`), X's two ways to position on a
// message (the `scrollToMessage` window event; router state on opening a conversation) and its
// message menu (hover, "More", Reply). `FakeChat` plays X's part. Every name and every line of text
// is invented.
import { El, Ev, h, mount, t } from './miniDom.ts';
const EPOCH = 1288834974657n;
/** A sequence number for a message sent at `ms` (a snowflake: time in the high bits). */
export const seqAt = (ms: number, n = 0) => (((BigInt(ms) - EPOCH) << 22n) + BigInt(n)).toString();
/** X's sequence class: text is not accepted where an instance is expected. */
export class Seq {
num: bigint;
constructor(num: bigint) { this.num = num; }
static fromString(text: string) { return new Seq(BigInt(text)); }
}
export type Person = { id: string; name: string; handle?: string };
export type Msg = { uuid: string; seq: string; from: Person; text: string; own?: boolean; clock?: string; replyTo?: Msg; media?: 'video' | 'image' };
export type Conversation = { id: string; title: string; history: Msg[] };
export const ADA: Person = { id: '1001', name: 'Ada Quill', handle: 'adaquill' };
export const TOMAS: Person = { id: '1002', name: 'Tomas Reed', handle: 'tomasreed' };
export const MIRA: Person = { id: '1003', name: 'Mira Holt' };
export const ME: Person = { id: '1000', name: 'Sam Birch', handle: 'sambirch' };
const LINES = [
'Lunch at noon?', 'The ferry leaves at six', 'I found the blue notebook', 'Bring the spare cable', 'Rain again tomorrow', 'The demo went fine',
'Call me after the match', 'Two tickets left', 'The bakery closes early', 'Send the map later', 'We moved the desk', 'Coffee is on me',
];
const uuid = (n: number) => `${String(n).padStart(8, '0')}-aaaa-4bbb-8ccc-${String(n).padStart(12, '0')}`;
/** `count` messages, one a minute from `start`, by `people` in turn. */
export function history(count: number, people: Person[], start = Date.UTC(2026, 5, 1, 9, 0), offset = 0): Msg[] {
return Array.from({ length: count }, (_, index) => {
const from = people[index % people.length]!;
const n = offset + index;
return { uuid: uuid(n), seq: seqAt(start + index * 60000, n % 4096), from, own: from === ME, text: `${LINES[index % LINES.length]} (${n})`, clock: `09:${String(index % 60).padStart(2, '0')}` };
});
}
/** The record X's page keeps for a message, with build-specific field names. */
export function record(msg: Msg, readableUser = true): Record<string, unknown> {
return {
aaa_1: msg.uuid,
cfe_1: new Seq(BigInt(msg.seq)),
pfd_1: { userId: msg.from.id, user: readableUser ? { displayName: msg.from.name, screenName: msg.from.handle } : { zz_1: msg.from.name } },
bbb_1: { ccc_1: msg.text, ddd_1: 3 },
...(msg.replyTo ? { replyPreview: { id: msg.replyTo.uuid, sequenceNumber: new Seq(BigInt(msg.replyTo.seq)), senderId: msg.replyTo.from.id, senderIsCurrentUser: !!msg.replyTo.own, senderDisplayName: msg.replyTo.from.name, previewText: msg.replyTo.text } } : {}),
};
}
type Env = {
doc: Record<string, unknown> & { emit(type: string, event?: unknown): void };
win: Record<string, unknown> & { emit(type: string, event?: unknown): void; addEventListener(type: string, fn: (event: unknown) => void): void };
mutate(): void;
};
export type ChatOptions = {
/** Rows X keeps in the DOM at once (default 8). */
rows?: number;
/** X's page exposes its loaded list in a prop above the rows (default true). */
exposeList?: boolean;
/** The sender's name and handle are readable in X's record (default true). */
readableUser?: boolean;
/** The menu's items carry icons (default true). */
icons?: boolean;
/** "More" opens on press only, not on a bare click (default false). */
menuOnPress?: boolean;
/** ms X needs to render after a jump (default 500). */
delay?: number;
/**
* X's chat inside the open shadow root of its embed host (XChatEmbed, seen live 2026-10-01): the
* page's own queries see only the host; everything else is in `host.shadowRoot` (default false).
*/
embed?: boolean;
};
/** A clock for the page: `tick(ms)` runs the timers that come due, letting promises settle in between. */
export function fakeClock(win: Record<string, unknown>) {
let now = 0;
let next = 1;
const pending = new Map<number, { at: number; fn: () => void }>();
win.setTimeout = (fn: () => void, ms = 0) => { const id = next++; pending.set(id, { at: now + ms, fn }); return id; };
win.clearTimeout = (id: number) => { pending.delete(id); };
const settle = () => new Promise<void>(resolve => setImmediate(resolve));
return {
pending: () => pending.size,
async tick(ms: number) {
const end = now + ms;
for (;;) {
await settle();
const due = [...pending].filter(([, timer]) => timer.at <= end).sort((a, b) => a[1].at - b[1].at)[0];
if (!due) break;
pending.delete(due[0]);
now = due[1].at;
due[1].fn();
}
now = end;
await settle();
},
};
}
export class FakeChat {
env: Env;
options: Required<ChatOptions>;
conversations = new Map<string, Conversation>();
clock: ReturnType<typeof fakeClock>;
body: El;
inboxList: El;
panelSlot: El;
/** The open conversation and the window of its history that is in the DOM. */
open: Conversation | null = null;
first = 0;
/** Index of the oldest message X has loaded (its local list). */
loadedFrom = 0;
/** What X's own UI did, to check what Chat+ caused. */
replyingTo: string | null = null;
sent = 0;
deleted: string[] = [];
forwarded: string[] = [];
reactions = 0;
jumps: string[] = [];
opened: { id: string; at: string | null }[] = [];
linkClicks: string[] = [];
constructor(env: Env, conversations: Conversation[], options: ChatOptions = {}) {
this.env = env;
this.options = { rows: 8, exposeList: true, readableUser: true, icons: true, menuOnPress: false, delay: 500, embed: false, ...options };
for (const conversation of conversations) this.conversations.set(conversation.id, conversation);
const { win } = env;
this.clock = fakeClock(win);
const location = win.location as { pathname: string; href: string };
Object.assign(win, {
innerWidth: 1280,
CustomEvent: Ev, PopStateEvent: Ev, MouseEvent: Ev, PointerEvent: Ev, KeyboardEvent: Ev, WheelEvent: Ev,
dispatchEvent: (event: Ev) => { win.emit(event.type, event); return true; },
history: {
state: null as unknown,
pushState(state: unknown, _title: string, url: string) { this.state = state; location.pathname = url; location.href = `https://x.com${url}`; },
},
});
this.inboxList = h('ul');
this.panelSlot = h('div');
const tree = t('dm-container',
t('dm-inbox-panel',
t('dm-inbox-header', t('dm-inbox-title', 'Chat'), h('button', { 'data-testid': 'dm-new-chat-button' })),
h('div', null, h('div', { 'data-testid': 'dm-conversation-scroller', role: 'listbox' }, this.inboxList))),
this.panelSlot);
if (this.options.embed) {
// The shadow root is not a child of its host: the page's querySelectorAll never reaches it.
const shadow = h('div', { 'data-fake-shadow-root': '' }, tree);
const host = h('div', { 'data-testid': 'xchatEmbedRoute' });
(host as unknown as { shadowRoot: El }).shadowRoot = shadow;
// Like a real shadow root: it knows its document, and composed events and focus reach the page.
Object.assign(shadow, { ownerDocument: env.doc });
shadow.doc = env.doc as unknown as El['doc'];
mount(env.doc, host);
this.body = shadow;
} else this.body = mount(env.doc, tree);
this.renderInbox();
// X's router: a popstate reads the address and the state that came with it.
win.addEventListener('popstate', () => {
const match = /^\/i\/chat\/([^/]+)$/.exec(location.pathname);
const state = (win.history as { state: { state?: { scrollToMessage?: string } } | null }).state;
if (match) this.show(match[1]!, state?.state?.scrollToMessage ?? null);
else this.close();
});
// X's message search: positions the open conversation on a sequence object of its own class.
win.addEventListener('scrollToMessage', event => {
const sequence = (event as { detail?: { sequenceNumber?: unknown } }).detail?.sequenceNumber;
if (!this.open || !(sequence instanceof Seq)) return;
this.jumps.push(sequence.num.toString());
const at = this.open.history.findIndex(msg => msg.seq === sequence.num.toString());
if (at >= 0) (win.setTimeout as (fn: () => void, ms: number) => unknown)(() => this.position(at), this.options.delay);
});
}
renderInbox() {
this.inboxList.replaceChildren(...[...this.conversations.values()].map((conversation, index) => {
const link = h('a', { href: `/i/chat/${conversation.id}` }, h('span', { dir: 'auto' }, conversation.title), h('span', { dir: 'auto' }, 'A preview line'));
// X's own link: its router takes the address and tells the page.
link.addEventListener('click', () => {
this.linkClicks.push(conversation.id);
(this.env.win.history as { pushState(state: unknown, title: string, url: string): void }).pushState(null, '', `/i/chat/${conversation.id}`);
this.env.win.emit('popstate');
});
return h('li', { 'data-index': String(index) }, h('div', { role: 'option', 'data-testid': `dm-conversation-item-${conversation.id.replace('-', ':')}` }, link));
}));
}
/** X opens a conversation (from its list, or through its router), optionally positioned on a message. */
show(id: string, at: string | null) {
const conversation = this.conversations.get(id);
if (!conversation) return;
this.opened.push({ id, at });
this.open = conversation;
this.replyingTo = null;
const last = Math.max(0, conversation.history.length - this.options.rows);
this.loadedFrom = Math.max(0, conversation.history.length - this.options.rows * 3);
const target = at ? conversation.history.findIndex(msg => msg.seq === at) : -1;
this.panelSlot.replaceChildren(t('dm-conversation-panel', t('dm-conversation-content',
t('dm-conversation-header', h('button', { 'data-testid': 'dm-conversation-back-button' }), t('dm-conversation-username', conversation.title), t('dm-conversation-header-item')),
t('dm-message-list-container', h('div', { 'data-testid': 'dm-message-list', 'data-dm-message-list-scope': id },
h('div', { 'data-testid': 'dm-message-scroller', 'data-virtualizer': 'tanstack', role: 'log' }))),
t('dm-composer-container', h('form', { 'data-testid': 'dm-composer-form' },
h('button', { 'data-testid': 'dm-composer-attachment-button' }),
t('dm-composer-input-container', h('textarea', { 'data-testid': 'dm-composer-textarea', placeholder: 'Message' })),
this.sendButton())))));
const header = this.body.querySelector('[data-testid="dm-conversation-header"]')!;
header.rect = { left: 400, top: 0, right: 1200, bottom: 56, width: 800, height: 56 };
this.scroller.addEventListener('wheel', () => this.wheelUp());
this.first = last;
this.render();
if (target >= 0) this.position(target);
}
close() {
this.open = null;
this.panelSlot.replaceChildren();
this.env.mutate();
}
private sendButton() {
// X's Send carries no test id; nothing of Chat+ may ever click it.
const button = h('button', { 'aria-label': 'Send' });
button.addEventListener('click', () => { this.sent += 1; });
return button;
}
get scroller(): El { return this.body.querySelector('[data-testid="dm-message-scroller"]')!; }
get composer(): El { return this.body.querySelector('[data-testid="dm-composer-container"]')!; }
get textarea(): El { return this.body.querySelector('[data-testid="dm-composer-textarea"]')!; }
row(id: string): El | null { return this.body.querySelector(`[data-dm-message-row="${id}"]`); }
rendered(): string[] { return this.scroller.querySelectorAll('[data-dm-message-row]').map(row => row.getAttribute('data-dm-message-row')!); }
/** X positions the list on message `at`: it loads what lies between and renders the rows around it. */
position(at: number) {
if (!this.open) return;
this.loadedFrom = Math.min(this.loadedFrom, Math.max(0, at - this.options.rows));
this.first = Math.max(0, Math.min(at - Math.floor(this.options.rows / 2), this.open.history.length - this.options.rows));
this.render();
}
/** A wheel step up: X loads a page of older history and shows older rows. */
wheelUp() {
if (!this.open) return;
const before = this.loadedFrom;
this.loadedFrom = Math.max(0, this.loadedFrom - this.options.rows);
this.first = Math.max(0, this.first - this.options.rows);
if (this.loadedFrom < before) this.scroller.scrollHeight += 1000;
this.render();
}
/** Renders the window: the virtualizer drops every row and makes new ones (nothing survives a scroll). */
render() {
const conversation = this.open!;
const scroller = this.scroller;
const group = conversation.id.startsWith('g');
const loaded = conversation.history.slice(this.loadedFrom);
const listFiber = { memoizedProps: this.options.exposeList ? { count: loaded.length, items: [{ kind: 'date' }, ...loaded.map(msg => ({ key: msg.uuid, messageItem: this.records.get(msg) ?? this.remember(msg) }))] } : { count: loaded.length }, return: null };
scroller.scrollHeight ||= 4000;
scroller.clientHeight = 800;
scroller.replaceChildren(
t('dm-message-list-spinner-slot'),
h('div', { 'data-index': '0' }, h('span', null, 'Monday')),
...conversation.history.slice(this.first, this.first + this.options.rows).map((msg, index) => this.rowOf(msg, index, group, listFiber)),
t('dm-message-list-composer-spacer'));
this.env.mutate();
}
private records = new Map<Msg, Record<string, unknown>>();
private remember(msg: Msg) { const made = record(msg, this.options.readableUser); this.records.set(msg, made); return made; }
private rowOf(msg: Msg, index: number, group: boolean, listFiber: unknown): El {
const id = `r${this.first + index}`;
const row = h('div', { 'data-dm-message-row': msg.uuid, 'data-testid': `message-${msg.uuid}`, 'data-send-status': 'sent', class: msg.own ? 'flex w-full justify-end' : 'flex w-full' },
group && !msg.own ? h('div', { 'aria-hidden': 'true' }, h('img', { src: 'https://pbs.twimg.com/profile_images/1/face_normal.jpg' })) : null,
h('span', { id: `${id}-sender`, class: 'sr-only' }, msg.own ? 'You' : msg.from.name),
h('span', { id: `${id}-time`, class: 'sr-only' }, msg.clock ?? '09:41'),
h('div', { role: 'article', tabindex: '-1', 'data-dm-message-focus-target': `${msg.uuid}:content`, 'aria-labelledby': `${id}-sender ${id}-time`, 'aria-describedby': `${id}-content` },
msg.replyTo ? h('div', null, h('svg', { 'data-icon': 'icon-reply-alt' }), h('div', null, msg.replyTo.text)) : null,
msg.media === 'video' ? h('video', { poster: 'https://ton.x.com/i/ton/data/xchat_media/c1/m1.jpg', src: 'blob:https://x.com/1' }) : null,
msg.media === 'image' ? h('img', { src: 'https://ton.x.com/i/ton/data/xchat_media/c1/p1.jpg' }) : null,
msg.text ? h('div', { id: `${id}-content`, 'data-testid': `message-text-${msg.uuid}` }, h('span', { dir: 'auto' }, msg.text), h('span', null, msg.clock ?? '09:41')) : null));
// React's handle on the row: its own props two components up, X's loaded list further up.
(row as unknown as Record<string, unknown>)['__reactFiber$k3y'] = {
memoizedProps: { className: 'flex' },
return: { memoizedProps: {}, return: { memoizedProps: { messageItem: this.records.get(msg) ?? this.remember(msg), isOneToOne: !group }, return: { memoizedProps: {}, return: listFiber } } },
};
// X shows a row's buttons while the pointer is over it.
row.addEventListener('mouseover', () => this.hover(msg, row));
return h('div', { 'data-index': String(index + 1) }, h('div', null, h('div', null, row)));
}
private hover(msg: Msg, row: El) {
if (row.querySelector(`[data-testid="message-overflow-button-${msg.uuid}"]`)) return;
const react = h('button', { 'data-testid': `message-reaction-button-${msg.uuid}`, 'aria-label': 'React' });
react.addEventListener('click', () => { this.reactions += 1; });
const more = h('button', { 'data-testid': `message-overflow-button-${msg.uuid}`, 'aria-label': 'More' });
let pressed = false;
more.addEventListener('pointerdown', () => { pressed = true; });
more.addEventListener('mousedown', () => { if (this.options.menuOnPress && pressed) this.menu(msg); });
more.addEventListener('click', () => { if (!this.options.menuOnPress) this.menu(msg); pressed = false; });
row.append(h('div', { class: 'flex items-center' }, react, more));
this.env.mutate();
}
private menu(msg: Msg) {
const item = (label: string, iconName: string, run: () => void) => {
const node = h('div', { role: 'menuitem', 'data-slot': 'xds-menu-item' }, this.options.icons ? h('svg', { 'data-icon': iconName }) : null, label);
node.addEventListener('click', () => { menu.remove(); run(); this.env.mutate(); });
return node;
};
const menu = h('div', { role: 'menu', 'data-slot': 'xds-menu-content' },
item('Reply', 'icon-reply', () => this.reply(msg)),
item('Forward', 'icon-forward', () => { this.forwarded.push(msg.uuid); }),
item('Copy message text', 'icon-copy', () => {}),
item('Info', 'icon-info', () => {}),
item('Delete for me', 'icon-trash', () => { this.deleted.push(msg.uuid); }));
menu.addEventListener('keydown', event => { if (event.key === 'Escape') menu.remove(); });
this.body.append(menu);
this.env.mutate();
}
private reply(msg: Msg) {
this.replyingTo = msg.uuid;
const cancel = h('button', { 'aria-label': 'Cancel reply' });
const bar = h('div', null, h('span', null, msg.from.name), h('span', null, msg.text), cancel);
cancel.addEventListener('click', () => { this.replyingTo = null; bar.remove(); });
this.composer.insertBefore(bar, this.composer.firstElementChild);
}
}
/** Whatever Chat+ added to the page: its nodes and its attributes on X's elements. */
export function traces(body: El): string[] {
const out: string[] = [];
const walk = (el: El) => {
for (const child of el.children) {
if (/(^|\s)bt-chat-/.test(child.className)) out.push(`.${child.className}`);
for (const name of ['data-bt-chat-starred', 'data-bt-chat-dim', 'data-bt-chat-kw', 'data-bt-chat-kw-label', 'data-bt-chat-flash', 'data-bt-chat-button']) if (child.hasAttribute(name)) out.push(`[${name}]`);
walk(child);
}
};
walk(body);
return out;
}

View file

@ -0,0 +1,61 @@
{
"data": {
"user": {
"result": {
"__typename": "User",
"timeline": {
"timeline": {
"instructions": [
{ "type": "TimelineClearCache" },
{
"type": "TimelineAddEntries",
"entries": [
{
"entryId": "tweet-1000000000000000001",
"content": {
"entryType": "TimelineTimelineItem",
"__typename": "TimelineTimelineItem",
"itemContent": {
"itemType": "TimelineTweet",
"__typename": "TimelineTweet",
"tweet_results": {
"result": {
"__typename": "Tweet",
"rest_id": "1000000000000000001",
"core": { "user_results": { "result": { "__typename": "User", "core": { "screen_name": "someone" } } } },
"legacy": {
"id_str": "1000000000000000001",
"full_text": "An invented post with a video",
"extended_entities": {
"media": [
{
"id_str": "1000000000000000002",
"media_key": "7_1000000000000000002",
"type": "video",
"media_url_https": "https://pbs.twimg.com/amplify_video_thumb/1000000000000000002/img/invented.jpg",
"video_info": {
"variants": [
{ "content_type": "application/x-mpegURL", "url": "https://video.twimg.com/amplify_video/1000000000000000002/pl/invented.m3u8" },
{ "bitrate": 832000, "content_type": "video/mp4", "url": "https://video.twimg.com/amplify_video/1000000000000000002/vid/avc1/640x360/low.mp4" },
{ "bitrate": 2176000, "content_type": "video/mp4", "url": "https://video.twimg.com/amplify_video/1000000000000000002/vid/avc1/1280x720/high.mp4" }
]
}
}
]
}
}
}
}
}
}
}
]
}
],
"metadata": {}
}
}
}
}
}
}

View file

@ -0,0 +1,224 @@
// A small DOM for tests/area-profile.test.ts: enough of Element (attributes, children, text,
// `closest`, `querySelectorAll` over the selectors Profile+ uses, listeners, `cloneNode`) to run the
// Profile+ modules and the old User Notes plugin over X-shaped markup without a browser, plus the
// pieces of X a profile feature reads (./x.ts builds them). Not a DOM library: it implements only
// what these tests use, and a selector it does not understand throws.
type Simple = { tag?: string; id?: string; classes: string[]; attrs: { name: string; op?: string; value?: string }[]; nots: Simple[] };
type Step = { simple: Simple; child: boolean };
/** Splits at top-level separators (not inside brackets, parentheses or quotes). */
function split(source: string, separators: string): { part: string; by: string }[] {
const out: { part: string; by: string }[] = [];
let depth = 0;
let quote = '';
let part = '';
let by = '';
for (const char of source) {
if (quote) { part += char; if (char === quote) quote = ''; continue; }
if (char === '"' || char === "'") { quote = char; part += char; continue; }
if (char === '[' || char === '(') depth += 1;
if (char === ']' || char === ')') depth -= 1;
if (depth === 0 && separators.includes(char)) {
if (part) { out.push({ part, by }); part = ''; by = ''; }
if (char !== ' ') by = char;
continue;
}
part += char;
}
if (part) out.push({ part, by });
return out;
}
function parseSimple(source: string): Simple {
const simple: Simple = { classes: [], attrs: [], nots: [] };
let rest = source;
const take = (pattern: RegExp) => { const match = pattern.exec(rest); if (match) rest = rest.slice(match[0].length); return match; };
let match = take(/^[a-zA-Z][\w-]*/);
if (match) simple.tag = match[0].toLowerCase();
while (rest) {
if ((match = take(/^#([\w-]+)/))) simple.id = match[1];
else if ((match = take(/^\.([\w-]+)/))) simple.classes.push(match[1]!);
else if ((match = take(/^\[([\w-]+)(?:([\^$*]?=)"([^"]*)")?\]/))) simple.attrs.push({ name: match[1]!, op: match[2], value: match[3] });
else if ((match = take(/^:not\(([^()]*)\)/))) simple.nots.push(parseSimple(match[1]!));
else throw new Error(`miniDom: selector not supported: ${source}`);
}
return simple;
}
const parsed = new Map<string, Step[][]>();
function parse(selector: string): Step[][] {
let list = parsed.get(selector);
if (!list) {
list = split(selector, ',').map(({ part }) => split(part.trim(), ' >').map(step => ({ simple: parseSimple(step.part), child: step.by === '>' })));
parsed.set(selector, list);
}
return list;
}
function matchesSimple(el: El, simple: Simple): boolean {
if (simple.tag && el.tagName.toLowerCase() !== simple.tag) return false;
if (simple.id && el.getAttribute('id') !== simple.id) return false;
for (const name of simple.classes) if (!el.classList.contains(name)) return false;
for (const { name, op, value } of simple.attrs) {
const actual = el.getAttribute(name);
if (actual === null) return false;
if (op === '=' && actual !== value) return false;
if (op === '^=' && !actual.startsWith(value!)) return false;
if (op === '$=' && !actual.endsWith(value!)) return false;
if (op === '*=' && !actual.includes(value!)) return false;
}
return !simple.nots.some(not => matchesSimple(el, not));
}
function matchesSteps(el: El, steps: Step[], index = steps.length - 1): boolean {
const step = steps[index]!;
if (!matchesSimple(el, step.simple)) return false;
if (index === 0) return true;
if (step.child) return !!el.parentElement && matchesSteps(el.parentElement, steps, index - 1);
for (let up = el.parentElement; up; up = up.parentElement) if (matchesSteps(up, steps, index - 1)) return true;
return false;
}
export class Txt {
text: string;
parentElement: El | null = null;
constructor(text: string) { this.text = text; }
}
type Listener = (event: Record<string, unknown>) => void;
const kebab = (key: string) => `data-${key.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`)}`;
export class El {
tagName: string;
parentElement: El | null = null;
childNodes: (El | Txt)[] = [];
tabIndex = -1;
value = '';
private attributes = new Map<string, string>();
private listeners = new Map<string, Listener[]>();
constructor(tag: string) { this.tagName = tag.toUpperCase(); }
get nodeName() { return this.tagName; }
get children(): El[] { return this.childNodes.filter((node): node is El => node instanceof El); }
get firstElementChild(): El | null { return this.children[0] ?? null; }
get previousElementSibling(): El | null { const all = this.parentElement?.children ?? []; return all[all.indexOf(this) - 1] ?? null; }
get nextElementSibling(): El | null { const all = this.parentElement?.children ?? []; return all[all.indexOf(this) + 1] ?? null; }
get isConnected(): boolean { for (let up: El | null = this; up; up = up.parentElement) if (up.tagName === 'HTML') return true; return false; }
getAttribute(name: string) { return this.attributes.get(name) ?? null; }
hasAttribute(name: string) { return this.attributes.has(name); }
setAttribute(name: string, value: string) { this.attributes.set(name, String(value)); }
removeAttribute(name: string) { this.attributes.delete(name); }
get className() { return this.getAttribute('class') ?? ''; }
set className(value: string) { this.setAttribute('class', value); }
get classList() {
const names = () => this.className.split(/\s+/).filter(Boolean);
return {
contains: (name: string) => names().includes(name),
add: (name: string) => { if (!names().includes(name)) this.className = [...names(), name].join(' '); },
remove: (name: string) => { this.className = names().filter(item => item !== name).join(' '); },
};
}
get dataset(): Record<string, string> {
return new Proxy({}, {
get: (_, key) => this.getAttribute(kebab(String(key))) ?? undefined,
set: (_, key, value) => { this.setAttribute(kebab(String(key)), String(value)); return true; },
}) as Record<string, string>;
}
get textContent(): string { return this.childNodes.map(node => (node instanceof El ? node.textContent : node.text)).join(''); }
set textContent(value: string) {
for (const node of this.childNodes) node.parentElement = null;
this.childNodes = [];
if (value !== '') this.append(String(value));
}
private adopt(nodes: (El | Txt | string)[]): (El | Txt)[] {
return nodes.map(node => {
const own = typeof node === 'string' ? new Txt(node) : node;
if (own instanceof El) own.remove();
return own;
});
}
private insert(at: number, nodes: (El | Txt)[]) {
this.childNodes.splice(at, 0, ...nodes);
for (const node of nodes) node.parentElement = this;
}
append(...nodes: (El | Txt | string)[]) { this.insert(this.childNodes.length, this.adopt(nodes)); }
appendChild<T extends El>(node: T): T { this.append(node); return node; }
after(...nodes: (El | Txt | string)[]) { const own = this.adopt(nodes); this.parentElement!.insert(this.parentElement!.childNodes.indexOf(this) + 1, own); }
before(...nodes: (El | Txt | string)[]) { const own = this.adopt(nodes); this.parentElement!.insert(this.parentElement!.childNodes.indexOf(this), own); }
remove() {
const parent = this.parentElement;
if (!parent) return;
parent.childNodes.splice(parent.childNodes.indexOf(this), 1);
this.parentElement = null;
}
cloneNode(deep = false): El {
const copy = new El(this.tagName);
for (const [name, value] of this.attributes) copy.setAttribute(name, value);
if (deep) copy.append(...this.childNodes.map(node => (node instanceof El ? node.cloneNode(true) : node.text)));
return copy;
}
matches(selector: string): boolean { return parse(selector).some(steps => matchesSteps(this, steps)); }
closest(selector: string): El | null {
for (let up: El | null = this; up; up = up.parentElement) if (up.matches(selector)) return up;
return null;
}
querySelectorAll(selector: string): El[] {
const out: El[] = [];
const walk = (el: El) => { for (const child of el.children) { if (child.matches(selector)) out.push(child); walk(child); } };
walk(this);
return out;
}
querySelector(selector: string): El | null { return this.querySelectorAll(selector)[0] ?? null; }
addEventListener(type: string, listener: Listener) { (this.listeners.get(type) ?? this.listeners.set(type, []).get(type)!).push(listener); }
removeEventListener(type: string, listener: Listener) { this.listeners.set(type, (this.listeners.get(type) ?? []).filter(item => item !== listener)); }
/** Calls this element's own listeners (no bubbling). */
emit(type: string, event: Record<string, unknown> = {}) {
for (const listener of [...(this.listeners.get(type) ?? [])]) listener({ type, target: this, preventDefault() {}, stopPropagation() {}, ...event });
}
click() { this.emit('click'); }
/** The markup, for comparing two trees. */
get outerHTML(): string {
const attrs = [...this.attributes].map(([name, value]) => ` ${name}="${value.replace(/"/g, '&quot;')}"`).join('');
const tag = this.tagName.toLowerCase();
return `<${tag}${attrs}>${this.childNodes.map(node => (node instanceof El ? node.outerHTML : node.text)).join('')}</${tag}>`;
}
}
type Child = El | string | null | false | undefined;
/** `h('div', { 'data-testid': 'UserName' }, h('span', null, 'Name'))`. */
export function h(tag: string, attrs?: Record<string, string> | null, ...children: Child[]): El {
const el = new El(tag);
for (const [name, value] of Object.entries(attrs ?? {})) el.setAttribute(name, value);
el.append(...children.filter((child): child is El | string => !!child));
return el;
}
/** Shorthand for X's test ids: `t('UserName', ...)` is `<div data-testid="UserName">`. */
export const t = (testid: string, ...children: Child[]) => h('div', { 'data-testid': testid }, ...children);
/**
* Puts `<html><body>…children</body></html>` behind an `areaPage` document: its queries and the
* elements it creates are this DOM's (a `<style>` stays the page's own, so `env.live()` still
* lists the stylesheets). Returns the body.
*/
export function mount(doc: Record<string, unknown>, ...children: Child[]): El {
const body = h('body', null, ...children);
const html = h('html', { lang: 'en' }, h('head'), body);
const createStyle = (doc.createElement as (tag: string) => unknown).bind(doc);
Object.assign(doc, {
body,
querySelectorAll: (selector: string) => html.querySelectorAll(selector),
querySelector: (selector: string) => html.querySelector(selector),
createElement: (tag: string) => (tag === 'style' ? createStyle(tag) : new El(tag)),
});
return body;
}

View file

@ -0,0 +1,73 @@
// The parts of X a Profile+ feature reads, shaped like X's own markup (the same nesting and test
// ids as scripts/visual-bugs/x-fixture.mjs; class names left out): a profile header, a post in a
// timeline, an opened post, a quoted post, a hover card and a profile's ··· menu. Invented
// accounts only. Not a live X test.
import { h, t, type El } from './miniDom.ts';
export type Account = { name: string; handle: string; bio?: string; joined?: string };
const words = (value: string) => h('div', { dir: 'ltr' }, h('span', null, value));
const nameText = (name: string) => h('div', { dir: 'ltr' }, h('span', null, h('span', null, name)));
/** `<div data-testid="primaryColumn">` with a profile header: name row, handle row, bio, "Joined …", the ··· button. */
export function profile({ name, handle, bio = 'Invented bio.', joined = 'Joined June 2015' }: Account): El {
return t('primaryColumn',
h('div', null,
h('button', { 'data-testid': 'userActions', 'aria-label': 'More' }),
h('div', null,
t('UserName', h('div', null, h('div', null,
h('div', { class: 'name-row' }, nameText(name)),
h('div', { class: 'handle-row' }, h('div', null, words(`@${handle}`)), t('userFollowIndicator', h('span', null, 'Follows you'))),
))),
bio ? t('UserDescription', h('span', null, bio)) : null,
t('UserProfileHeader_Items', joined ? h('span', { 'data-testid': 'UserJoinDate' }, h('span', null, joined)) : null),
),
),
);
}
/** A post's name block in a timeline: the name and the "@handle · time" in one row. */
export function userName({ name, handle }: Account): El {
return t('User-Name',
h('div', { class: 'name-row' }, h('div', null, h('a', { href: `/${handle}`, role: 'link' }, h('div', null, nameText(name))))),
h('div', { class: 'handle-row' }, h('div', null,
h('div', null, h('a', { href: `/${handle}`, role: 'link', tabindex: '-1' }, words(`@${handle}`))),
h('div', { dir: 'ltr', 'aria-hidden': 'true' }, h('span', null, '·')),
h('div', null, h('a', { href: `/${handle}/status/1` }, h('time', { datetime: '2026-09-28T10:00:00.000Z' }, '2h'))),
)),
);
}
export function post(account: Account, body = 'Invented post.', quoted?: Account): El {
return t('cellInnerDiv', h('article', { 'data-testid': 'tweet', role: 'article' },
userName(account),
t('tweetText', h('span', null, body)),
quoted ? h('div', { role: 'link', tabindex: '0' },
// A quoted post names its author without links.
t('User-Name', h('div', { class: 'name-row' }, nameText(quoted.name)), h('div', { class: 'handle-row' }, h('div', null, h('div', null, words(`@${quoted.handle}`))))),
) : null,
));
}
/** X's hover card as it is in `#layers`: avatar, name link, handle link, bio, counts. */
export function hoverCard({ name, handle, bio = 'Invented bio.' }: Account): El {
return t('hoverCardParent', t('HoverCard',
t(`UserAvatar-Container-${handle}`),
h('div', null,
h('div', { class: 'name-row' }, h('a', { href: `/${handle}`, role: 'link' }, h('div', null, nameText(name)))),
h('div', { class: 'handle-row' }, h('a', { href: `/${handle}`, role: 'link', tabindex: '-1' }, words(`@${handle}`))),
),
bio ? t('UserDescription', h('span', null, bio)) : null,
h('div', null, h('a', { href: `/${handle}/following` }, h('span', null, '30'), h('span', null, ' Following'))),
));
}
/** The menu X opens for a profile's ··· button. */
export function profileMenu(handle: string): El[] {
return [
t('mask'),
h('div', { role: 'menu' }, h('div', { role: 'menuitem', 'data-testid': 'block', tabindex: '0' },
h('svg', null, h('path', { d: 'M0 0' }), h('path', { d: 'M1 1' })), h('div', null, h('span', null, `Block @${handle}`)))),
];
}