// 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);
});