Three creation gestures + shared form modal for the v4.2 annotation playground (per critical decisions #2-#4 + research-06): Gesture 1 — text-anchored adder-popup: - mouseup-debounce 200ms (settles selection) - 300ms grace before hide (Hypothes.is friction-mitigation) - floating .voyage-adder-popup positioned at selection-bound corner - click -> opens form modal with derived heading-path + line + snippet Gesture 2 — paragraph-anchored hover-icon: - 24px pencil SVG injected per <p>/<li> after each render - opacity 0 default, opacity 1 on hover/focus-visible - click -> opens form modal with no snippet Gesture 3 — page-level note: - .voyage-page-note-btn injected in viewport - click -> opens form modal with target=page Form modal (shared): - role="dialog" aria-modal="true" + aria-labelledby - 400px right-anchored on backdrop (per critical decision #3) - 4 intent buttons (Fiks / Endre / Spørsmål / Block) as aria-pressed group - <textarea> required for comment - ESC + backdrop-click + Avbryt close - Lagre persists via saveModalAsAnnotation Anchor-ID generation (per critical decision #2 + risk-assessor H7): - sequential ANN-NNNN per project+file scope - persisted in localStorage under voyage_ann_<project>__<file>.drafts Test coverage: tests/playground/voyage-playground.test.mjs +3 cases — aria-modal, ANN-, 300ms grace. Verify: node --test tests/playground/voyage-playground.test.mjs -> 16 pass / 0 fail. Full npm test: 590 pass / 0 fail / 2 skipped (Docker). Refs plan.md Step 9 + critical decisions #2/#3/#4 + research-06.
114 lines
5.2 KiB
JavaScript
114 lines
5.2 KiB
JavaScript
// tests/playground/voyage-playground.test.mjs
|
|
// Filesystem + content tests for v4.2 voyage playground.
|
|
// Pure existence + grep checks — no browser launch.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { existsSync, statSync, readFileSync, readdirSync } from 'node:fs';
|
|
import { dirname, resolve, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = resolve(__dirname, '..', '..');
|
|
const PLAYGROUND = join(ROOT, 'playground');
|
|
const HTML = join(PLAYGROUND, 'voyage-playground.html');
|
|
const VENDOR = join(PLAYGROUND, 'vendor', 'playground-design-system');
|
|
const MANIFEST = join(VENDOR, 'MANIFEST.json');
|
|
|
|
test('voyage-playground.html exists and has nonzero size', () => {
|
|
assert.ok(existsSync(HTML), 'voyage-playground.html must exist');
|
|
assert.ok(statSync(HTML).size > 0, 'must have content');
|
|
});
|
|
|
|
test('voyage-playground.html has DOCTYPE + html closing tag', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /^<!DOCTYPE html>/i);
|
|
assert.match(text, /<\/html>\s*$/);
|
|
});
|
|
|
|
test('voyage-playground.html does NOT contain external (http/https) URLs', () => {
|
|
// SC1 zero-network constraint: all assets must be relative to ./vendor/
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.ok(!/https?:\/\//.test(text), 'no external URLs allowed in playground HTML');
|
|
});
|
|
|
|
test('voyage-playground.html does NOT contain literal `marked` (renderer ban per risk-assessor H1)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
// marked is disqualified by issue #3515; markdown-it locked instead
|
|
// Allow comments mentioning "marked" as an explanatory artifact, but no actual import paths
|
|
assert.ok(!/from ['"].*marked/.test(text), 'no import from marked');
|
|
assert.ok(!/<script[^>]*marked\.min\.js/.test(text), 'no marked script tag');
|
|
});
|
|
|
|
test('voyage-playground.html includes skip-to-main link (A11Y baseline)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /Skip to main content/);
|
|
});
|
|
|
|
test('voyage-playground.html declares aria-live region', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /aria-live="polite"/);
|
|
});
|
|
|
|
test('playground/vendor/playground-design-system/MANIFEST.json exists and parses as JSON with expected keys', () => {
|
|
assert.ok(existsSync(MANIFEST), 'MANIFEST.json must be present from sync-design-system.mjs');
|
|
const obj = JSON.parse(readFileSync(MANIFEST, 'utf-8'));
|
|
assert.ok(obj.source_commit, 'source_commit field required');
|
|
assert.ok(obj.sync_date, 'sync_date field required');
|
|
assert.ok(obj.files && typeof obj.files === 'object', 'files map required');
|
|
});
|
|
|
|
test('playground/vendor/playground-design-system/ contains expected DS files', () => {
|
|
const files = readdirSync(VENDOR);
|
|
for (const expected of ['tokens.css', 'base.css', 'components.css', 'fonts.css', 'print.css']) {
|
|
assert.ok(files.includes(expected), `${expected} expected in vendor/`);
|
|
}
|
|
assert.ok(files.includes('fonts'), 'fonts/ subdirectory expected');
|
|
});
|
|
|
|
// --- Step 8 — render pipeline + vendored libs ---------------------------
|
|
|
|
const PLAYGROUND_LIB = join(PLAYGROUND, 'lib');
|
|
|
|
test('voyage-playground.html references markdown-it (Step 8 render pipeline)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /markdown-it/, 'voyage-playground.html should load/initialize markdown-it');
|
|
});
|
|
|
|
test('voyage-playground.html references highlight.js (Step 8 syntax highlighting)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /highlight/, 'voyage-playground.html should load highlight.js');
|
|
});
|
|
|
|
test('voyage-playground.html includes paste-import-row (Step 8 import affordance)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /paste-import-row/, 'voyage-playground.html should include the paste-import-row pattern');
|
|
});
|
|
|
|
test('voyage-playground.html declares voyage_ann_ localStorage key prefix (Step 8 risk-assessor H7)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /voyage_ann_/, 'localStorage key prefix voyage_ann_<project>__<file> must appear');
|
|
});
|
|
|
|
test('playground/lib/ contains vendored markdown-it + front-matter + highlight bundles', () => {
|
|
for (const f of ['markdown-it.min.js', 'markdown-it-front-matter.min.js', 'highlight.min.js', 'VENDOR-MANIFEST.json']) {
|
|
assert.ok(existsSync(join(PLAYGROUND_LIB, f)), `playground/lib/${f} expected from vendor-playground-libs.mjs`);
|
|
}
|
|
});
|
|
|
|
// --- Step 9 — annotation creation gestures + form modal ---------------
|
|
|
|
test('voyage-playground.html declares aria-modal="true" (Step 9 form modal A11Y)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /aria-modal="true"/, 'form modal must carry aria-modal="true"');
|
|
});
|
|
|
|
test('voyage-playground.html declares ANN- anchor-ID prefix (Step 9 ID generation)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /ANN-/, 'sequential ANN-NNNN ID generation must appear in playground JS');
|
|
});
|
|
|
|
test('voyage-playground.html declares 300ms grace constant (Step 9 adder-popup grace)', () => {
|
|
const text = readFileSync(HTML, 'utf-8');
|
|
assert.match(text, /300\s*ms|GRACE_MS\s*=\s*300|ADDER_GRACE_MS/i, '300ms grace period for adder-popup must be present');
|
|
});
|