mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-03 13:51:41 +00:00
9b36b7c487
Red commit: 86083fe176 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26970512724)
Fixes #1518.
Adds `branding.homeUrl` to the Branding tab so operators embedding
CoreScope inside a larger site can point the navbar logo at their own
home page instead of the in-app `#/` route.
## What
- New optional config: `branding.homeUrl`. When set, `<a
class="nav-brand">[href]` is rewritten to that URL. Empty / null /
invalid → falls through to the existing `#/` default.
- Customizer Branding tab gets a new "Home URL" field next to Logo URL.
- Strict whitelist validator `isValidHomeUrl()`:
- **Accepts**: `http(s)://...` absolute URLs, `#`-prefixed app routes
(`#/`, `#/home`, etc.)
- **Rejects**: `javascript:`, `data:`, `vbscript:`, `file:`, `about:`,
protocol-relative `//`, bare paths, ftp, whitespace, non-strings, and
whitespace-obfuscated `java\tscript:` payloads.
- Cross-origin URLs open in the SAME tab (no `target="_blank"`);
operators can wrap with their own anchor handling if they need new-tab.
- **Bottom-nav 🏠 unchanged** — stays in-app to preserve SPA back-stack
on mobile (per triage decision).
## Scope
Touched files:
- `public/customize-v2.js` — new field, validator, override application
- `config.example.json` — `branding.homeUrl` + `_comment` updated per
AGENTS.md Config Documentation Rule
- `test-issue-1518-home-url.js` — new unit suite (validator + DOM-string
asserts)
- `test-customize-branding-e2e.js` — extended with three homeUrl
assertions
- `.github/workflows/deploy.yml` — wires new unit test into CI
## TDD
- Red commit lands tests + a permissive `isValidHomeUrl` stub so the
assertions execute (no compile/undefined-function errors). Tests fail on
assertion as expected.
- Green commit replaces the stub with the real whitelist, adds the
Branding-tab field, wires the override, and updates
`config.example.json`.
## E2E coverage
Extended `test-customize-branding-e2e.js` with three browser-level
assertions:
- `homeUrl='https://example.com/embed-home'` → `.nav-brand[href]` equals
it
- `homeUrl='javascript:alert(1)'` → `.nav-brand[href]` is NOT
javascript: (validator drops it)
- Empty `homeUrl` → `.nav-brand[href]` falls through to `#/`
E2E assertion added: `test-customize-branding-e2e.js:~95`
## Out of scope
- `public/bottom-nav.js` 🏠 button — left alone deliberately (mobile SPA
back-stack).
- `target="_blank"` / `rel="noopener"` magic — operators who need
new-tab can wrap.
- Server-side validation — homeUrl is purely a frontend display
override; SITE_CONFIG already proxies `branding.*` opaquely
(`map[string]interface{}` in `cmd/server/config.go`), no shape change
required.
158 lines
7.0 KiB
JavaScript
158 lines
7.0 KiB
JavaScript
/**
|
|
* E2E (#1297 B4): Customizer V2 — Branding (siteName, tagline, logo, favicon)
|
|
*
|
|
* Verifies the branding subsystem in public/customize-v2.js:
|
|
* - Site name input → updates .brand-text + document.title live
|
|
* - Logo URL input → swaps inline SVG for <img> (PR #1137 helper)
|
|
* - Override persisted to cs-theme-overrides.branding
|
|
* - Survives reload
|
|
*
|
|
* Usage: BASE_URL=http://localhost:13581 node test-customize-branding-e2e.js
|
|
*/
|
|
'use strict';
|
|
const { chromium } = require('playwright');
|
|
const BASE = process.env.BASE_URL || 'http://localhost:3000';
|
|
|
|
let passed = 0, failed = 0;
|
|
async function step(name, fn) {
|
|
try { await fn(); passed++; console.log(' \u2713 ' + name); }
|
|
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
|
|
}
|
|
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
|
|
|
(async () => {
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath: process.env.CHROMIUM_PATH || undefined,
|
|
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
|
});
|
|
const ctx = await browser.newContext();
|
|
const page = await ctx.newPage();
|
|
page.setDefaultTimeout(8000);
|
|
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
|
|
|
console.log(`\n=== #1297 B4 customize-branding E2E against ${BASE} ===`);
|
|
|
|
await step('setup: clear overrides + load', async () => {
|
|
await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate(() => localStorage.removeItem('cs-theme-overrides'));
|
|
await page.reload({ waitUntil: 'load' });
|
|
await page.waitForFunction(() => window._customizerV2 && window._customizerV2.initDone, null, { timeout: 8000 });
|
|
});
|
|
|
|
await step('open customizer + switch to branding tab', async () => {
|
|
await page.click('#customizeToggle');
|
|
await page.waitForSelector('.cust-overlay:not(.hidden)');
|
|
const brandingTab = await page.$('.cust-tab[data-tab="branding"]');
|
|
if (brandingTab) await brandingTab.click();
|
|
await page.waitForSelector('input[data-cv2-field="branding.siteName"]', { timeout: 4000 });
|
|
});
|
|
|
|
await step('siteName input updates document.title live', async () => {
|
|
const inp = await page.$('input[data-cv2-field="branding.siteName"]');
|
|
assert(inp, 'branding.siteName input missing');
|
|
await page.evaluate((el) => {
|
|
el.value = 'MyMeshTest';
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}, inp);
|
|
await page.waitForTimeout(400);
|
|
const title = await page.title();
|
|
assert(title === 'MyMeshTest', 'document.title should update live, got: ' + title);
|
|
});
|
|
|
|
await step('siteName persists to cs-theme-overrides.branding.siteName', async () => {
|
|
const raw = await page.evaluate(() => localStorage.getItem('cs-theme-overrides'));
|
|
assert(raw, 'overrides not written');
|
|
const parsed = JSON.parse(raw);
|
|
assert(parsed.branding && parsed.branding.siteName === 'MyMeshTest',
|
|
'branding.siteName missing in overrides: ' + raw);
|
|
});
|
|
|
|
await step('logoUrl input triggers _setBrandLogoUrl helper (swaps SVG → img)', async () => {
|
|
const inp = await page.$('input[data-cv2-field="branding.logoUrl"]');
|
|
assert(inp, 'branding.logoUrl input missing');
|
|
const testUrl = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=';
|
|
await page.evaluate((args) => {
|
|
args.el.value = args.url;
|
|
args.el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}, { el: inp, url: testUrl });
|
|
await page.waitForTimeout(300);
|
|
// Brand logo node should now be <img>
|
|
const tag = await page.evaluate(() => {
|
|
const n = document.querySelector('.nav-brand .brand-logo');
|
|
return n ? n.tagName.toLowerCase() : null;
|
|
});
|
|
assert(tag === 'img', 'expected brand-logo to be <img> after logoUrl set, got: ' + tag);
|
|
const src = await page.evaluate(() => {
|
|
const n = document.querySelector('.nav-brand .brand-logo');
|
|
return n ? n.getAttribute('src') : null;
|
|
});
|
|
assert(src === testUrl, 'brand-logo src should match URL, got: ' + (src || '').slice(0, 40));
|
|
});
|
|
|
|
// ── #1518: branding.homeUrl override redirects nav-brand[href] ──
|
|
await step('#1518: branding.homeUrl override sets .nav-brand[href]', async () => {
|
|
const inp = await page.$('input[data-cv2-field="branding.homeUrl"]');
|
|
assert(inp, 'branding.homeUrl input missing — Branding tab must expose homeUrl field');
|
|
const target = 'https://example.com/embed-home';
|
|
await page.evaluate((args) => {
|
|
args.el.value = args.v;
|
|
args.el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}, { el: inp, v: target });
|
|
await page.waitForTimeout(500);
|
|
const href = await page.evaluate(() => {
|
|
const a = document.querySelector('a.nav-brand');
|
|
return a ? a.getAttribute('href') : null;
|
|
});
|
|
assert(href === target, '.nav-brand[href] should equal homeUrl override, got: ' + href);
|
|
});
|
|
|
|
await step('#1518: branding.homeUrl rejects javascript: scheme', async () => {
|
|
const inp = await page.$('input[data-cv2-field="branding.homeUrl"]');
|
|
await page.evaluate((el) => {
|
|
el.value = 'javascript:alert(1)';
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}, inp);
|
|
await page.waitForTimeout(500);
|
|
const href = await page.evaluate(() => {
|
|
const a = document.querySelector('a.nav-brand');
|
|
return a ? a.getAttribute('href') : null;
|
|
});
|
|
assert(href !== 'javascript:alert(1)', '.nav-brand[href] must NEVER be javascript:, got: ' + href);
|
|
});
|
|
|
|
await step('#1518: empty branding.homeUrl falls through to #/', async () => {
|
|
const inp = await page.$('input[data-cv2-field="branding.homeUrl"]');
|
|
await page.evaluate((el) => {
|
|
el.value = '';
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}, inp);
|
|
await page.waitForTimeout(500);
|
|
const href = await page.evaluate(() => {
|
|
const a = document.querySelector('a.nav-brand');
|
|
return a ? a.getAttribute('href') : null;
|
|
});
|
|
assert(href === '#/', '.nav-brand[href] should fall through to "#/" when homeUrl is empty, got: ' + href);
|
|
});
|
|
|
|
await step('branding overrides persist across reload', async () => {
|
|
await page.reload({ waitUntil: 'load' });
|
|
await page.waitForFunction(() => window._customizerV2 && window._customizerV2.initDone, null, { timeout: 8000 });
|
|
const title = await page.title();
|
|
// app.js applies branding.siteName to document.title on init (or customizer pipeline does)
|
|
// At minimum, the override is still in localStorage:
|
|
const raw = await page.evaluate(() => localStorage.getItem('cs-theme-overrides'));
|
|
const parsed = JSON.parse(raw);
|
|
assert(parsed.branding && parsed.branding.siteName === 'MyMeshTest',
|
|
'siteName override should persist, got: ' + raw);
|
|
});
|
|
|
|
await step('cleanup: clear overrides', async () => {
|
|
await page.evaluate(() => localStorage.removeItem('cs-theme-overrides'));
|
|
});
|
|
|
|
await browser.close();
|
|
console.log('\n' + passed + '/' + (passed + failed) + ' tests passed');
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
})();
|