go-bananas/hack/pilot-go-tell.js
Daniel Noulet 951dd8d26b
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
initial commit
2026-10-07 15:11:35 +02:00

167 lines
No EOL
7.4 KiB
JavaScript

/**
* 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)
})