/** * The go-tell pilot check. * * Injected into a *copy* of go-tell's shell; go-tell's own source is untouched. * * go-tell is the hard case among the applications: before this it had no CSS custom * properties at all, 340 hardcoded hex values and 153 per-palette override rules. Its * stylesheet now reads from the shared contract, so the question this answers is * whether that rewrite actually works, and whether the two shipped controls can drive * an application whose mechanism is `html[data-theme="…"]` rather than a class or a * `--theme` variable. */ import { getContractState } from '/assets/components.js' const results = [] const check = (name, pass, detail = '') => results.push({ name, pass: !!pass, detail: String(detail) }) const root = document.documentElement const state = getContractState() /** The computed background of the page, which is what a palette actually looks like. */ const pageBg = () => getComputedStyle(document.body).backgroundColor /** The computed colour of a heading, which proves the text role resolved. */ const headingColour = () => { const el = document.querySelector('.brand, h1, .header') return el ? getComputedStyle(el).color : '' } /** * Waits past this application's own CSS transitions. * * go-tell transitions `background` over 300ms, so reading a computed background sooner * than that returns a value mid-animation and makes a working palette switch look * broken. The wait is deliberately longer than the longest transition in the * stylesheet rather than merely "long enough today". */ async function settle() { await new Promise((r) => setTimeout(r, 450)) } async function run() { await new Promise((r) => setTimeout(r, 600)) // the selector's fetch const selector = document.querySelector('bananas-theme-selector') const toggle = document.querySelector('bananas-appearance-toggle') const shadow = (el) => /** @type {ShadowRoot} */ (el.shadowRoot) check('the controls upgraded', !!selector && !!toggle && !!shadow(selector) && !!shadow(toggle)) if (!selector || !toggle) return finish() const buttons = [...shadow(selector).querySelectorAll('button')] const labels = buttons.map((b) => b.textContent.trim()) check('palettes are listed from the service', buttons.length > 1, labels.join(', ')) // go-tell's own four palettes must all be offered, because those are the slugs its // stylesheet and its rules key on. for (const name of ['Cyberpunk', 'Nordic Moss', 'Deep Space Obsidian', 'Terminal Retro']) { check(`go-tell's "${name}" is offered`, labels.some((l) => l.includes(name))) } // The decisive one: the contract must reach go-tell's page, which no longer // defines a single custom property of its own. const surface = getComputedStyle(document.body).getPropertyValue('--bn-surface').trim() // Diagnostics, because "the contract did not reach the page" has three very // different causes and the symptom alone cannot tell them apart. const link = document.querySelector('link[data-theme-contract]') const linkHref = link ? link.href : '(no link element)' let linkLoaded = 'unknown' if (link && link.sheet) { try { linkLoaded = String(link.sheet.cssRules.length) + ' rules' } catch { // A cross-origin sheet is unreadable by design. Reaching this branch means the // stylesheet loaded and the browser is refusing to expose its rules, which is // the healthy case for a cross-origin link. linkLoaded = 'loaded (cross-origin, rules not readable)' } } else if (link) { linkLoaded = 'not loaded' } check('the contract link is present', !!link, linkHref) check('the contract stylesheet loaded', linkLoaded !== 'not loaded', linkLoaded) check('the contract reaches go-tell', surface !== '', 'href=' + linkHref + ' loaded=' + linkLoaded + ' data-theme=' + root.getAttribute('data-theme') + ' data-mode=' + root.getAttribute('data-mode')) check('--bn-surface has a real value', surface !== '' && surface !== 'rgba(0, 0, 0, 0)', surface) // Switching a palette must move the page, through go-tell's own rules. let startBg = pageBg() const startText = headingColour() for (const name of ['Nordic Moss', 'Deep Space Obsidian', 'Terminal Retro', 'Cyberpunk']) { const button = buttons.find((b) => b.textContent.trim().includes(name)) if (!button) { check(`selecting ${name}`, false, 'button not found') continue } // The loop returns to Cyberpunk at the end, which is where it started. Selecting // the palette that is already showing cannot change anything, so the "did the // page move" assertion does not apply to that step. const alreadyShowing = buttons.find((b) => b.getAttribute('aria-checked') === 'true') const wasThis = alreadyShowing && alreadyShowing.textContent.trim().includes(name) button.click() await settle() const slug = root.getAttribute('data-theme') const contractBg = getComputedStyle(document.body).getPropertyValue('--bn-bg').trim() check(`${name} writes data-theme`, !!slug, slug) if (wasThis) { check(`${name} is a no-op when already showing`, pageBg() === startBg, pageBg()) } else { check(`${name} changes the page`, pageBg() !== startBg || headingColour() !== startText, `slug=${slug} mode=${root.getAttribute('data-mode')} bodyBg=${pageBg()} --bn-bg=${contractBg} text=${headingColour()}`) } startBg = pageBg() } // The terminal palette forces a monospace face with !important on every element. // Shadow DOM is supposed to make that impossible to reach the controls in; if the // controls inherit the terminal face here, the isolation has failed. const terminal = buttons.find((b) => b.textContent.trim().includes('Terminal')) if (terminal) { terminal.click() await settle() const font = getComputedStyle(terminal).fontFamily check('the controls are not restyled by the terminal palette', !font.includes('Courier'), font) check('the app itself did adopt the monospace face', getComputedStyle(document.body).fontFamily.includes('Courier') || getComputedStyle(document.body).fontFamily.includes('monospace'), getComputedStyle(document.body).fontFamily) } // The appearance axis: go-tell had no light mode before this. The control writing // data-mode is what makes one possible. const toggleButtons = [...shadow(toggle).querySelectorAll('button')] check('the appearance toggle has three states', toggleButtons.length === 3, toggleButtons.map((b) => b.getAttribute('aria-label')).join(', ')) const light = toggleButtons.find((b) => b.getAttribute('aria-label') === 'Light') if (light) { light.click() await settle() check('light writes data-mode', root.getAttribute('data-mode') === 'light', root.getAttribute('data-mode')) } check('the contract store is reachable', state && typeof state.selectTheme === 'function') finish() } function finish() { const failed = results.filter((r) => !r.pass) const out = document.createElement('pre') out.id = 'pilot' out.textContent = failed.length ? 'PILOT FAIL ' + failed.length + '/' + results.length + ' :: ' + failed.map((f) => f.name + ' [' + f.detail + ']').join(' | ') : 'PILOT PASS ' + results.length + ' :: ' + results.map((f) => f.name).join(' | ') document.body.append(out) } run().catch((error) => { const out = document.createElement('pre') out.id = 'pilot' out.textContent = 'PILOT FAIL the check threw: ' + (error instanceof Error ? error.message : String(error)) document.body.append(out) })