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
94
hack/check-contract.sh
Executable file
94
hack/check-contract.sh
Executable file
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# End-to-end check of the shared styling contract and the two shipped controls.
|
||||
#
|
||||
# The unit tests cover the store and the components in isolation. This covers the
|
||||
# things only a browser can answer:
|
||||
#
|
||||
# - Does the rendered contract CSS actually resolve to the right computed values?
|
||||
# - Does the legacy alias layer reach a host page that never mentions --bn-*?
|
||||
# - Does Shadow DOM isolate the controls in both directions against a hostile rule?
|
||||
# - Do the controls, driven by a real click, move the attributes on <html>?
|
||||
#
|
||||
# The page verifies itself and writes a verdict into the DOM, so this needs only a
|
||||
# headless Chromium and no driver library.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK="$(mktemp -d)"
|
||||
PORT="${PORT:-8399}"
|
||||
CHROME="${CHROME:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || true)}"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then kill "$SERVER_PID" 2>/dev/null || true; fi
|
||||
rm -rf "$WORK"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -z "$CHROME" ]]; then
|
||||
echo "no chromium found; set CHROME to a headless browser binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> building the server and the SDK"
|
||||
(cd "$ROOT" && go build -o "$WORK/go-bananas" ./cmd/go-bananas)
|
||||
(cd "$ROOT/sdk" && npm run build >/dev/null)
|
||||
|
||||
# The check page is served from the SDK directory, so it is same-origin with the
|
||||
# service. A cross-origin run would additionally need its origin in
|
||||
# CORS_ALLOWED_ORIGINS, which is a deployment concern and not a property of the
|
||||
# contract.
|
||||
cp "$ROOT"/sdk/dist/*.js "$WORK/"
|
||||
cp "$ROOT/hack/integration-check.html" "$ROOT/hack/contract-check.js" "$WORK/"
|
||||
|
||||
echo "==> starting the service on :$PORT"
|
||||
DATABASE_PATH="$WORK/check.db" \
|
||||
BANANAS_ENVIRONMENT=development \
|
||||
BANANAS_AUTH_MODE=dev \
|
||||
BANANAS_PUBLIC_BASE_URL="http://localhost:$PORT" \
|
||||
BANANAS_HTTP_ADDR=":$PORT" \
|
||||
BANANAS_SDK_DIR="$WORK" \
|
||||
"$WORK/go-bananas" >"$WORK/server.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
if curl -fsS -m 2 "http://localhost:$PORT/healthz" >/dev/null 2>&1; then break; fi
|
||||
sleep 0.25
|
||||
done
|
||||
if ! curl -fsS -m 2 "http://localhost:$PORT/healthz" >/dev/null 2>&1; then
|
||||
echo "the service did not become healthy:" >&2
|
||||
cat "$WORK/server.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> driving the page"
|
||||
"$CHROME" \
|
||||
--headless \
|
||||
--no-sandbox \
|
||||
--disable-gpu \
|
||||
--virtual-time-budget=10000 \
|
||||
--dump-dom "http://localhost:$PORT/rt/v1/integration-check.html" \
|
||||
>"$WORK/dom.html" 2>/dev/null
|
||||
|
||||
verdict="$(python3 - "$WORK/dom.html" <<'PY'
|
||||
import re, sys
|
||||
dom = open(sys.argv[1]).read()
|
||||
m = re.search(r'id="verdict"[^>]*>(.*?)</div>', dom, re.S)
|
||||
print(m.group(1).strip() if m else "")
|
||||
PY
|
||||
)"
|
||||
|
||||
if [[ -z "$verdict" ]]; then
|
||||
echo "the page produced no verdict, so the check did not run." >&2
|
||||
echo "the script most likely threw; the server log follows." >&2
|
||||
cat "$WORK/server.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "$verdict"
|
||||
|
||||
if [[ "$verdict" != ALL\ PASS* ]]; then
|
||||
exit 1
|
||||
fi
|
||||
153
hack/check-pilot-go-loose.sh
Executable file
153
hack/check-pilot-go-loose.sh
Executable file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Pilot check: go-loose consuming the shared contract.
|
||||
#
|
||||
# This is the test that decides whether the consolidation is real. It loads go-loose's
|
||||
# *actual* index.html and its *actual* hand-written style.css, with only the theme
|
||||
# origin substituted exactly as go-loose's server does it, and then asks the question
|
||||
# that matters:
|
||||
#
|
||||
# Do the components shipped by go-bananas change the attributes that go-loose's own
|
||||
# CSS already keys on, so go-loose is themed without a single edit to its stylesheet?
|
||||
#
|
||||
# If that is true, adoption is one <link> and one <script> per page, which is what
|
||||
# ADR 0005 claims. If it is false, the contract is decoration.
|
||||
#
|
||||
# It does not boot go-loose: that needs a database and a first-run installer, and
|
||||
# neither is what is being tested. What is being tested is the page and the stylesheet.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOOSE="${GO_LOOSE_DIR:-/home/dnoulet/go/go-loose}"
|
||||
WORK="$(mktemp -d)"
|
||||
PORT="${PORT:-8398}"
|
||||
THEME_PORT="${THEME_PORT:-8397}"
|
||||
CHROME="${CHROME:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || true)}"
|
||||
|
||||
cleanup() {
|
||||
for pid in "${SERVER_PID:-}" "${THEME_PID:-}"; do
|
||||
[[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
echo "WORK=$WORK" >/tmp/pilot-work-dir; rm -rf "$WORK"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
[[ -d "$LOOSE/web" ]] || { echo "go-loose not found at $LOOSE; set GO_LOOSE_DIR" >&2; exit 1; }
|
||||
[[ -n "$CHROME" ]] || { echo "no chromium found; set CHROME" >&2; exit 1; }
|
||||
|
||||
echo "==> building go-bananas and the SDK"
|
||||
# The SDK is built first, then the server that embeds it. The other order compiles a
|
||||
# binary containing whatever sdk/dist happened to contain when the pilot last ran,
|
||||
# which 404s /rt/v1/components.js — and the check used not to notice, because it loaded
|
||||
# the module from its own copy instead of from the service.
|
||||
(cd "$ROOT/sdk" && npm run build >/dev/null)
|
||||
(cd "$ROOT" && go build -o "$WORK/go-bananas" ./cmd/go-bananas)
|
||||
|
||||
mkdir -p "$WORK/site" "$WORK/site/assets"
|
||||
|
||||
# go-loose's own files with the theme origin substituted the way its server does it.
|
||||
# This is the only transformation: nothing in its stylesheet or markup is edited.
|
||||
echo "==> assembling go-loose's page"
|
||||
THEME_ORIGIN="http://localhost:$THEME_PORT"
|
||||
export LOOSE_WEB="$LOOSE/web" SITE="$WORK/site" THEME_ORIGIN
|
||||
python3 - <<'PY'
|
||||
import os, re
|
||||
|
||||
src = os.environ['LOOSE_WEB']
|
||||
out = os.environ['SITE']
|
||||
origin = os.environ['THEME_ORIGIN']
|
||||
|
||||
html = open(os.path.join(src, 'index.html')).read().replace('__GO_LOOSE_THEME_ORIGIN__', origin)
|
||||
|
||||
# Nothing is rewritten to a local copy any more. The module used to be pointed at a
|
||||
# copy under /assets/ so that the check would not depend on CORS being configured, and
|
||||
# that is exactly what hid the bug this now covers: the service sent no CORS headers,
|
||||
# so in a real deployment the ES module could not be fetched and the control rendered
|
||||
# empty, while the pilot passed against its own copy. A module <script> is always
|
||||
# fetched under CORS rules, so the only way to test the deployed path is to load it
|
||||
# from the service.
|
||||
|
||||
# The reporter is appended to the *copy*. go-loose's source is never edited.
|
||||
if 'id="pilot"' not in html:
|
||||
html = html.replace('</body>', ' <script type="module" src="/pilot.js"></script>\n</body>')
|
||||
|
||||
open(os.path.join(out, 'index.html'), 'w').write(html)
|
||||
|
||||
# go-loose serves its own files under /assets/, so they belong in site/assets/ rather
|
||||
# than at the site root. Getting this wrong 404s every one of them while leaving the
|
||||
# page looking merely unstyled.
|
||||
assets = os.path.join(out, 'assets')
|
||||
os.makedirs(assets, exist_ok=True)
|
||||
for name in ('style.css', 'app.js', 'account.js', 'roles.js', 'profile.js'):
|
||||
p = os.path.join(src, name)
|
||||
if os.path.exists(p):
|
||||
open(os.path.join(assets, name), 'w').write(open(p).read())
|
||||
PY
|
||||
sed "s#from '/rt/v1/components.js'#from '$THEME_ORIGIN/rt/v1/components.js'#" \
|
||||
"$ROOT/hack/pilot-go-loose.js" > "$WORK/site/pilot.js"
|
||||
|
||||
echo "==> starting the theme service on :$THEME_PORT"
|
||||
# The selector fetches contract.json, and this page is on a different origin from the
|
||||
# service. A cross-origin module fetch and a JSON fetch both need the consuming
|
||||
# origin in CORS_ALLOWED_ORIGINS; a same-origin deployment needs nothing here. That is
|
||||
# the one setting adopting the contract requires, and setting it here is what proves it
|
||||
# is sufficient.
|
||||
DATABASE_PATH="$WORK/pilot.db" \
|
||||
BANANAS_ENVIRONMENT=development \
|
||||
BANANAS_AUTH_MODE=dev \
|
||||
BANANAS_PUBLIC_BASE_URL="$THEME_ORIGIN" \
|
||||
BANANAS_HTTP_ADDR=":$THEME_PORT" \
|
||||
BANANAS_SDK_DIR="$WORK/site/assets" \
|
||||
BANANAS_CORS_ALLOWED_ORIGINS="http://localhost:$PORT" \
|
||||
"$WORK/go-bananas" >"$WORK/theme.log" 2>&1 &
|
||||
THEME_PID=$!
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
curl -fsS -m 2 "http://localhost:$THEME_PORT/healthz" >/dev/null 2>&1 && break
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
echo "==> serving go-loose's page on :$PORT"
|
||||
(cd "$WORK/site" && exec python3 -m http.server "$PORT") >"$WORK/site.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
for _ in $(seq 1 40); do
|
||||
curl -fsS -m 2 "http://localhost:$PORT/index.html" >/dev/null 2>&1 && break
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
echo "==> driving the page"
|
||||
"$CHROME" \
|
||||
--headless --no-sandbox --disable-gpu \
|
||||
--virtual-time-budget=12000 \
|
||||
--dump-dom "http://localhost:$PORT/index.html" \
|
||||
>"$WORK/dom.html" 2>"$WORK/chrome.log"
|
||||
|
||||
result="$(python3 - "$WORK/dom.html" <<'PY'
|
||||
import re, sys
|
||||
dom = open(sys.argv[1]).read()
|
||||
m = re.search(r'id="pilot"[^>]*>(.*?)</pre>', dom, re.S)
|
||||
print(m.group(1).strip() if m else "")
|
||||
PY
|
||||
)"
|
||||
|
||||
if [[ -z "$result" ]]; then
|
||||
echo "PILOT FAIL: the page recorded nothing, so the check did not run." >&2
|
||||
echo "the reporter most likely threw; diagnostics follow." >&2
|
||||
echo "--- browser console ---" >&2
|
||||
grep -iE 'CONSOLE.*(error|Uncaught|violates|refused)' "$WORK/chrome.log" | head -5 >&2 || true
|
||||
echo "--- dom size / page head ---" >&2
|
||||
wc -c <"$WORK/dom.html" >&2 || true
|
||||
head -c 300 "$WORK/dom.html" >&2 || true
|
||||
echo >&2
|
||||
echo "--- static server 404s ---" >&2
|
||||
grep -oE 'GET /[^"]*" 404' "$WORK/site.log" | head -5 >&2 || true
|
||||
echo "--- pilot element ---" >&2
|
||||
grep -o 'id="pilot"[^<]*' "$WORK/dom.html" | head -2 >&2 || true
|
||||
echo "--- theme service log (last 5) ---" >&2
|
||||
tail -5 "$WORK/theme.log" >&2 || true
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "$result"
|
||||
[[ "$result" == PILOT\ PASS* ]]
|
||||
151
hack/check-pilot-go-tell.sh
Executable file
151
hack/check-pilot-go-tell.sh
Executable file
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Pilot check: go-tell consuming the shared contract.
|
||||
#
|
||||
# go-tell is the hard case. Before the migration it had no CSS custom properties at
|
||||
# all: 340 hardcoded hex values and 153 per-palette override rules, with cyberpunk
|
||||
# baked into the un-scoped base rules. Its stylesheet now reads from the contract, so
|
||||
# this check exists to prove the rewrite works rather than to assert that it was typed:
|
||||
#
|
||||
# - The four palettes go-tell's own rules key on are all published.
|
||||
# - Switching a palette through the shipped control moves the rendered page.
|
||||
# - The terminal palette's global `font-family: ... !important` reaches the
|
||||
# application but not into the controls' shadow roots.
|
||||
# - The appearance axis writes data-mode, which is what gives go-tell the light
|
||||
# mode it never had.
|
||||
#
|
||||
# It does not boot go-tell: the application needs Postgres and a session cookie to
|
||||
# reach its authenticated routes, and none of that is what is being tested. What is
|
||||
# tested is the built shell and the built stylesheet.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TELL="${GO_TELL_DIR:-/home/dnoulet/go/go-tell}"
|
||||
WORK="$(mktemp -d)"
|
||||
PORT="${PORT:-8396}"
|
||||
THEME_PORT="${THEME_PORT:-8395}"
|
||||
CHROME="${CHROME:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || true)}"
|
||||
|
||||
cleanup() {
|
||||
for pid in "${SERVER_PID:-}" "${THEME_PID:-}"; do
|
||||
[[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
rm -rf "$WORK"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
[[ -d "$TELL/ui/app" ]] || { echo "go-tell not found at $TELL; set GO_TELL_DIR" >&2; exit 1; }
|
||||
[[ -n "$CHROME" ]] || { echo "no chromium found; set CHROME" >&2; exit 1; }
|
||||
|
||||
echo "==> building the service, the SDK and go-tell's UI"
|
||||
(cd "$ROOT" && go build -o "$WORK/go-bananas" ./cmd/go-bananas)
|
||||
(cd "$ROOT/sdk" && npm run build >/dev/null)
|
||||
(cd "$TELL/ui/app" && npm run build >/dev/null)
|
||||
|
||||
DIST="$TELL/ui/dist"
|
||||
[[ -d "$DIST" ]] || { echo "expected a build at $DIST" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$WORK/site" "$WORK/site/assets"
|
||||
cp "$ROOT"/sdk/dist/*.js "$WORK/site/assets/"
|
||||
cp "$DIST/index.html" "$WORK/site/index.html"
|
||||
cp "$DIST"/assets/* "$WORK/site/" 2>/dev/null || true
|
||||
cp "$ROOT/hack/pilot-go-tell.js" "$WORK/site/pilot.js"
|
||||
|
||||
# The contract is added here rather than served through go-tell's Go handler, because
|
||||
# this check serves the built shell with a plain static server. The mechanism itself is
|
||||
# covered where it lives, by `go test ./internal/web/ -run Theme` in go-tell; this check
|
||||
# is about whether the stylesheet and the controls work together. What is added below is
|
||||
# what applyThemeOrigin adds when CMS_THEME_BASE_URL is set.
|
||||
echo "==> assembling go-tell's built shell"
|
||||
SHELL="$WORK/site/index.html" SITE_ASSETS="$WORK/site" THEME_ORIGIN="http://localhost:$THEME_PORT" \
|
||||
python3 <<'PY'
|
||||
import os, re
|
||||
|
||||
path = os.environ['SHELL']
|
||||
origin = os.environ['THEME_ORIGIN']
|
||||
s = open(path).read()
|
||||
|
||||
# The shell references its own bundle by absolute path; the check serves those files
|
||||
# beside the shell rather than under /assets.
|
||||
s = re.sub(r'(src|href)="/assets/', r'\1="/', s)
|
||||
|
||||
# What go-tell's handler adds when the theme origin is configured: a stylesheet and the
|
||||
# control module, inside the head so the stylesheet parses before the body renders.
|
||||
tag = (
|
||||
'\n <link rel="stylesheet" href="' + origin + '/rt/v1/contract.css" data-theme-contract>'
|
||||
'\n <script type="module" src="' + origin + '/rt/v1/components.js"></script>\n'
|
||||
)
|
||||
if '</head>' in s:
|
||||
s = s.replace('</head>', tag + ' </head>', 1)
|
||||
|
||||
# Transitions are disabled for the check. go-tell transitions `background` over
|
||||
# 300ms, and a headless run under --virtual-time-budget fast-forwards timers without
|
||||
# advancing the style clock, so a transition never leaves its start value: the
|
||||
# untransitioned --bn-bg already reports the new palette while the computed background
|
||||
# still reports the old one. That is an artefact of the harness, not of the
|
||||
# application, and reading through it is the only way to make the assertions
|
||||
# deterministic.
|
||||
s = s.replace(
|
||||
'</head>',
|
||||
'<style>*,*::before,*::after{transition:none !important;animation:none !important}</style>'
|
||||
' </head>',
|
||||
1,
|
||||
)
|
||||
|
||||
# The reporter, appended to the copy. go-tell's own source is never edited.
|
||||
s = s.replace('</body>', ' <script type="module" src="/pilot.js"></script>\n</body>')
|
||||
open(path, 'w').write(s)
|
||||
PY
|
||||
|
||||
echo "==> starting the theme service on :$THEME_PORT"
|
||||
DATABASE_PATH="$WORK/tell.db" \
|
||||
BANANAS_ENVIRONMENT=development \
|
||||
BANANAS_AUTH_MODE=dev \
|
||||
BANANAS_PUBLIC_BASE_URL="http://localhost:$THEME_PORT" \
|
||||
BANANAS_HTTP_ADDR=":$THEME_PORT" \
|
||||
BANANAS_SDK_DIR="$WORK/site/assets" \
|
||||
BANANAS_CORS_ALLOWED_ORIGINS="http://localhost:$PORT" \
|
||||
"$WORK/go-bananas" >"$WORK/theme.log" 2>&1 &
|
||||
THEME_PID=$!
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
curl -fsS -m 2 "http://localhost:$THEME_PORT/healthz" >/dev/null 2>&1 && break
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
echo "==> serving go-tell's shell on :$PORT"
|
||||
(cd "$WORK/site" && exec python3 -m http.server "$PORT") >"$WORK/site.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
for _ in $(seq 1 40); do
|
||||
curl -fsS -m 2 "http://localhost:$PORT/index.html" >/dev/null 2>&1 && break
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
echo "==> driving the page"
|
||||
"$CHROME" \
|
||||
--headless --no-sandbox --disable-gpu \
|
||||
--virtual-time-budget=15000 \
|
||||
--dump-dom "http://localhost:$PORT/index.html" \
|
||||
>"$WORK/dom.html" 2>"$WORK/chrome.log"
|
||||
|
||||
result="$(python3 - "$WORK/dom.html" <<'PYEOF'
|
||||
import re, sys
|
||||
dom = open(sys.argv[1]).read()
|
||||
m = re.search(r'id="pilot"[^>]*>(.*?)</pre>', dom, re.S)
|
||||
print(m.group(1).strip() if m else "")
|
||||
PYEOF
|
||||
)"
|
||||
|
||||
if [[ -z "$result" ]]; then
|
||||
echo "PILOT FAIL: the page recorded nothing, so the check did not run." >&2
|
||||
echo "--- browser console ---" >&2
|
||||
grep -iE 'CONSOLE.*(error|Uncaught|violates|refused)' "$WORK/chrome.log" | head -5 >&2 || true
|
||||
echo "--- dom size ---" >&2
|
||||
wc -c <"$WORK/dom.html" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "$result"
|
||||
[[ "$result" == PILOT\ PASS* ]]
|
||||
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
|
||||
260
hack/go.mod
Normal file
260
hack/go.mod
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
module hack/tools
|
||||
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.6 // indirect
|
||||
codeberg.org/chavacava/garif v0.2.1 // indirect
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v5 v5.2.0 // indirect
|
||||
dev.gaijin.team/go/golib v0.8.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
github.com/4meepo/tagalign v1.4.4 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.8 // indirect
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
|
||||
github.com/AlwxSin/noinlineerr v1.0.6 // indirect
|
||||
github.com/Antonboom/errname v1.1.2 // indirect
|
||||
github.com/Antonboom/nilnil v1.1.2 // indirect
|
||||
github.com/Antonboom/testifylint v1.6.4 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.5.0 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/air-verse/air v1.67.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
github.com/alexkohler/prealloc v1.1.0 // indirect
|
||||
github.com/alfatraining/structtag v1.0.0 // indirect
|
||||
github.com/alingse/asasalint v0.0.11 // indirect
|
||||
github.com/alingse/nilnesserr v0.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bep/godartsass/v2 v2.5.0 // indirect
|
||||
github.com/bep/golibsass v1.2.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.6 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||
github.com/bombsimon/wsl/v5 v5.9.0 // indirect
|
||||
github.com/breml/bidichk v0.3.3 // indirect
|
||||
github.com/breml/errchkjson v0.4.1 // indirect
|
||||
github.com/butuzov/ireturn v0.4.1 // indirect
|
||||
github.com/butuzov/mirror v1.3.3 // indirect
|
||||
github.com/catenacyber/perfsprint v0.10.1 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charithe/durationcheck v0.0.11 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/ckaznocha/intrange v0.3.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/cubicdaiya/gonp v1.0.4 // indirect
|
||||
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
||||
github.com/daixiang0/gci v0.13.7 // indirect
|
||||
github.com/dave/dst v0.27.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/ghostiam/protogetter v1.0.1 // indirect
|
||||
github.com/go-critic/go-critic v0.15.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/godoc-lint/godoc-lint v0.11.4 // indirect
|
||||
github.com/gofrs/flock v0.13.1 // indirect
|
||||
github.com/gohugoio/hashstructure v0.6.0 // indirect
|
||||
github.com/gohugoio/hugo v0.164.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
||||
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.1 // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
|
||||
github.com/golangci/golangci-lint/v2 v2.14.0 // indirect
|
||||
github.com/golangci/golines v0.15.0 // indirect
|
||||
github.com/golangci/misspell v0.8.0 // indirect
|
||||
github.com/golangci/plugin-module-register v0.1.2 // indirect
|
||||
github.com/golangci/revgrep v0.8.0 // indirect
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
|
||||
github.com/google/cel-go v0.28.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jgautheron/goconst v1.11.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
|
||||
github.com/kisielk/errcheck v1.20.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
github.com/kulti/thelper v0.7.1 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.15 // indirect
|
||||
github.com/ldez/exptostd v0.4.5 // indirect
|
||||
github.com/ldez/gomoddirectives v0.10.0 // indirect
|
||||
github.com/ldez/grignotin v0.10.1 // indirect
|
||||
github.com/ldez/structtags v0.6.1 // indirect
|
||||
github.com/ldez/tagliatelle v0.8.0 // indirect
|
||||
github.com/ldez/usetesting v0.5.0 // indirect
|
||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
||||
github.com/macabu/inamedparam v0.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
|
||||
github.com/manuelarte/funcorder v0.6.0 // indirect
|
||||
github.com/maratori/testableexamples v1.0.1 // indirect
|
||||
github.com/maratori/testpackage v1.1.2 // indirect
|
||||
github.com/matoous/godox v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mgechev/revive v1.17.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/ncruces/go-sqlite3 v0.32.0 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/nishanths/exhaustive v0.13.0 // indirect
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pganalyze/pg_query_go/v6 v6.2.2 // indirect
|
||||
github.com/pingcap/errors v0.11.5-0.20250523034308-74f78ae071ee // indirect
|
||||
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
|
||||
github.com/pingcap/log v1.1.0 // indirect
|
||||
github.com/pingcap/tidb/pkg/parser v0.0.0-20260418072757-ce92298d1124 // indirect
|
||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/raeperd/recvcheck v0.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/riza-io/grpc-go v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.16.0 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.29.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sonatard/noctx v0.5.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.9.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.12.0 // indirect
|
||||
github.com/sqlc-dev/doubleclick v1.0.0 // indirect
|
||||
github.com/sqlc-dev/sqlc v1.31.1 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.12.1 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.12 // indirect
|
||||
github.com/tetafro/godot v1.5.6 // indirect
|
||||
github.com/tetratelabs/wazero v1.12.0 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
|
||||
github.com/timonwong/loggercheck v0.12.0 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||
github.com/ultraware/funlen v0.2.0 // indirect
|
||||
github.com/ultraware/whitespace v0.2.0 // indirect
|
||||
github.com/uudashr/gocognit v1.2.1 // indirect
|
||||
github.com/uudashr/iface v1.5.1 // indirect
|
||||
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect
|
||||
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
|
||||
github.com/xen0n/gosmopolitan v1.3.0 // indirect
|
||||
github.com/xo/terminfo v1.0.0 // indirect
|
||||
github.com/yagipy/maintidx v1.0.0 // indirect
|
||||
github.com/yeya24/promlinter v0.3.0 // indirect
|
||||
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||
go-simpler.org/musttag v0.14.0 // indirect
|
||||
go-simpler.org/sloglint v0.12.0 // indirect
|
||||
go.augendre.info/arangolint v0.4.0 // indirect
|
||||
go.augendre.info/fatcontext v0.10.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/net v0.59.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
|
||||
google.golang.org/grpc v1.83.1 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
honnef.co/go/tools v0.8.1 // indirect
|
||||
mvdan.cc/gofumpt v0.12.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
|
||||
)
|
||||
1029
hack/go.sum
Normal file
1029
hack/go.sum
Normal file
File diff suppressed because it is too large
Load diff
135
hack/helm-matrix.sh
Executable file
135
hack/helm-matrix.sh
Executable file
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Renders the chart across a matrix of configurations and checks each result.
|
||||
#
|
||||
# Two things are being verified, and `helm lint` only covers the first:
|
||||
#
|
||||
# 1. Every configuration that should work renders to YAML that *parses*. A
|
||||
# template bug that only triggers under a non-default value produces valid
|
||||
# YAML in review and an unapplyable manifest in production, so each render is
|
||||
# fed back through a YAML parser rather than trusted because helm exited zero.
|
||||
#
|
||||
# 2. Every configuration that should be refused is refused, and the refusal says
|
||||
# which value to change. A validation guard that silently accepts a bad value
|
||||
# is worse than no guard, because it moves the failure to run time.
|
||||
#
|
||||
# The matrix lives in a script rather than in the Makefile because it is a list of
|
||||
# cases, and a list is easier to read, review and extend as shell than as make.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CHART="${CHART:-deploy/helm/go-bananas}"
|
||||
RELEASE="${RELEASE:-go-bananas}"
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# yaml_ok verifies that a rendered file is parseable YAML.
|
||||
yaml_ok() {
|
||||
python3 -c '
|
||||
import sys
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
sys.exit(3)
|
||||
list(yaml.safe_load_all(open(sys.argv[1])))
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# ok renders a configuration that must succeed.
|
||||
ok() {
|
||||
local desc="$1"
|
||||
shift
|
||||
if ! helm template "$RELEASE" "$CHART" "$@" >"$work/out.yaml" 2>"$work/err.txt"; then
|
||||
printf 'FAIL %s (should have rendered)\n' "$desc"
|
||||
sed -n '1,3p' "$work/err.txt" | sed 's/^/ /'
|
||||
fail=$((fail + 1))
|
||||
return
|
||||
fi
|
||||
yaml_ok "$work/out.yaml"
|
||||
case $? in
|
||||
0) printf 'ok %s\n' "$desc"; pass=$((pass + 1)) ;;
|
||||
3) printf 'ok %s (PyYAML absent, skipped the parse check)\n' "$desc"; pass=$((pass + 1)) ;;
|
||||
*) printf 'FAIL %s (rendered output is not valid YAML)\n' "$desc"; fail=$((fail + 1)) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# rejected renders a configuration that must fail, and requires the message to name
|
||||
# the problem rather than being a bare stack trace.
|
||||
rejected() {
|
||||
local desc="$1"
|
||||
shift
|
||||
if helm template "$RELEASE" "$CHART" "$@" >"$work/out.yaml" 2>"$work/err.txt"; then
|
||||
printf 'FAIL %s (should have been refused but rendered)\n' "$desc"
|
||||
fail=$((fail + 1))
|
||||
return
|
||||
fi
|
||||
if grep -qE 'execution error|Error:' "$work/err.txt"; then
|
||||
printf 'ok %s (refused: %s)\n' "$desc" \
|
||||
"$(grep -oE '(execution error.*|Error: .*)' "$work/err.txt" | head -1 | cut -c1-100)"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
printf 'FAIL %s (refused, but without an actionable message)\n' "$desc"
|
||||
sed -n '1,3p' "$work/err.txt" | sed 's/^/ /'
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "== configurations that must render =="
|
||||
ok "default values"
|
||||
ok "oidc with an existing secret" \
|
||||
--set identity.oidc.generateSecret=false \
|
||||
--set identity.oidc.existingSecret=go-bananas-oidc
|
||||
ok "oidc with a generated secret" \
|
||||
--set identity.mode=oidc \
|
||||
--set config.publicBaseUrl=https://themes.example.com
|
||||
ok "production over https" \
|
||||
--set config.environment=production \
|
||||
--set config.publicBaseUrl=https://themes.example.com
|
||||
ok "identity disabled" --set identity.mode=disabled
|
||||
ok "development bypass" --set identity.mode=dev
|
||||
ok "ingress with TLS" \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=themes.example.com \
|
||||
--set ingress.tls[0].secretName=themes-tls \
|
||||
--set ingress.tls[0].hosts[0]=themes.example.com
|
||||
ok "ephemeral storage" --set persistence.type=emptyDir
|
||||
ok "existing claim" --set persistence.existingClaim=bananas-data
|
||||
ok "autoscaling and disruption budget" \
|
||||
--set autoscaling.enabled=true --set podDisruptionBudget.enabled=true
|
||||
ok "network policy" --set networkPolicy.enabled=true
|
||||
ok "service monitor" --set metrics.serviceMonitor.enabled=true
|
||||
ok "image pinned by digest" --set image.digest=sha256:deadbeef
|
||||
ok "image in another registry" --set image.repository=quay.io/urpi/go-bananas
|
||||
ok "base path" --set config.basePath=/themes
|
||||
ok "extra environment" --set 'extraEnv[0].name=FOO' --set 'extraEnv[0].value=bar'
|
||||
ok "snapshot cronjob" --set persistence.snapshot.enabled=true
|
||||
|
||||
echo
|
||||
echo "== configurations that must be refused =="
|
||||
rejected "both an existing and a generated secret" \
|
||||
--set identity.oidc.existingSecret=go-bananas-oidc
|
||||
rejected "neither an existing nor a generated secret" \
|
||||
--set identity.oidc.generateSecret=false
|
||||
rejected "an empty public base URL" \
|
||||
--set config.publicBaseUrl=
|
||||
rejected "a public base URL with a trailing slash" \
|
||||
--set config.publicBaseUrl=https://themes.example.com/
|
||||
rejected "a public base URL that is not a URL" \
|
||||
--set config.publicBaseUrl=themes.example.com
|
||||
rejected "TLS verification disabled in production" \
|
||||
--set identity.oidc.skipTLSVerify=true --set config.environment=production
|
||||
rejected "an unknown identity mode" --set identity.mode=nonsense
|
||||
rejected "an OIDC mode with no issuer" --set identity.mode=oidc --set identity.oidc.issuer=
|
||||
rejected "a shutdown timeout longer than the grace period" \
|
||||
--set config.shutdownTimeoutMs=60000 --set deployment.terminationGracePeriodSeconds=30
|
||||
rejected "an unknown top-level value" --set thisKeyDoesNotExist=1
|
||||
|
||||
echo
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
printf '%d of %d checks failed\n' "$fail" "$((pass + fail))"
|
||||
exit 1
|
||||
fi
|
||||
printf 'all %d chart checks passed\n' "$pass"
|
||||
76
hack/integration-check.html
Normal file
76
hack/integration-check.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="midnight" data-mode="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Shared styling — integration check</title>
|
||||
|
||||
<!-- The contract. One stylesheet, every palette. SBN_BASE lets one copy of this file
|
||||
be served from anywhere while still pointing at the service. -->
|
||||
<link rel="stylesheet" href="/rt/v1/contract.css" id="contract-link">
|
||||
|
||||
<style>
|
||||
/* Host page styling, deliberately using only the unprefixed legacy aliases, which
|
||||
is how the applications being consolidated are written today. If the alias layer
|
||||
works, everything below is themed without a single rename. */
|
||||
:root { --page-gap: 16px; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: var(--page-gap);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 15px/1.5 ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: calc(var(--page-gap) * 1.25);
|
||||
margin-bottom: var(--page-gap);
|
||||
}
|
||||
h1 { margin: 0 0 4px; font-size: 1.25rem; }
|
||||
.muted { color: var(--muted); }
|
||||
.swatchrow { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 12px; }
|
||||
.swatchrow > div {
|
||||
width: 44px; height: 28px; border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.15);
|
||||
}
|
||||
/* A rule that would wreck an unisolated injected component. */
|
||||
.card button { all: unset; }
|
||||
|
||||
/* The shadow-root proof: this must not touch anything inside either component. */
|
||||
button { font: inherit; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Shared styling</h1>
|
||||
<p class="muted">This page uses only the unprefixed legacy aliases: <code>--bg</code>,
|
||||
<code>--text</code>, <code>--line</code>, <code>--accent</code>.</p>
|
||||
<div class="swatchrow" id="row"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h1>The controls</h1>
|
||||
<p class="muted">Injected, not hand-written. Shadow DOM isolates them from the
|
||||
<code>.card button {{ all: unset }}</code> rule above.</p>
|
||||
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
|
||||
<bananas-theme-selector id="selector"></bananas-theme-selector>
|
||||
<bananas-appearance-toggle id="toggle" show-labels></bananas-appearance-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h1>Host page, after the components</h1>
|
||||
<p class="muted">If component styles leaked, this card would change. It must not.</p>
|
||||
<button id="host">A plain host button</button>
|
||||
</div>
|
||||
|
||||
<pre id="state" class="card" style="font:12px/1.6 ui-monospace,monospace"></pre>
|
||||
<div id="verdict" style="font:16px/1.4 monospace;padding:12px;border:2px solid #888"></div>
|
||||
</main>
|
||||
|
||||
<script type="module" src="./contract-check.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
124
hack/pilot-go-loose.js
Normal file
124
hack/pilot-go-loose.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* 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)
|
||||
})
|
||||
167
hack/pilot-go-tell.js
Normal file
167
hack/pilot-go-tell.js
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* 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)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue