// Theme invariants (waybetter runtime themeInvariants.ts) on hand-built DOM in Chromium: each invariant // fails on its smallest bad shape and passes on the good one. The shapes are the owner's reported bugs. import { test, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { build } from 'esbuild'; import { fileURLToPath } from 'node:url'; import { chromium, type Browser, type Page } from 'playwright'; const src = fileURLToPath(new URL('../../../waybetter/packages/runtime/src/themeInvariants.ts', import.meta.url)); let browser: Browser, page: Page, bundle: string; before(async () => { bundle = (await build({ entryPoints: [src], bundle: true, format: 'iife', globalName: 'WBInv', write: false, target: 'chrome120' })).outputFiles[0].text; browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined }); page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); }); after(async () => { await browser?.close(); }); async function run(body: string, opts: Record = {}) { await page.setContent(`${body}`); await page.addScriptTag({ content: bundle }); return page.evaluate(o => (window as unknown as { WBInv: { runInvariants: (d: Document, o: unknown) => { counts: Record; findings: { id: string; detail: string }[] } } }).WBInv.runInvariants(document, o), opts); } const BAR = (items: string) => `
`; test('I1: the "New" badge, navy on indigo, fails; white on black passes', async () => { const bad = await run('

New

'); assert.equal(bad.counts.I1, 1); assert.match(bad.findings[0].detail, /1\.1\d:1/); assert.equal((await run('

New

')).counts.I1, 0); }); test('I1: the HoverLabel, black on grey 82, fails; large text uses 3:1', async () => { assert.equal((await run('
More
')).counts.I1, 1); assert.equal((await run('

Big

')).counts.I1, 0); assert.equal((await run('

Big

', { access: 'high' })).counts.I1, 1); }); test('I1b: a faint icon fails', async () => { const svg = (c: string) => ``; assert.equal((await run(svg('rgb(238,238,238)'))).counts.I1b, 1); assert.equal((await run(svg('rgb(40,40,40)'))).counts.I1b, 0); }); test('I2: X blue left on a themed page fails, unless the theme is X default', async () => { const html = '
Follow
'; assert.equal((await run(html)).counts.I2, 1); assert.equal((await run(html, { exemptLeak: true })).counts.I2, 0); }); test('I3: a bar label cut to "H…" fails; a whole label passes; a long user name is allowed', async () => { const label = (w: number) => `Notifications`; assert.equal((await run(BAR(label(24)))).counts.I3, 1); assert.equal((await run(BAR(label(200)))).counts.I3, 0); const name = await run(''); assert.equal(name.counts.I3, 0); }); test('I4: a horizontal bar with uneven items fails; equal items pass', async () => { const item = (w: number, t: string) => `${t}`; assert.ok((await run(BAR(item(80, 'Home') + item(140, 'Explore') + item(80, 'Chat')))).counts.I4 >= 1); assert.equal((await run(BAR(item(100, 'Home') + item(100, 'Explore') + item(100, 'Chat')))).counts.I4, 0); }); test('I4: an item covered by another element fails the hit test', async () => { const item = (t: string) => `${t}`; const r = await run(`${BAR(item('Home') + item('Explore') + item('Chat'))}
`); assert.equal(r.counts.I4, 3); }); test('I5: X line icons in the rail fail under a pack; masked glyphs pass', async () => { const xIcon = ''; const packed = ''; assert.equal((await run(BAR(xIcon), { pack: true })).counts.I5, 1); assert.equal((await run(BAR(packed), { pack: true })).counts.I5, 0); assert.equal((await run(BAR(xIcon), { pack: false })).counts.I5, 0); }); test('I6: the empty grey sidebar box fails; the same box with content passes', async () => { assert.equal((await run('
')).counts.I6, 1); assert.equal((await run('
Who to follow
')).counts.I6, 0); }); test('I7: a frame around the whole sidebar column fails', async () => { const r = await run('
x
'); assert.equal(r.counts.I7, 1); }); test('I8: the account menu 52px past the right edge and a HoverCard cut at the bottom fail', async () => { const menu = await run('
Add an existing account
'); assert.equal(menu.counts.I8, 1); const card = await run('
Profile
'); assert.equal(card.counts.I8, 1); assert.equal((await run('
Menu
')).counts.I8, 0); }); test('I10: a 16px bar target fails; 44px is needed with access high', async () => { const item = (s: number) => `.`; assert.equal((await run(BAR(item(16) + item(16) + item(16)))).counts.I10, 3); assert.equal((await run(BAR(item(32) + item(32) + item(32)))).counts.I10, 0); assert.equal((await run(BAR(item(32) + item(32) + item(32)), { access: 'high' })).counts.I10, 3); });