// Theme invariants (theme engine v3, plan 6.2): a pure measurement over a rendered document that // says, with numbers, whether a theme is correct. Runs in the page (fixtures over Playwright, the live // app over CDP); it never changes the page. Grew out of verification/theme-qa/probe.js. // // I1 text contrast 4.5:1 (3:1 large; 7:1 with access high) of ink over its painted backdrop // I1b non-text contrast icons (svg, masked glyphs) 3:1 over their backdrop // I2 X colour leakage computed colours equal to an X palette key that is not whitelisted // I3 truncated labels in bar, nav, tabs, buttons and menu rows (X's own long-name truncation allowed) // I4 bar geometry a horizontal bar: equal item widths with labels, gaps <= one item, hit-testable // I5 icon provenance in pack contexts: X's own line paths still visible (xLine) // I6 empty frames a framed box (edge or own fill) with nothing rendered inside, taller than 2px // I7 frame nesting a framed box holding another framed box of the same fill, or a whole column // I8 popover bounds menus, hover cards, dropdowns, tooltips inside the viewport minus the bar inset // I10 targets interactive elements in bar, action bar, composer >= 24px (44 with access high) // I9 (forced :hover) needs CDP and I11 is static; both live in the runners. export type InvariantId = 'I1' | 'I1b' | 'I2' | 'I3' | 'I4' | 'I5' | 'I6' | 'I7' | 'I8' | 'I10'; export const INVARIANT_IDS: InvariantId[] = ['I1', 'I1b', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7', 'I8', 'I10']; export type Rect = [number, number, number, number]; export interface Finding { id: InvariantId; sel: string; rect: Rect; detail: string; value?: number } export interface InvariantOptions { /** Colours (`rgb(r, g, b)`) that are X's own palette; default: X's light and dark neutrals and blue. */ xColourKeys?: string[]; /** The X default theme keeps X's colours: I2 off. */ exemptLeak?: boolean; /** The theme draws an icon pack: I5 on. */ pack?: boolean; access?: 'standard' | 'high'; /** Cap of findings kept per invariant (counts are always complete). */ keep?: number; } export interface InvariantReport { counts: Record; findings: Finding[]; /** Measured but not failures: text over images, X icons with no slot, X's allowed truncation. */ notes: { unknownBackdrop: number; allowedTruncation: number; bar: 'none' | 'rail' | 'horizontal'; barMode?: 'labels' | 'icons' }; viewport: [number, number]; } /** X's own palette (Default and Lights out neutrals, X blue): the leak list until the engine exports X_COLOUR_KEYS. */ export const DEFAULT_X_KEYS = ['rgb(29, 155, 240)', 'rgb(26, 140, 216)', 'rgb(15, 20, 25)', 'rgb(83, 100, 113)', 'rgb(113, 118, 123)', 'rgb(239, 243, 244)', 'rgb(247, 249, 249)', 'rgb(231, 233, 234)', 'rgb(22, 24, 28)', 'rgb(32, 35, 39)', 'rgb(47, 51, 54)', 'rgb(20, 20, 20)', 'rgb(207, 217, 222)', 'rgb(51, 54, 57)']; type RGBA = [number, number, number, number]; const parse = (c: string | null | undefined): RGBA | null => { const m = /rgba?\(([\d.]+),? ([\d.]+),? ([\d.]+)(?:,? ?\/? ?([\d.]+))?/.exec(c || ''); return m ? [+m[1], +m[2], +m[3], m[4] === undefined ? 1 : +m[4]] : null; }; const lum = ([r, g, b]: RGBA) => { const f = (v: number) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); }; export const contrast = (a: RGBA, b: RGBA) => { const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); }; const blend = (top: RGBA, under: RGBA): RGBA => top[3] >= 1 ? top : [0, 1, 2].map(i => top[i] * top[3] + under[i] * (1 - top[3])).concat(1) as RGBA; const css = (c: RGBA) => `rgb(${c.slice(0, 3).map(Math.round).join(', ')})`; export function runInvariants(doc: Document = document, opts: InvariantOptions = {}): InvariantReport { const win = doc.defaultView as Window & typeof globalThis; const vw = win.innerWidth, vh = win.innerHeight; const keep = opts.keep ?? 12; const high = opts.access === 'high'; const counts = Object.fromEntries(INVARIANT_IDS.map(id => [id, 0])) as Record; const findings: Finding[] = []; const notes: InvariantReport['notes'] = { unknownBackdrop: 0, allowedTruncation: 0, bar: 'none' }; const cs = (e: Element) => win.getComputedStyle(e); const up = (e: Element): Element | null => e.parentElement ?? ((e.parentNode as ShadowRoot | null)?.host ?? null); const rectOf = (e: Element): Rect => { const r = e.getBoundingClientRect(); return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)]; }; const sel = (el: Element) => { const p: string[] = []; for (let e: Element | null = el; e && e !== doc.body && p.length < 4; e = up(e)) { let s = e.tagName.toLowerCase(); const t = e.getAttribute('data-testid'); if (t) { p.unshift(`${s}[data-testid="${t}"]`); break; } if (e.id) { p.unshift(`#${e.id}`); break; } const c = [...e.classList].filter(c => !/^(css|r)-/.test(c)).slice(0, 2); if (c.length) s += '.' + c.join('.'); const role = e.getAttribute('role'); if (role) s += `[role="${role}"]`; p.unshift(s); } return p.join(' > '); }; const add = (id: InvariantId, el: Element, detail: string, value?: number) => { counts[id]++; if (findings.filter(f => f.id === id).length < keep) findings.push({ id, sel: sel(el), rect: rectOf(el), detail, ...(value === undefined ? {} : { value: +value.toFixed(2) }) }); }; const hidden = (el: Element) => { for (let e: Element | null = el; e; e = up(e)) { const s = cs(e); if (s.display === 'none' || s.visibility === 'hidden' || +s.opacity < 0.05) return true; } return false; }; const onScreen = (el: Element) => { const r = el.getBoundingClientRect(); return r.width > 1 && r.height > 1 && r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw; }; const visible = (el: Element) => onScreen(el) && !hidden(el); const pageColor = (): RGBA => { for (const e of [doc.body, doc.documentElement]) { const c = parse(cs(e).backgroundColor); if (c && c[3] > 0) return blend(c, [255, 255, 255, 1]); } return [255, 255, 255, 1]; }; /** The painted backdrop under an element: the alpha stack up to the first opaque fill; `image` when an image or gradient paints first. */ const backdrop = (el: Element): { color: RGBA; image: boolean } => { const stack: RGBA[] = []; for (let e: Element | null = el; e && e !== doc.documentElement; e = up(e)) { const s = cs(e); if (s.backgroundImage !== 'none' && !/^linear-gradient\(rgba?\([^)]*\), rgba?\([^)]*\)\)$/.test(s.backgroundImage)) return { color: pageColor(), image: true }; const c = parse(s.backgroundColor); if (c && c[3] > 0) { stack.push(c); if (c[3] >= 1) break; } } let base = pageColor(); for (const c of stack.reverse()) base = blend(c, base); return { color: base, image: false }; }; const ownText = (el: Element) => [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('').trim(); const all = [...doc.querySelectorAll('body *')]; // I1 text contrast, I2 leakage, I3 truncation const xKeys = new Set(opts.xColourKeys ?? DEFAULT_X_KEYS); const whitelisted = (el: Element) => !!el.closest('[data-testid="icon-verified"],[data-testid="unlike"],[data-testid="unretweet"],[data-testid="removeBookmark"],img,video,canvas'); const LABEL_CONTEXT = 'nav[role="navigation"], header[role="banner"], [role="tablist"], [role="tab"], button, [role="button"], [role="menuitem"], .dv-tab'; const ALLOWED_TRUNC = '[data-testid="User-Name"], [data-testid="trend"], .bt-exact-time, [data-testid="UserName"], [data-testid="UserCell"]'; for (const el of all) { if (el.closest('[aria-hidden="true"]') || el.closest('svg') && el.tagName.toLowerCase() !== 'svg') continue; const text = ownText(el); if (!visible(el)) continue; const st = cs(el); if (!opts.exemptLeak && !whitelisted(el)) { for (const prop of ['color', 'backgroundColor', 'borderTopColor', 'borderBottomColor', 'fill', 'outlineColor'] as const) { const v = st[prop] as string; if (!xKeys.has(v)) continue; if (prop.startsWith('border') && parseFloat(prop === 'borderTopColor' ? st.borderTopWidth : st.borderBottomWidth) === 0) continue; if (prop === 'outlineColor' && (st.outlineStyle === 'none' || parseFloat(st.outlineWidth) === 0)) continue; if (prop === 'color' && !text) continue; if (prop === 'fill' && el.tagName.toLowerCase() !== 'svg') continue; add('I2', el, `${v} as ${prop}`); break; } } if (!text) continue; const fg = parse(st.color); if (!fg) continue; const back = backdrop(el); if (back.image) notes.unknownBackdrop++; else { const size = parseFloat(st.fontSize), bold = +st.fontWeight >= 700; const large = size >= 24 || (size >= 18.66 && bold); const need = high ? (large ? 4.5 : 7) : (large ? 3 : 4.5); const ink = blend([fg[0], fg[1], fg[2], fg[3] * Math.min(1, +st.opacity || 1)], back.color); const r = contrast(ink, back.color); if (r < need - 0.005) add('I1', el, `${r.toFixed(2)}:1 < ${need} ${st.color} on ${css(back.color)} "${text.slice(0, 24)}"`, r); } if (el.closest(LABEL_CONTEXT)) { const cut = el.scrollWidth > el.clientWidth + 1 && (st.textOverflow === 'ellipsis' || st.overflow === 'hidden' || st.overflowX === 'hidden'); const barLabel = !!el.closest('nav[role="navigation"]') && st.textOverflow === 'ellipsis' && el.scrollWidth > el.clientWidth + 1; if (cut || barLabel) { if (el.closest(ALLOWED_TRUNC)) notes.allowedTruncation++; else add('I3', el, `"${text.slice(0, 24)}" shown in ${el.clientWidth}px of ${el.scrollWidth}px`, el.clientWidth); } } } // I1b non-text contrast and I5 icon provenance const PACK_CONTEXT = 'nav[role="navigation"], header[role="banner"], [role="group"], [data-testid="toolBar"], #layers [role="menu"], [data-testid="GrokDrawer"], [data-testid="DMDrawer"], [data-testid="app-bar-back"], [data-testid="app-bar-close"]'; for (const svg of doc.querySelectorAll('svg')) { if (!visible(svg) || svg.closest('[data-testid="icon-verified"]')) continue; const s = cs(svg); const painted = [...svg.querySelectorAll('path, circle, rect, polygon, ellipse, line')].filter(p => { const ps = cs(p); return +ps.opacity > 0.05 && (ps.fill !== 'none' || ps.stroke !== 'none'); }); const masked = (e: Element) => { const es = cs(e); return (es.maskImage && es.maskImage !== 'none') || ((es as unknown as Record).webkitMaskImage ?? 'none') !== 'none' || es.backgroundImage !== 'none'; }; if (opts.pack && svg.closest(PACK_CONTEXT) && painted.length && !masked(svg) && !(svg.parentElement && masked(svg.parentElement))) add('I5', svg, `X's own drawing in ${svg.closest(PACK_CONTEXT)!.tagName.toLowerCase()}`); const ink = parse(masked(svg) ? s.backgroundColor : s.color); if (!ink || ink[3] === 0 || (!painted.length && !masked(svg))) continue; const back = backdrop(svg.parentElement ?? svg); if (back.image) continue; const r = contrast(blend(ink, back.color), back.color); if (r < 3 - 0.005) add('I1b', svg, `${r.toFixed(2)}:1 < 3 icon ${css(ink)} on ${css(back.color)}`, r); } // I6 empty frames, I7 nesting const framed = (el: Element) => { const s = cs(el), r = el.getBoundingClientRect(); if (r.width < 60 || r.height <= 2 || s.position === 'fixed' && r.width >= vw - 2 && r.height >= vh - 2) return false; if (s.backgroundImage !== 'none') return false; const edge = ['Top', 'Right', 'Bottom', 'Left'].filter(k => parseFloat(s[`border${k}Width` as 'borderTopWidth']) > 0 && s[`border${k}Style` as 'borderTopStyle'] !== 'none' && (parse(s[`border${k}Color` as 'borderTopColor'])?.[3] ?? 0) > 0).length >= 3 || /inset/.test(s.boxShadow); const fill = parse(s.backgroundColor); const parent = up(el); const own = !!fill && fill[3] > 0.05 && !!parent && css(blend(fill, backdrop(parent).color)) !== css(backdrop(parent).color); return edge || own; }; const MEDIA = 'img, svg, video, canvas, input, textarea, select, iframe, [role="img"], [role="textbox"], [role="progressbar"]'; const frames: Element[] = []; for (const el of all) { if (!visible(el) || el.closest('#layers [data-testid="mask"]') || el.matches('[data-testid="mask"]')) continue; if (el.matches(MEDIA) || !framed(el)) continue; frames.push(el); const content = (el.textContent ?? '').trim() || [...el.querySelectorAll(MEDIA)].some(m => visible(m)) || [...el.querySelectorAll('*')].some(c => cs(c).backgroundImage !== 'none' && visible(c)); if (!content) add('I6', el, `empty frame ${rectOf(el)[2]}x${rectOf(el)[3]} ${cs(el).backgroundColor}`, rectOf(el)[3]); } for (const el of frames) { const fill = cs(el).backgroundColor; const column = el.querySelector('[data-testid="sidebarColumn"], [data-testid="primaryColumn"]'); // Controls (a bevelled button on a bevelled bar, a pill in a card) are not frames: only boxes nest wrongly. const inner = frames.find(f => f !== el && el.contains(f) && !f.closest('a, button, [role="button"], [role="link"], [role="tab"], input, label') && cs(f).backgroundColor === fill && parse(fill)![3] > 0 && /solid|inset/.test(cs(f).borderTopStyle + cs(f).boxShadow)); if (column) add('I7', el, `frame holds the ${column.getAttribute('data-testid')}`); else if (inner) add('I7', el, `frame holds a frame of the same fill (${sel(inner)})`); } // I4 bar geometry and I10 targets const nav = doc.querySelector('header[role="banner"] nav[role="navigation"]'); if (nav && visible(nav)) { const items = [...nav.children].filter(c => visible(c)) as HTMLElement[]; const rects = items.map(i => i.getBoundingClientRect()); const horizontal = rects.length > 2 && rects.every(r => Math.abs(r.top - rects[0].top) < 6); notes.bar = horizontal ? 'horizontal' : 'rail'; const labelled = items.some(i => [...i.querySelectorAll('span, div')].some(s => ownText(s) && visible(s))); if (horizontal) { notes.barMode = labelled ? 'labels' : 'icons'; const widths = rects.map(r => r.width); if (labelled && Math.max(...widths) - Math.min(...widths) > 2) add('I4', nav, `item widths ${Math.round(Math.min(...widths))}..${Math.round(Math.max(...widths))}px`, Math.max(...widths) - Math.min(...widths)); const sorted = [...rects].sort((a, b) => a.left - b.left); const unit = Math.max(...widths); for (let i = 1; i < sorted.length; i++) { const gap = sorted[i].left - sorted[i - 1].right; if (gap > unit + 1) { add('I4', nav, `gap ${Math.round(gap)}px > one item ${Math.round(unit)}px`, gap); break; } } } for (const [i, item] of items.entries()) { const r = rects[i]; const x = r.left + r.width / 2, y = r.top + r.height / 2; if (x < 0 || y < 0 || x > vw || y > vh) { add('I4', item, 'item off screen'); continue; } const hit = doc.elementFromPoint(x, y); // A layer over the bar (modal mask, menu, hover card) is I8's business, not the bar's. if (hit?.closest('#layers')) continue; if (!hit || !(item === hit || item.contains(hit))) add('I4', item, `item not hit-testable (under ${hit ? sel(hit) : 'nothing'})`); } } const minTarget = high ? 44 : 24; const TARGET_CONTEXT = 'header[role="banner"] nav[role="navigation"] > *, header[role="banner"] [data-testid="SideNav_NewTweet_Button"], header[role="banner"] [data-testid="SideNav_AccountSwitcher_Button"], article [role="group"] button, article [role="group"] a, [data-testid="toolBar"] button, [data-testid="toolBar"] [role="button"]'; for (const el of doc.querySelectorAll(TARGET_CONTEXT)) { if (!visible(el)) continue; const r = el.getBoundingClientRect(); if (r.width < minTarget - 0.5 || r.height < minTarget - 0.5) add('I10', el, `${Math.round(r.width)}x${Math.round(r.height)} < ${minTarget}`, Math.min(r.width, r.height)); } // I8 popover bounds const root = cs(doc.documentElement); const insetTop = parseFloat(root.getPropertyValue('--wb-bar-inset-top')) || 0, insetBottom = parseFloat(root.getPropertyValue('--wb-bar-inset-bottom')) || 0; for (const el of doc.querySelectorAll('[data-testid="hoverCardParent"], [role="menu"], [role="listbox"], [data-testid="HoverCard"], [data-testid="Dropdown"], [data-testid="HoverLabel"]')) { if (!visible(el) && !(el.getBoundingClientRect().width > 1 && !hidden(el))) continue; if (el.parentElement?.closest('[data-testid="hoverCardParent"], [role="menu"], [data-testid="HoverCard"]')) continue; const r = el.getBoundingClientRect(); if (r.width < 2 || r.height < 2) continue; const out = Math.max(-r.left, -(r.top - insetTop), r.right - vw, r.bottom - (vh - insetBottom)); if (out > 1) add('I8', el, `${Math.round(out)}px outside the viewport`, out); } return { counts, findings, notes, viewport: [vw, vh] }; }