'use strict'; /** * 投稿编辑器正文基础插件(front-submit-editor-basics.js)的行为测试。 * * 用 jsdom 模拟 TinyMCE 暴露给插件的 API(getBody / dom / selection / getParam / * 事件总线),直接跑真实的插件源码。 * * 两个回归点: * 1. 占位符曾以 `data-mce-bogus` 段落插进正文 —— TinyMCE 在 setContent 时会清掉它, * 表现为「首次加载不显示,输入一个再删掉才出现」;现在改为 body 属性 + CSS ::before。 * 2. 光标在块级结构里时点击正文空白处无法落回正文;现在点击 body 会补一个可编辑 * 尾段并把光标放进去。 */ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const { JSDOM } = require(path.join(root, 'assets/creator-studio/node_modules/jsdom')); const pluginSource = fs.readFileSync( path.join(root, 'assets/js/front-submit-editor-basics.js'), 'utf8' ); /** 极简 TinyMCE 替身:只实现插件实际用到的部分。 */ function createEditor(html, settings = {}) { const dom = new JSDOM(`
${html}`); const doc = dom.window.document; const listeners = {}; const state = { selectionNode: doc.body, caret: null }; const editor = { settings, id: 'jio-test-editor', getParam(name, fallback) { return Object.prototype.hasOwnProperty.call(settings, name) ? settings[name] : fallback; }, getBody: () => doc.body, getDoc: () => doc, dom: { create(tag, attrs, inner) { const node = doc.createElement(tag); Object.entries(attrs || {}).forEach(([key, value]) => node.setAttribute(key, String(value))); if (inner !== undefined) node.innerHTML = inner; return node; }, encode: (value) => String(value), getParent: (node, selector) => (node && node.closest ? node.closest(selector) : null), }, selection: { getNode: () => state.selectionNode, setCursorLocation(node, offset) { state.caret = { node, offset }; }, }, on(names, handler) { String(names) .split(/\s+/) .filter(Boolean) .forEach((name) => { (listeners[name] = listeners[name] || []).push(handler); }); }, fire(name, payload = {}) { (listeners[name] || []).forEach((handler) => handler(payload)); }, }; return { dom, doc, editor, state }; } function loadPlugin() { const registered = {}; const tinymce = { PluginManager: { add(name, factory) { registered[name] = factory; }, }, }; const fakeWindow = { tinymce, setTimeout: (fn) => fn() }; // 插件是 IIFE,只依赖传入的 window;用 Function 注入避免污染全局。 new Function('window', pluginSource)(fakeWindow); return registered; } const PLUGIN = 'jio_editor_basics'; test('placeholder is flagged on first load of an empty document', () => { const { doc, editor } = createEditor('
正文
code');
const plugin = loadPlugin()[PLUGIN];
plugin(editor);
editor.fire('init');
state.selectionNode = doc.querySelector('pre');
editor.fire('click', { target: doc.body });
const last = doc.body.lastElementChild;
assert.equal(last.tagName, 'P', 'a paragraph must be appended after the structural block');
assert.equal(state.caret && state.caret.node, last, 'caret must move into that paragraph');
});
test('clicking blank space while the caret is already in text does not move it', () => {
const { doc, editor, state } = createEditor('正文
第二段
'); const plugin = loadPlugin()[PLUGIN]; plugin(editor); editor.fire('init'); state.selectionNode = doc.querySelectorAll('p')[1]; editor.fire('click', { target: doc.body }); assert.equal(state.caret, null, 'browser-placed caret must be left alone'); assert.equal(doc.body.children.length, 2, 'no extra paragraph is added'); }); test('clicking a nested editable block does not append a trailing paragraph', () => { const { doc, editor, state } = createEditor( '引用内容