- images.mjs: an embedded collection never has a caption (D47), so the caption option, its "Linked images" heading, the embedded count string and the heading arm of the focus fallback are removed; callers no longer pass caption:false. Behavior is unchanged. - workspace.css: delete the never-applied .recorded-run-dialog::backdrop rule (the concept's .ca-tools dialog::backdrop always won) and note that the D3 row's min-inline-size: 0 is load-bearing for the concept's <=700px 1fr track. - concept-css test: the waiting-rule guard is case-insensitive and is proven against the exact retired comments and the kept provenance labels. - history-concept spec: function replacement for the singular run label, and the DOM mutation removes Compare negatives by role. - D47 markers sit where the caption trim happens.
151 lines
9.1 KiB
JavaScript
151 lines
9.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import { createHash } from 'node:crypto';
|
||
import { spawnSync } from 'node:child_process';
|
||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, copyFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import test from 'node:test';
|
||
import { lintOverlay } from '../../tools/vendor/sync-concept-css.mjs';
|
||
|
||
const root = join(dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const script = join(root, 'tools/vendor/sync-concept-css.mjs');
|
||
const concept = join(root, 'docs/concepts/workspace/style.css');
|
||
const copy = join(root, 'web/js/workspace/concept.css');
|
||
const sha = path => createHash('sha256').update(readFileSync(path)).digest('hex');
|
||
|
||
function walk(dir) {
|
||
return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
||
const path = join(dir, entry.name);
|
||
return entry.isDirectory() ? walk(path) : [path];
|
||
});
|
||
}
|
||
|
||
test('concept copy is byte-identical', () => {
|
||
assert.ok(existsSync(copy), 'web/js/workspace/concept.css is missing; run sync-concept-css.mjs --write');
|
||
assert.equal(sha(copy), sha(concept));
|
||
const result = spawnSync(process.execPath, [script, '--check'], { cwd: root, encoding: 'utf8' });
|
||
assert.equal(result.status, 0, result.stderr);
|
||
});
|
||
|
||
test('check fails on a one-byte drift', () => {
|
||
const temp = mkdtempSync(join(tmpdir(), 'concept-css-'));
|
||
try {
|
||
mkdirSync(join(temp, 'docs/concepts/workspace'), { recursive: true });
|
||
mkdirSync(join(temp, 'web/js/workspace'), { recursive: true });
|
||
copyFileSync(concept, join(temp, 'docs/concepts/workspace/style.css'));
|
||
writeFileSync(join(temp, 'web/js/workspace/concept.css'), Buffer.concat([readFileSync(concept), Buffer.from(' ')]));
|
||
const run = () => spawnSync(process.execPath, [script, '--check', '--root', temp], { encoding: 'utf8' });
|
||
const drifted = run();
|
||
assert.equal(drifted.status, 1);
|
||
assert.match(drifted.stderr, /drift/i);
|
||
assert.equal(spawnSync(process.execPath, [script, '--write', '--root', temp]).status, 0);
|
||
assert.equal(run().status, 0);
|
||
} finally { rmSync(temp, { recursive: true, force: true }); }
|
||
});
|
||
|
||
test('overlay lint requires a decision marker on concept selectors', () => {
|
||
const conceptCss = '.card, .row { color: red; }\n@media (max-width: 700px) { .card .title { margin: 0; } }\n';
|
||
assert.deepEqual(lintOverlay(conceptCss, '.error-row { color: red; }').violations, []);
|
||
assert.equal(lintOverlay(conceptCss, '.x, .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '@media (max-width: 700px) { .card .title { margin: 1px; } }').violations.length, 1);
|
||
assert.deepEqual(lintOverlay(conceptCss, '/* D12 */\n.card { color: blue; }').violations, []);
|
||
assert.deepEqual(lintOverlay(conceptCss, '.card { /* D3 */ color: blue; }').violations, []);
|
||
assert.equal(lintOverlay(conceptCss, '/* tweak */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* D1 */ .card { color: blue; }\n.row { gap: 1px; }').violations.length, 1);
|
||
});
|
||
|
||
test('overlay lint accepts only this spec\'s (#249) decision numbers', () => {
|
||
const conceptCss = '.card { color: red; }\n';
|
||
assert.deepEqual(lintOverlay(conceptCss, '/* D12 of #249 */ .card { color: blue; }').violations, []);
|
||
assert.deepEqual(lintOverlay(conceptCss, '/* #249 D3: reason */ .card { color: blue; }').violations, []);
|
||
// Another issue's decision numbers are not #249 rulings, so they never mark a concept override.
|
||
assert.equal(lintOverlay(conceptCss, '/* D5 (#245) */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* D2 of #240 and #249 */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* #245 decision 5 */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* AD5 */ .card { color: blue; }').violations.length, 1);
|
||
});
|
||
|
||
test('overlay rules on concept selectors carry a decision marker', () => {
|
||
const overlay = join(root, 'web/js/workspace/workspace.css');
|
||
assert.ok(existsSync(overlay), 'web/js/workspace/workspace.css is missing');
|
||
const { violations } = lintOverlay(readFileSync(concept, 'utf8'), readFileSync(overlay, 'utf8'));
|
||
assert.deepEqual(violations, []);
|
||
});
|
||
|
||
// #249 PR F-b Task 3: every page is rebuilt, so no rule waits for a page PR any more and the history page's
|
||
// production-only rules live in the overlay in the concept's tokens (D48). A comment that opens with a page PR
|
||
// ("PR F: ...", "PR E/F: ...") or defers to one ("until ... PR", "retired in PR F") is a waiting rule; a provenance
|
||
// label inside a heading ("(#249 PR E)") is not.
|
||
const WAITING_RULE = [
|
||
/\/\*\s*PR [A-F](\/[A-F]|-[ab])?\b/i,
|
||
/\buntil\b[^*]*\b(PR [A-F]?|rebuild)/i,
|
||
/\bretired in PR\b/i,
|
||
/\binterim page guards\b/i,
|
||
];
|
||
const waitingRules = css => WAITING_RULE.filter(pattern => pattern.test(css)).map(String);
|
||
|
||
test('the waiting-rule guard catches the retired comments and spares provenance labels', () => {
|
||
// The exact comments this PR retired (workspace.css and library.css at 10a4847).
|
||
for (const retired of [
|
||
'/* Interim page guards: concept element rules (`.ca-tools dialog`, `pre`) now reach production dialogs and prompt text',
|
||
' that keep their own design until their page PR rebuilds them in concept markup. */',
|
||
'.ca-tools dialog.prompt-sender::backdrop { background: #000b; } /* PR D */',
|
||
'.ca-tools dialog.recorded-run-dialog { padding: 16px; } /* PR F */',
|
||
' .ca-tools .recorded-history .history-toolbar button { min-height: 44px; } /* PR F: concept coarse buttons are 40px */',
|
||
'/* Until their pages rebuild (PR F history): the history page\'s and the recorded run\'s prompt blocks keep their',
|
||
'/* PR E/F: the Archive dialog and the frozen field provenance keep `pre.prompt-text`, guarded against the concept `pre` rule. */',
|
||
" keeps the concept's) and are retired in PR F (history). */",
|
||
]) assert.notDeepEqual(waitingRules(retired), [], retired);
|
||
for (const kept of [
|
||
"/* Prompt library cards (#249 PR E): the concept's `article.prompt-card` (s653–708) with real prompts, titles and media. */",
|
||
'/* History variants panel (#249 PR F). */',
|
||
]) assert.deepEqual(waitingRules(kept), [], kept);
|
||
});
|
||
|
||
test('the overlay keeps no rule waiting for a page rebuild', () => {
|
||
const css = readFileSync(join(root, 'web/js/workspace/workspace.css'), 'utf8');
|
||
assert.deepEqual(waitingRules(css), []);
|
||
for (const retired of ['.library-chip', '--library-positive', '--library-negative', '--prompt-role']) {
|
||
assert.equal(css.includes(retired), false, `${retired} is retired`);
|
||
}
|
||
});
|
||
|
||
// #249 C8 and D3: the Images page and the inspector render the concept DOM, so the replaced production rules stay
|
||
// retired; the few production-only rules still consumed live in the overlay.
|
||
test('the overlay keeps no retired Images page or inspector rules', () => {
|
||
const css = readFileSync(join(root, 'web/js/workspace/workspace.css'), 'utf8');
|
||
// `.image-grid` and `.image-card-actions` are concept selectors now, so only the retired production names are listed.
|
||
for (const retired of ['.image-facts', '.image-filters', '.image-toolbar', '.image-preview', '.image-collection', '.image-continuation',
|
||
'.image-details', '.file-cover', '.file-inspector-inner', '.file-title', '.file-chip', '.file-facts', '.generation-facts', '.file-inspector-footer']) {
|
||
assert.equal(css.includes(retired), false, `${retired} is retired`);
|
||
}
|
||
});
|
||
|
||
test('only workspace.html loads workspace CSS', () => {
|
||
const offenders = walk(join(root, 'web'))
|
||
.filter(path => /\.(html|js|mjs)$/.test(path) && !path.includes('/lib/') && !path.includes('/shared/'))
|
||
.filter(path => /\b(concept|workspace)\.css\b/.test(readFileSync(path, 'utf8')))
|
||
.map(path => path.slice(root.length + 1));
|
||
assert.ok(offenders.every(path => path === 'web/workspace.html'), `unexpected consumers: ${offenders.join(', ')}`);
|
||
});
|
||
|
||
// Task A2 order: the concept's own order (toolkit, Viewer.js, concept), then the production overlay last; the page
|
||
// stylesheets that waited for their PRs are all retired (PR F-b deleted the last, library.css).
|
||
test('workspace.html loads controls, viewer, concept and the overlay in order', () => {
|
||
const html = readFileSync(join(root, 'web/workspace.html'), 'utf8');
|
||
const hrefs = [...html.matchAll(/<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"/g)].map(match => match[1]);
|
||
assert.deepEqual(hrefs, [
|
||
'js/shared/toolkit/controls.css',
|
||
'lib/viewerjs/viewer.min.css',
|
||
'js/workspace/concept.css',
|
||
'js/workspace/workspace.css',
|
||
]);
|
||
});
|
||
|
||
test('the retired shell, vocabulary, editor, Images, inspector and library stylesheets stay deleted', () => {
|
||
for (const name of ['style.css', 'vocabulary.css', 'library-editor.css', 'images.css', 'inspector.css', 'library.css']) {
|
||
assert.equal(existsSync(join(root, 'web/js/workspace', name)), false, `web/js/workspace/${name} must not come back`);
|
||
}
|
||
});
|