Files
meshcore-analyzer/test-logo-theme-e2e.js
T
Kpa-clawbot 364c5766fc feat(logo): wire new CoreScope SVG logo into navbar + home hero (#1137)
## Adds new logo and home hero

Replaces the navbar mushroom emoji + "CoreScope" text spans with the new
CoreScope SVG mark, and adds a hero SVG (with the MESH ANALYZER tagline)
above the home page H1.

### What changed
- `public/img/corescope-logo.svg` — navbar mark, no tagline (locked
"aggressive low-amp chirp" variant: facing-arcs + low-amp chirp
connector between the two nodes).
- `public/img/corescope-hero.svg` — home hero version, includes the MESH
ANALYZER tagline.
- `public/index.html` — replaces `<span class="brand-icon">🍄</span><span
class="brand-text">CoreScope</span>` with `<img class="brand-logo"
src="img/corescope-logo.svg?__BUST__" …>`. `.nav-brand` link still
routes to `#/`. `.live-dot` retained.
- `public/style.css` — adds `.brand-logo { height: 36px }` (32px on
tablet ≤900px). Existing 52px nav height unchanged.
- `public/home.js` / `public/home.css` — adds `<img
class="home-hero-logo">` above the hero `<h1>`, sized `max-width:
min(720px, 90vw)` and centered.

### TDD
Red→green is visible in the branch:
- `3159b82` — `test(logo): add failing E2E …` (red commit). Adds
`test-logo-rebrand-e2e.js` and wires it into the `e2e-test` job in
`deploy.yml` with `CHROMIUM_REQUIRE=1`. On this commit `index.html`
still has the emoji + text spans, `home.js` has no hero img, and the SVG
asset files do not exist — the test asserts on each so CI fails on
assertion.
- `19434e1` — `feat(logo): wire new CoreScope SVG logo …` (green
commit). Implements the fix.

### E2E asserts
1. `.nav-brand img` exists with `src` ending `corescope-logo.svg`
2. legacy `.brand-icon` / `.brand-text` are gone
3. `.live-dot` is present, visible, and to the right of the logo (no
overlap)
4. `.home-hero img.home-hero-logo` exists with `src` ending
`corescope-hero.svg`, positioned BEFORE the `<h1>`
5. both `/img/corescope-{logo,hero}.svg` return 200 with svg
content-type

### Customizer compatibility
- `customize.js` still does `querySelector('.brand-text')` /
`.brand-icon` for live branding updates. Both now return `null`;
existing `if (el)` guards make those branches silent no-ops. **No JS
errors, but the customizer's `branding.siteName` and `branding.logoUrl`
fields no longer rewrite the navbar brand** — the brand is now a fixed
SVG asset.
- **Theme accent does NOT recolor the SVG.** SVGs loaded via `<img src>`
are isolated documents and cannot inherit document CSS variables; the
SVG falls back to its embedded brand colors. This is appropriate for a
brand mark; if recoloring per theme is desired later, swap to inline SVG
(separate PR).

### Browser validation
Local Chromium not available in this env; the E2E test soft-skips
locally and hard-fails in CI (`CHROMIUM_REQUIRE=1`). Server-side checks
done locally:
- `curl http://localhost:13581/` → confirmed `<img class="brand-logo"
src="img/corescope-logo.svg?<bust>" …>` rendered, no
`.brand-icon`/`.brand-text` spans.
- `curl -I /img/corescope-logo.svg` and `/img/corescope-hero.svg` → both
200.

### Performance
No hot-path changes. Two new static SVG assets (~7.6KB each), served
directly by the Go static handler. Cache-busted via `?__BUST__`
(auto-replaced server-side).

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
2026-05-06 19:17:46 -07:00

164 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
/* Logo theme reactivity E2E — verifies that the navbar + hero logos
* inherit page CSS custom properties and remain visible when the user
* switches to the Light theme.
*
* Asserts:
* 1. With data-theme="light", the navbar wordmark CORE/SCOPE elements
* have a computed fill that is NOT the legacy hardcoded sage
* (#cfd9c9 / rgb(207,217,201)).
* 2. The hero SVG does NOT contain a full-canvas opaque background
* rect (no <rect width=1200 height=300> with a non-transparent fill
* reachable via the inline SVG in the home-hero region).
* 3. The hero wordmark CORE/SCOPE compute-fills also drop the legacy
* sage hex when the page theme is Light.
*
* Designed to FAIL on the pre-fix branch (where the SVGs are loaded as
* <img>, the wordmark fill is baked to #cfd9c9, and the hero SVG ships a
* solid <rect fill="var(--logo-bg, #0e1714)">).
*/
'use strict';
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:13581';
const LEGACY_SAGE = 'rgb(207, 217, 201)';
function fail(msg) {
console.error(`test-logo-theme-e2e.js: FAIL — ${msg}`);
process.exit(1);
}
async function main() {
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
let browser;
try {
browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || undefined,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
});
} catch (err) {
if (requireChromium) {
console.error(`test-logo-theme-e2e.js: FAIL — Chromium required but unavailable: ${err.message}`);
process.exit(1);
}
console.log(`test-logo-theme-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`);
process.exit(0);
}
let passed = 0;
const total = 3;
try {
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await context.newPage();
page.setDefaultTimeout(10000);
// Force Light theme BEFORE first navigation so initial paint uses it.
await page.addInitScript(() => {
try { localStorage.setItem('meshcore-user-level', 'experienced'); } catch (_) {}
});
await page.goto(BASE + '/#/', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.nav-brand', { timeout: 8000 });
await page.evaluate(() => { document.documentElement.setAttribute('data-theme', 'light'); });
// 1. Navbar wordmark must be inline-SVG <text> (not <img>) and computed
// fill must NOT be the legacy hardcoded sage. We grep for any <text>
// with textContent CORE or SCOPE inside .nav-brand.
const navWordmarkFills = await page.evaluate(() => {
const out = [];
const root = document.querySelector('.nav-brand');
if (!root) return { error: '.nav-brand missing' };
const texts = root.querySelectorAll('svg text');
texts.forEach((t) => {
const tc = (t.textContent || '').trim();
if (tc === 'CORE' || tc === 'SCOPE') {
out.push({ tc, fill: getComputedStyle(t).fill });
}
});
return { out };
});
if (navWordmarkFills.error) fail(navWordmarkFills.error);
if (!navWordmarkFills.out || navWordmarkFills.out.length < 2) {
fail(`navbar inline-SVG wordmark <text> CORE/SCOPE not found (found: ${JSON.stringify(navWordmarkFills.out)}). Navbar logo must be inline <svg> so CSS vars apply.`);
}
for (const w of navWordmarkFills.out) {
if (w.fill === LEGACY_SAGE) {
fail(`navbar wordmark "${w.tc}" still computes legacy sage fill ${LEGACY_SAGE} — wordmark fill must theme via CSS var`);
}
}
console.log(` ✅ navbar wordmark fills are theme-reactive (${navWordmarkFills.out.map((w) => w.tc + '=' + w.fill).join(', ')})`);
passed++;
// 2. Hero SVG must NOT have a full-canvas opaque background rect.
await page.evaluate(() => { window.location.hash = '#/home'; });
await page.waitForFunction(() => location.hash === '#/home');
await page.waitForSelector('.home-hero', { timeout: 8000 });
// Ensure light theme survives reload.
await page.evaluate(() => { document.documentElement.setAttribute('data-theme', 'light'); });
const heroBg = await page.evaluate(() => {
const hero = document.querySelector('.home-hero');
if (!hero) return { error: '.home-hero missing' };
const svg = hero.querySelector('svg');
if (!svg) return { error: '.home-hero has no inline <svg> child (hero must be inline so CSS vars apply)' };
// Look for a child <rect> that covers the entire viewBox with a non-transparent fill.
const rects = svg.querySelectorAll('rect');
const offending = [];
rects.forEach((r) => {
const w = r.getAttribute('width') || '';
const h = r.getAttribute('height') || '';
const cs = getComputedStyle(r);
const fill = cs.fill || '';
const op = parseFloat(cs.fillOpacity || '1');
// legacy hero shipped <rect width=1200 height=300 fill=var(--logo-bg, #0e1714)>
if ((w === '1200' || w === '100%') && (h === '300' || h === '100%') && fill && fill !== 'none' && fill !== 'rgba(0, 0, 0, 0)' && op > 0.05) {
offending.push({ w, h, fill, op });
}
});
return { offending, rectCount: rects.length };
});
if (heroBg.error) fail(heroBg.error);
if (heroBg.offending && heroBg.offending.length > 0) {
fail(`hero SVG has full-canvas opaque background rect — paints over light theme: ${JSON.stringify(heroBg.offending)}`);
}
console.log(` ✅ hero SVG has no full-canvas opaque background rect`);
passed++;
// 3. Hero wordmark CORE/SCOPE must not compute legacy sage fill on light theme.
const heroWordmarkFills = await page.evaluate(() => {
const hero = document.querySelector('.home-hero');
if (!hero) return { error: '.home-hero missing' };
const out = [];
hero.querySelectorAll('svg text').forEach((t) => {
const tc = (t.textContent || '').trim();
if (tc === 'CORE' || tc === 'SCOPE') {
out.push({ tc, fill: getComputedStyle(t).fill });
}
});
return { out };
});
if (heroWordmarkFills.error) fail(heroWordmarkFills.error);
if (!heroWordmarkFills.out || heroWordmarkFills.out.length < 2) {
fail(`hero inline-SVG wordmark <text> CORE/SCOPE not found (found: ${JSON.stringify(heroWordmarkFills.out)})`);
}
for (const w of heroWordmarkFills.out) {
if (w.fill === LEGACY_SAGE) {
fail(`hero wordmark "${w.tc}" still computes legacy sage fill ${LEGACY_SAGE} — invisible on light theme`);
}
}
console.log(` ✅ hero wordmark fills are theme-reactive (${heroWordmarkFills.out.map((w) => w.tc + '=' + w.fill).join(', ')})`);
passed++;
await browser.close();
console.log(`\ntest-logo-theme-e2e.js: ${passed}/${total} PASS`);
} catch (err) {
try { await browser.close(); } catch (_) {}
console.error(`test-logo-theme-e2e.js: FAIL — ${err.message}`);
process.exit(1);
}
}
main();