/** * The pilot check's reporter. * * It is injected into a *copy* of go-loose's page; go-loose's own source is untouched. * * The question it answers is narrow and specific: does the component shipped by * go-bananas change the attributes that go-loose's own hand-written stylesheet already * keys on? If it does, go-loose is themed by the shared contract without a single edit * to its CSS, which is the entire claim of ADR 0005. */ import { getContractState } from '/rt/v1/components.js' const results = [] /** @param {string} name @param {boolean} pass @param {string} [detail] */ const check = (name, pass, detail = '') => results.push({ name, pass: !!pass, detail: String(detail) }) const root = document.documentElement const state = getContractState() /** The computed background of go-loose's own page surface. */ function pageBackground() { return getComputedStyle(document.body).backgroundColor } /** * The value go-loose's page sees for a contract custom property. * * This is the claim worth testing: go-loose's stylesheet never mentions --bn-* or * --surface being defined anywhere, yet reading the property off its own body proves * the contract reached the page through the one stylesheet link. */ function contractValueOnHost(name) { return getComputedStyle(document.body).getPropertyValue(name).trim() } /** The text colour go-loose's own CSS takes from --text. */ function pageText() { return getComputedStyle(document.body).color } async function run() { // The selector fetches its palette list from the service. The wait has to cover a // real cross-origin round trip, not the instant rejection that used to happen when // the service sent no CORS headers: that failure was fast enough to hide behind a // short settle time, and the wait was tuned to it. await new Promise((r) => setTimeout(r, 4000)) const selector = document.querySelector('bananas-theme-selector') const toggle = document.querySelector('bananas-appearance-toggle') const shadow = (el) => /** @type {ShadowRoot} */ (el.shadowRoot) check('the selector element upgraded', !!shadow(selector), selector ? selector.tagName : 'absent') check('the toggle element upgraded', !!shadow(toggle), toggle ? toggle.tagName : 'absent') 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-loose's four palettes must all be offered, because those are the slugs its own // CSS has blocks for. for (const slug of ['Tokyo Night', 'Catppuccin', 'Tomorrow', 'Kanagawa']) { check(`go-loose's "${slug}" is offered`, labels.includes(slug)) } // The contract stylesheet loaded and reached go-loose's page. const bgTokyo = pageBackground() check('go-loose\'s page is themed through the contract', bgTokyo !== 'rgba(0, 0, 0, 0)' && bgTokyo !== '', bgTokyo) // The decisive test: change the palette with the component, and confirm go-loose's // OWN stylesheet responded. Nothing in style.css was edited. const startBg = bgTokyo const startText = pageText() const kanagawa = buttons.find((b) => b.textContent.trim() === 'Kanagawa') if (!kanagawa) { check('the Kanagawa button was found', false, labels.join(', ')) } else { kanagawa.click() await new Promise((r) => setTimeout(r, 60)) check('selecting a palette writes data-theme', root.getAttribute('data-theme') === 'kanagawa', root.getAttribute('data-theme')) check('selecting a palette writes the legacy attribute', root.getAttribute('data-bananas-theme') === 'kanagawa', root.getAttribute('data-bananas-theme')) check("go-loose's own CSS followed the palette", pageBackground() !== startBg, startBg + ' -> ' + pageBackground()) // The alias layer reached a page that never referenced it. check('go-loose\'s page reads --surface from the contract', contractValueOnHost('--surface') !== '', contractValueOnHost('--surface')) check('go-loose\'s page reads --bn-surface from the contract', contractValueOnHost('--bn-surface') !== '', contractValueOnHost('--bn-surface')) check('the alias and the contract agree', contractValueOnHost('--surface') === contractValueOnHost('--bn-surface'), contractValueOnHost('--surface') + ' vs ' + contractValueOnHost('--bn-surface')) } // And the appearance axis, which go-loose has never had. 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 new Promise((r) => setTimeout(r, 60)) check('light writes data-mode', root.getAttribute('data-mode') === 'light', root.getAttribute('data-mode')) check("go-loose's own CSS followed the appearance", pageBackground() !== 'rgb(0, 0, 0)', pageBackground()) } // Isolation: go-loose has a broad `.card button { all: unset }`-style surface, and a // control that leaked would be reset by it. const first = shadow(selector).querySelector('button') const style = first ? getComputedStyle(first) : null check('the control is not reset by the host page', !!style && style.padding !== '0px', style ? style.padding : 'no button') check('the control is not inheriting the page font', !!style && !style.fontFamily.includes('DM Sans'), style ? style.fontFamily : '') // The store is reachable, so go-loose can read or set the selection itself. check('the contract store is reachable from the module', state && typeof state.selectTheme === 'function') const failed = results.filter((r) => !r.pass) const out = document.getElementById('pilot') || 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) })