feat(web): customizable placeholder, footer text, and color theme via BOR_* env vars
BOR_INPUT_PLACEHOLDER / BOR_FOOTER_TEXT / BOR_THEME (+ the indigo.css example theme); authoring guide: frontend/assets/themes/README.md, docs: README 'Customizing the look'.
This commit is contained in:
@@ -95,6 +95,60 @@ def test_brand_js_reskins_title_brand_text_prose_and_attributes() -> None:
|
||||
assert marker in js, f"the attribute pass must cover {marker}"
|
||||
|
||||
|
||||
def test_brand_js_applies_phase_62_customization_from_the_same_fetch() -> None:
|
||||
"""Phase 62 (owner-locked 2026-09-01, TODO L3): the SAME settled
|
||||
/api/config answer also drives the three customization keys — the
|
||||
#message-input placeholder, every .footer-text node, and the theme
|
||||
stylesheet link (inserted right after the styles.css link, guarded
|
||||
by #theme-override, degrading with a console.warn on 404 — A5).
|
||||
No second network call: the keys ride the existing boot fetch."""
|
||||
js = _text(BRAND_JS)
|
||||
assert js.count('= fetch("/api/config"') == 1, (
|
||||
"the three keys must ride the existing boot fetch — no new call"
|
||||
)
|
||||
# 5. The composer placeholder (chat page only — the null guard
|
||||
# no-ops on every other page).
|
||||
assert 'document.querySelector("#message-input")' in js
|
||||
assert "input_placeholder" in js
|
||||
# 6. The footer line on all 9 pages (the phase-61 hook) — via
|
||||
# textContent: an operator string can't inject markup.
|
||||
assert 'document.querySelectorAll(".footer-text")' in js
|
||||
assert "footer_text" in js
|
||||
# 7. The theme link: /assets/themes/<name>, inserted right after
|
||||
# the styles.css link, tagged #theme-override (the idempotency
|
||||
# guard), with the A5 degradation warn.
|
||||
assert '"/assets/themes/"' in js
|
||||
assert 'link.id = "theme-override"' in js
|
||||
assert 'getElementById("theme-override")' in js
|
||||
assert 'insertAdjacentElement("afterend", link)' in js
|
||||
assert "link.onerror" in js
|
||||
assert '"brand: theme " + themeName' in js
|
||||
# The styles.css finder must survive the phase-33/54 cache-bust
|
||||
# rewrite: the SERVED HTML carries the asset ref with a
|
||||
# ?v=<token> query (and el.href is the absolute URL), so the match
|
||||
# has to run on the RAW attribute path with query/fragment
|
||||
# stripped — el.href.endsWith(…) would silently skip the insertion
|
||||
# (the theme never applied; found by the task-05 E2E).
|
||||
assert 'getAttribute("href")' in js
|
||||
assert "split(/[?#]/)[0]" in js
|
||||
assert 'endsWith("styles.css")' in js
|
||||
assert "el.href.endsWith" not in js
|
||||
# The empty-skip no-op contract: each key is guarded before any
|
||||
# DOM write, so an unset deployment stays byte-identical.
|
||||
for guard in ("if (placeholder) {", "if (footerText) {", "if (themeName) {"):
|
||||
assert guard in js, (
|
||||
f"an empty value must skip its application ({guard})"
|
||||
)
|
||||
# Independence: the phase-62 block sits AFTER the app_name passes
|
||||
# in the same .then — never gated by the name.
|
||||
assert js.index("// 4. Attributes:") < js.index("// Phase 62"), (
|
||||
"the customization keys must apply after the app_name block, "
|
||||
"even when the name is the default/empty"
|
||||
)
|
||||
# The app_name literal default pin still holds.
|
||||
assert 'window.BOR_BRAND = "Brain of Reese"' in js
|
||||
|
||||
|
||||
def test_page_scripts_keep_the_default_literal_exactly_once() -> None:
|
||||
"""The fallback literal lives in the page scripts' brand() reads —
|
||||
exactly one copy per file (a second copy could drift out of sync)."""
|
||||
|
||||
Reference in New Issue
Block a user