// C2-11a. Unchanged files alone cannot detect escaped wikilinks: the editor
// keeps raw source. The browser checks the structured node and a dirty block.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { loadEditor } from './helpers/editor-bundle.mjs';
import { WIKILINK_CASES } from './helpers/wikilink-cases.mjs';
import { fixture } from './fixture.mjs';
import { browser } from './browser.mjs';
const editor = loadEditor();
for (const { name, text } of WIKILINK_CASES) test(`wikilink round-trip: ${name}`, () => {
assert.equal(editor.roundTrip(text), text);
assert.equal(editor.roundTrip(text.trimEnd()), text.trimEnd(), 'no trailing newline');
});
const page = async t => {
const f = await fixture(); t.after(() => f.close());
const b = await browser(); t.after(() => b.close());
await b.navigate(f.base);
return b;
};
const mount = text => `(() => {
window.__wikiEditor?.destroy();
const host = document.createElement('div'); document.body.append(host);
window.__wikiEditor = window.MosaicEditor.mount(host, {text: ${JSON.stringify(text)}});
return true;
})()`;
const atoms = `(() => {
const result = [];
window.__wikiEditor.view.state.doc.descendants(node => {
if (node.type.name === 'wikilink') result.push({source: node.attrs.source, atom: node.isAtom, inline: node.isInline});
}); return result;
})()`;
test('wikilinks are inline atoms and survive a neighbouring edit in every required context', { timeout: 120000 }, async t => {
const b = await page(t);
for (const { name, text, sources } of WIKILINK_CASES) {
await b.evaluate(mount(text));
assert.deepEqual(await b.evaluate(atoms), sources.map(source => ({ source, atom: true, inline: true })), name);
assert.equal(await b.evaluate('document.querySelectorAll(".md-wikilink a, .md-wikilink img, .md-wikilink iframe").length'), 0, 'no navigation or resource elements');
const changed = await b.evaluate(`(() => {
const e = window.__wikiEditor; let end;
e.view.state.doc.descendants((node, pos) => { if (node.isTextblock && node.type.name !== 'raw_block') end = pos + node.nodeSize - 1; });
e.view.dispatch(e.view.state.tr.insertText(' changed', end));
const output = e.text(); e.setText(output); return output;
})()`);
assert.ok(changed.includes(' changed'), name);
for (const source of sources) assert.ok(changed.includes(source), `${name}: spelling survives dirty-block serialisation`);
assert.deepEqual(await b.evaluate(atoms), sources.map(source => ({ source, atom: true, inline: true })), `${name}: survives reparse`);
if (text.startsWith('---')) assert.ok(changed.startsWith(text.slice(0, text.indexOf('\n\n') + 2)), 'front matter is untouched');
if (name === 'reference-definition collision')
assert.ok(changed.endsWith('\n\n[a]: u\n'), 'the separate reference-definition block keeps its exact bytes');
}
});
test('code, escaped openers, malformed syntax and HTML-like targets stay outside wikilink atoms', { timeout: 120000 }, async t => {
const b = await page(t);
const text = '\\[[escaped]] `[[inline-code]]`\n\n```\n[[fenced-code]]\n```\n\n[[open\n\n[[]]\n\n[[Q&A]]\n\n[[a`]]`\n\n[[
]]\n';
assert.equal(editor.roundTrip(text), text);
await b.evaluate(mount(text));
assert.deepEqual(await b.evaluate(atoms), []);
assert.equal(await b.evaluate('document.querySelector(".ProseMirror").querySelectorAll("img, iframe, script").length'), 0);
});
test('wikilink DOM paste accepts only bounded single-line inert syntax; malformed attrs serialise as escaped text', { timeout: 120000 }, async t => {
const b = await page(t); await b.evaluate(mount('[[a]]\n'));
const result = await b.evaluate(`(() => {
const e = window.__wikiEditor, spec = e.view.state.schema.nodes.wikilink.spec;
const attrs = source => spec.parseDOM[0].getAttrs({getAttribute: () => source});
const invalid = ['not a link', '[[a]]\\n\\n', '[[
]]', '[[a©]]', null].map(source => attrs(source));
const bad = e.view.state.schema.nodes.wikilink.create({source:'[[a]]\\n\\n
©'});
e.view.dispatch(e.view.state.tr.replaceWith(1, e.view.state.doc.content.size - 1, bad));
return {valid:attrs('[[a#h|label]]'), invalid, output:e.text()};
})()`);
assert.deepEqual(result.valid, { source: '[[a#h|label]]' });
assert.deepEqual(result.invalid, [false, false, false, false, false]);
assert.ok(!result.output.includes('\n\n![]()