ZDDC/tests/logo.spec.js
ZDDC 7fd96c7c78 feat(shared): clickable logo links every tool's header to project home
The .app-header__logo SVG was decorative on every tool. Web's
strongest convention is "click logo → go home" — so users tapping
it expecting that fallback got nothing. Now the logo is wrapped in
an anchor whose href reflects the URL the page was loaded from:

  file://                    → no wrap (no server home to point at)
  /                          → wrap, href=/         (deployment root)
  /index.html / /<tool>.html → wrap, href=/         (root, no project)
  /<project>/...             → wrap, href=/<project> (project landing)

The wrap happens client-side at DOMContentLoaded via shared/logo.js,
loaded by every tool's build.sh after toast/nav. Idempotent — a
template-supplied anchor or a second mount call is a no-op.

The companion shared/logo.css adds a subtle hover/focus affordance
(opacity 0.82, focus ring) so the logo reads as clickable without
otherwise altering its visual weight. Tools opt out by setting
window.zddc.logo.disabled = true before DOMContentLoaded (e.g. for
deployments that pin the logo to an external destination).

Five Playwright tests (tests/logo.spec.js) lock the contract:
no-wrap on file://, href=/ at root, href=/<project> in project
subtree, aria-label matches target, idempotent re-mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 07:34:28 -05:00

85 lines
3.3 KiB
JavaScript

// Tests for shared/logo.js — turns the .app-header__logo SVG into a
// link to the project landing (or deployment root). Mounts at
// DOMContentLoaded across every tool's bundle.
//
// Strategy: serve any tool's compiled HTML at multiple URL paths via a
// tiny in-process HTTP server, so we can verify the wrapping anchor's
// href reflects the URL the page was loaded from.
import { test, expect } from '@playwright/test';
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
const HTML_PATH = path.resolve('classifier/dist/classifier.html');
let server;
let baseUrl;
test.beforeAll(async () => {
const html = fs.readFileSync(HTML_PATH, 'utf8');
server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
baseUrl = `http://127.0.0.1:${server.address().port}`;
});
test.afterAll(async () => {
if (server) await new Promise(r => server.close(r));
});
test.describe('shared/logo.js', () => {
test('does NOT wrap the logo on file://', async ({ page }) => {
const filePath = path.resolve('classifier/dist/classifier.html');
await page.goto(`file://${filePath}`, { waitUntil: 'load' });
const wrapped = await page.evaluate(() => {
const logo = document.querySelector('.app-header__logo');
return logo && logo.parentElement && logo.parentElement.tagName === 'A';
});
expect(wrapped).toBe(false);
});
test('wraps with href=/ at the deployment root', async ({ page }) => {
await page.goto(`${baseUrl}/`, { waitUntil: 'load' });
const got = await page.evaluate(() => {
const a = document.querySelector('.app-header__logo-link');
return a && a.getAttribute('href');
});
expect(got).toBe('/');
});
test('wraps with href=/<project> when inside a project subtree', async ({ page }) => {
await page.goto(`${baseUrl}/Project-1/working/casey/notes.md`, { waitUntil: 'load' });
const got = await page.evaluate(() => {
const a = document.querySelector('.app-header__logo-link');
return a && a.getAttribute('href');
});
expect(got).toBe('/Project-1');
});
test('the wrapper carries an aria-label matching its target', async ({ page }) => {
await page.goto(`${baseUrl}/Project-1/staging/`, { waitUntil: 'load' });
const probe = await page.evaluate(() => {
const a = document.querySelector('.app-header__logo-link');
return a && {
href: a.getAttribute('href'),
label: a.getAttribute('aria-label'),
};
});
expect(probe.href).toBe('/Project-1');
expect(probe.label).toBe('Project home');
});
test('mount is idempotent — already-wrapped logo is left alone', async ({ page }) => {
await page.goto(`${baseUrl}/Project-1/`, { waitUntil: 'load' });
const wrapperCount = await page.evaluate(() => {
// Run mount a second time — should be a no-op.
window.zddc.logo.mount();
return document.querySelectorAll('.app-header__logo-link').length;
});
expect(wrapperCount).toBe(1);
});
});