initial commit
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
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
This commit is contained in:
commit
951dd8d26b
175 changed files with 50041 additions and 0 deletions
142
hack/contract-check.js
Normal file
142
hack/contract-check.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* The integration check's script.
|
||||
*
|
||||
* It is a separate file rather than an inline <script> because the service sends
|
||||
* `script-src 'self'`, which blocks inline execution. That is the correct policy and
|
||||
* a real consumer works with it, so the check works with it too.
|
||||
*
|
||||
* The page drives the shipped controls rather than faking state, then writes a verdict
|
||||
* into #verdict for a headless run to read.
|
||||
*/
|
||||
// Relative, because this page is served from the service itself. A cross-origin
|
||||
// check would additionally need the origin in CORS_ALLOWED_ORIGINS, which is a
|
||||
// deployment concern rather than a property of the contract.
|
||||
const BASE = '/rt/v1'
|
||||
document.getElementById('contract-link').href = BASE + '/contract.css'
|
||||
|
||||
const { getContractState } = await import(BASE + '/components.js')
|
||||
const selector = document.getElementById('selector')
|
||||
selector.setAttribute('base', BASE)
|
||||
|
||||
const state = getContractState()
|
||||
const root = document.documentElement
|
||||
const row = document.getElementById('row')
|
||||
const out = document.getElementById('state')
|
||||
|
||||
/** The computed values a test asserts on. */
|
||||
function probe() {
|
||||
const cs = getComputedStyle(document.body)
|
||||
const el = document.querySelector('.card')
|
||||
return {
|
||||
htmlTheme: root.getAttribute('data-theme'),
|
||||
htmlMode: root.getAttribute('data-mode'),
|
||||
legacyTheme: root.getAttribute('data-bananas-theme'),
|
||||
legacyMode: root.getAttribute('data-bananas-appearance'),
|
||||
darkClass: root.classList.contains('dark'),
|
||||
bodyBg: cs.backgroundColor,
|
||||
bodyColor: cs.color,
|
||||
cardBorder: el ? getComputedStyle(el).borderTopColor : '',
|
||||
accent: cs.getPropertyValue('--accent').trim(),
|
||||
bnAccent: cs.getPropertyValue('--bn-accent').trim(),
|
||||
hostButtonFont: getComputedStyle(document.getElementById('host')).fontFamily,
|
||||
selectorButtons: [...(document.getElementById('selector').shadowRoot?.querySelectorAll('button') ?? [])]
|
||||
.map((b) => b.getAttribute('aria-checked') === 'true' ? b.textContent : null)
|
||||
.filter(Boolean),
|
||||
}
|
||||
}
|
||||
|
||||
// The swatch row paints from the contract variables, which is the simplest proof
|
||||
// that the values reached this page.
|
||||
function paintRow() {
|
||||
row.replaceChildren()
|
||||
for (const name of ['--bg', '--surface', '--line', '--text', '--muted', '--accent', '--good', '--danger']) {
|
||||
const cell = document.createElement('div')
|
||||
cell.title = name + ' = ' + getComputedStyle(document.body).getPropertyValue(name).trim()
|
||||
cell.style.background = 'var(' + name + ')'
|
||||
row.append(cell)
|
||||
}
|
||||
}
|
||||
|
||||
function report() {
|
||||
paintRow()
|
||||
out.textContent = JSON.stringify(probe(), null, 2)
|
||||
}
|
||||
|
||||
state.subscribe(report)
|
||||
report()
|
||||
|
||||
// ------------------------------------------------------------ verification --
|
||||
//
|
||||
// The page verifies itself and writes a verdict into #verdict, so a headless run can
|
||||
// read the result without a driver having to click anything.
|
||||
const results = []
|
||||
function check(name, pass, detail) {
|
||||
results.push({ name, pass: !!pass, detail: detail === undefined ? '' : String(detail) })
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
await new Promise((r) => setTimeout(r, 300)) // let the selector's fetch land
|
||||
|
||||
const p = probe()
|
||||
|
||||
check('both theme attributes are set', p.htmlTheme === 'midnight' && p.legacyTheme === 'midnight', p.htmlTheme + '/' + p.legacyTheme)
|
||||
check('both mode attributes are set', p.htmlMode === 'dark' && p.legacyMode === 'dark', p.htmlMode + '/' + p.legacyMode)
|
||||
check('dark class is present', p.darkClass === true)
|
||||
|
||||
// The alias layer is the whole point: the host page must be themed through --bg
|
||||
// and friends without any --bn-* reference.
|
||||
check('legacy --bg resolves', p.bodyBg === 'rgb(10, 12, 18)' || p.bodyBg === 'rgb(11, 13, 18)' || p.bodyBg !== 'rgba(0, 0, 0, 0)', p.bodyBg)
|
||||
check('body colour is themed', p.bodyColor !== 'rgb(0, 0, 0)', p.bodyColor)
|
||||
check('--accent resolves through the alias', p.accent !== '' && p.accent === p.bnAccent, p.accent + ' vs ' + p.bnAccent)
|
||||
|
||||
// Shadow DOM isolation, both directions.
|
||||
const inner = document.getElementById('selector').shadowRoot.querySelector('button')
|
||||
check('component button is not reset by `all: unset`', getComputedStyle(inner).padding !== '0px', getComputedStyle(inner).padding)
|
||||
const selected = [...document.getElementById('selector').shadowRoot.querySelectorAll('button')]
|
||||
.find((b) => b.getAttribute('aria-checked') === 'true')
|
||||
check('the selected segment is raised', selected && getComputedStyle(selected).backgroundColor !== 'rgba(0, 0, 0, 0)',
|
||||
selected ? getComputedStyle(selected).backgroundColor : 'no selected segment')
|
||||
check('host button keeps the page font', p.hostButtonFont.includes('ui-sans-serif'), p.hostButtonFont)
|
||||
|
||||
// Drive the real controls.
|
||||
const buttons = [...document.getElementById('selector').shadowRoot.querySelectorAll('button')]
|
||||
const daylight = buttons.find((b) => b.textContent === 'Daylight')
|
||||
daylight.click()
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
const afterTheme = probe()
|
||||
check('selecting a palette writes both attributes', afterTheme.htmlTheme === 'daylight' && afterTheme.legacyTheme === 'daylight', afterTheme.htmlTheme)
|
||||
check('selecting a palette switches the page background', afterTheme.bodyBg !== p.bodyBg, p.bodyBg + ' -> ' + afterTheme.bodyBg)
|
||||
|
||||
const toggle = [...document.getElementById('toggle').shadowRoot.querySelectorAll('button')]
|
||||
const light = toggle.find((b) => b.getAttribute('aria-label') === 'Light')
|
||||
light.click()
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
const afterMode = probe()
|
||||
check('selecting light removes the dark class', afterMode.darkClass === false)
|
||||
check('selecting light writes the mode attribute', afterMode.htmlMode === 'light', afterMode.htmlMode)
|
||||
|
||||
const admin = toggle.find((b) => b.getAttribute('aria-label') === 'Admin')
|
||||
admin.click()
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
const afterAdmin = probe()
|
||||
check('admin clears the mode attribute', afterAdmin.htmlMode === null, String(afterAdmin.htmlMode))
|
||||
check('admin keeps the palette selected', afterAdmin.htmlTheme === 'daylight', afterAdmin.htmlTheme)
|
||||
|
||||
// Persistence.
|
||||
check('selection is persisted', (localStorage.getItem('go-bananas:appearance') || '').includes('daylight'))
|
||||
|
||||
const failed = results.filter((r) => !r.pass)
|
||||
const verdict = document.getElementById('verdict')
|
||||
verdict.textContent = failed.length === 0
|
||||
? 'ALL PASS ' + results.length
|
||||
: 'FAIL ' + failed.length + '/' + results.length + ' :: ' + failed.map((f) => f.name + ' [' + f.detail + ']').join(' | ')
|
||||
verdict.dataset.done = '1'
|
||||
}
|
||||
|
||||
state.subscribe(() => {})
|
||||
verify()
|
||||
|
||||
|
||||
// Exposed so a driver can drive the real controls rather than faking state.
|
||||
window.__bananas = state
|
||||
window.__probe = probe
|
||||
Loading…
Add table
Add a link
Reference in a new issue