May 2026 reshape. archive/ is now the only physical project-root
directory; working/, staging/, reviewing/ move from the project root
into each archive/<party>/ folder. Six top-level URLs become virtual
aggregators served via the cascade rather than disk:
ssr/mdl/rsk tables rollups across parties with a
synthesised $party source-party column
working/staging/ browse folder-nav listings of parties with
reviewing non-empty content in the slot; per-party
URLs 302-redirect to archive/<party>/<slot>/
Mkdir at the project root is restricted to `archive` and `_`/`.`-
prefixed system names — virtual aggregator names and ad-hoc folders
return 409.
Plan Review hardcodes the scaffold convention (archive/<party>/
{reviewing,staging}/<tracking>/); the pre-reshape
on_plan_review.{reviewing_root,staging_root} cascade keys are dropped.
document_controller is now subtree-admin of every archive/<party>/
(not of project-root working/staging/ as before), so per-party
lifecycle slots inherit admin authority through the cascade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3.3 KiB
JavaScript
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/archive/Acme/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/archive/Acme/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);
|
|
});
|
|
});
|