// Row 154 (#1648, Q168): the Console's visual editor, the parts that need no browser. The routes, the // round trip (open and save with no edit writes the same bytes), and the rules the editor source keeps. // The imported source and the build are editor-provenance.test.mjs's; the page is editor-browser.test.mjs's. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, readdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadEditor } from './helpers/editor-bundle.mjs'; import { EVERY } from './helpers/every-element.mjs'; import { fixture } from './fixture.mjs'; const webui = join(dirname(fileURLToPath(import.meta.url)), '..'); const repo = join(webui, '../..'); const editor = loadEditor(); test('the editor routes serve the built files with their types and no cache', async t => { const f = await fixture(); t.after(() => f.close()); for (const [path, type] of [['/editor.js', 'text/javascript'], ['/editor.css', 'text/css']]) { const response = await fetch(f.base + path); assert.equal(response.status, 200, path); assert.ok(response.headers.get('content-type').startsWith(type), path); assert.equal(response.headers.get('cache-control'), 'no-store', path); assert.equal(await response.text(), readFileSync(join(webui, 'src/public', path), 'utf8'), `${path} is the built file`); } const index = await (await fetch(f.base + '/')).text(); assert.match(index, //); assert.ok(index.indexOf('src="/editor.js"') < index.indexOf('src="/bus.js"'), 'the editor loads before the page script, so bus.js can mount it'); }); test('open and save with no edit writes the same bytes: the goals review and a doc with every element', () => { const goals = readFileSync(join(repo, 'docs/plans/2026-09-27_goals-review.md'), 'utf8'); assert.equal(editor.roundTrip(goals), goals); assert.equal(editor.roundTrip(EVERY), EVERY); // the same with the other line ending conventions a file can end in for (const text of ['', '\n', '\n\n\n', 'no newline', '# T\n', '# T\n\n\n\ntext\n\n\n', '\n\n indented first line\n']) assert.equal(editor.roundTrip(text), text, JSON.stringify(text)); }); test('open and save with no edit writes the same bytes: every repository doc under docs/ and spec/', () => { const found = []; const walk = dir => { for (const e of readdirSync(dir, { withFileTypes: true })) { if (e.isDirectory()) { if (!['node_modules', '.git', 'v1'].includes(e.name)) walk(join(dir, e.name)); } else if (e.name.endsWith('.md')) found.push(join(dir, e.name)); } }; for (const d of ['docs', 'spec']) { try { walk(join(repo, d)); } catch (e) { if (e.code !== 'ENOENT') throw e; } } assert.ok(found.length > 20, `found ${found.length} docs`); const changed = []; for (const file of found) { const text = readFileSync(file, 'utf8').replace(/\r\n/g, '\n'); // CRLF is saved as LF, as row 148's save does if (editor.roundTrip(text) !== text) changed.push(file.slice(repo.length + 1)); } assert.deepEqual(changed, [], 'files the editor would change on an open and save with no edit'); }); test('the editor source builds markup only with DOM calls, and the bundle has no eval', () => { const sinks = /\binnerHTML\b|\bouterHTML\b|\binsertAdjacentHTML\b|\bdocument\.write\b|\beval\s*\(|\bnew\s+Function\b|\bsrcdoc\b|\bcreateContextualFragment\b|\bDOMParser\b/; for (const file of ['entry.mjs', 'model.mjs', 'view.mjs']) { const source = readFileSync(join(webui, 'editor', file), 'utf8').split('\n').filter(l => !l.trim().startsWith('//')).join('\n'); assert.doesNotMatch(source, sinks, `editor/${file}`); } const bundle = readFileSync(join(webui, 'src/public/editor.js'), 'utf8'); assert.doesNotMatch(bundle, /\beval\s*\(|\bnew\s+Function\b/, 'src/public/editor.js'); }); test('the link gate and the image rule are the preview\'s', () => { const model = readFileSync(join(webui, 'editor/model.mjs'), 'utf8'); assert.match(model, /globalThis\.MosaicMarkdown\?\.linkTarget/, 'the link target goes through the one gate'); assert.doesNotMatch(model, /tag:\s*'img/, 'no schema rule makes an '); // a file's link the gate refuses stays source text, byte for byte, on a round trip and an edit-free save const hostile = '[a](javascript:alert(1)) [b](data:text/html,x) [c](vbscript:x) [d](//evil.example/x) [e](https://ok.example/)\n'; assert.equal(editor.roundTrip(hostile), hostile); });