<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cherry Editor - Toolbar API 完整测试</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
}
.control-panel {
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
background: #fff;
border: 2px solid #3582fb;
border-radius: 8px;
padding: 10px 16px;
box-shadow: 0 4px 16px rgba(0,0,0,.12);
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
max-width: 95vw;
}
.control-panel h3 {
margin: 0 8px 0 0;
color: #333;
font-size: 13px;
white-space: nowrap;
}
button {
padding: 5px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all .15s;
white-space: nowrap;
font-weight: 500;
}
button:hover { opacity: .9; transform: translateY(-1px); }
button:active { transform: translateY(0); }
.btn-run-all { background: linear-gradient(135deg,#667eea,#764ba2); color: #fff; }
.btn-reset { background: #6b7280; color: #fff; }
.btn-group { display: inline-flex; align-items: center; gap: 4px; margin: 0 4px; padding-right: 8px; border-right: 1px solid #ddd; }
.btn-group:last-child { border-right: none; }
.report-panel {
position: fixed;
bottom: 0; left: 0; right: 0;
z-index: 9999;
background: rgba(15,23,42,.94);
color: #e2e8f0;
border-top: 3px solid #3582fb;
font-family: 'SF Mono', Monaco, 'Menlo', monospace;
font-size: 12px;
line-height: 1.5;
max-height: 45vh;
overflow-y: auto;
transition: transform .25s ease;
transform: translateY(calc(100% - 32px));
}
.report-panel.expanded { transform: translateY(0); }
.report-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 16px;
cursor: pointer;
user-select: none;
background: rgba(255,255,255,.04);
}
.report-header:hover { background: rgba(255,255,255,.08); }
.report-summary {
display: flex;
gap: 16px;
font-weight: 600;
}
.stat-pass { color: #34d399; }
.stat-fail { color: #f87171; }
.stat-total { color: #93c5fd; }
.report-toggle { font-size: 11px; color: #94a3b8; cursor: pointer; }
.report-body { padding: 8px 16px 16px; }
.test-case {
padding: 6px 10px;
margin: 4px 0;
border-radius: 4px;
background: rgba(255,255,255,.03);
border-left: 3px solid transparent;
}
.test-case.pass { border-left-color: #34d399; }
.test-case.fail { border-left-color: #f87171; background: rgba(248,113,113,.08); }
.test-name { font-weight: 600; color: #e2e8f0; }
.test-detail { color: #94a3b8; font-size: 11px; margin-top: 2px; }
.test-assert { color: #a78bfa; font-size: 11px; margin-top: 2px; }
.assert-ok::before { content: '✓ '; color: #34d399; }
.assert-err::before { content: '✗ '; color: #f87171; }
#markdown { min-height: 60vh; padding-top: 56px; }
</style>
<link rel="stylesheet" type="text/css" href="../packages/cherry-markdown/dist/cherry-markdown.css" />
<link rel="Shortcut Icon" href="./logo/favicon.ico" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" crossorigin="anonymous">
</head>
<body>
<div class="control-panel">
<h3>Toolbar API Test Suite</h3>
<button class="btn-run-all" onclick="runAllTests()">▶ 运行全部测试</button>
<button class="btn-reset" onclick="resetEditor()">↺ 重置编辑器</button>
<span class="btn-group">
<button class="primary" style="background:#3582fb;color:#fff" onclick="manual('resetAdd')">resetToolbar +按钮</button>
<button class="danger" style="background:#ef4444;color:#fff" onclick="manual('resetClear')">resetToolbar 清空</button>
</span>
<span class="btn-group">
<button onclick="manual('epShow')" style="background:#22c55e;color:#fff">e&p+显TB</button>
<button onclick="manual('epHide')" style="background:#f59e0b;color:#fff">e&p+隐TB</button>
</span>
<span class="btn-group">
<button onclick="manual('eoShow')" style="background:#06b6d4;color:#fff">eo+显TB</button>
<button onclick="manual('eoHide')" style="background:#f97316;color:#fff">eo+隐TB</button>
</span>
<span class="btn-group">
<button onclick="manual('po')" style="background:#8b5cf6;color:#fff">previewOnly</button>
<button onclick="manual('recover')" style="background:#ec4899;color:#fff">恢复双栏</button>
</span>
</div>
<div id="markdown"></div>
<div id="reportPanel" class="report-panel">
<div class="report-header" onclick="toggleReport()">
<div class="report-summary">
<span id="summaryText">点击展开/折叠报告</span>
<span id="passStat" class="stat-pass"></span>
<span id="failStat" class="stat-fail"></span>
<span id="totalStat" class="stat-total"></span>
</div>
<span class="report-toggle" id="toggleLabel">▲ 展开详情</span>
</div>
<div id="reportBody" class="report-body"></div>
</div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
<script src="../packages/cherry-markdown/dist/cherry-markdown.js"></script>
<script type="module">
let testResults = [];
function assert(condition, message) {
return { ok: condition, msg: message };
}
async function testCase(name, fn) {
const result = { name, asserts: [], passed: true };
try {
const assertions = await fn();
result.asserts = assertions;
result.passed = assertions.every(a => a.ok);
} catch (err) {
result.asserts = [{ ok: false, msg: `异常: ${err.message}` }];
result.passed = false;
}
testResults.push(result);
renderResult(result);
return result.passed;
}
function renderResult(r) {
const body = document.getElementById('reportBody');
const el = document.createElement('div');
el.className = `test-case ${r.passed ? 'pass' : 'fail'}`;
const assertHtml = r.asserts.map(a =>
`<div class="test-assert ${a.ok ? 'assert-ok' : 'assert-err'}">${a.msg}</div>`
).join('');
el.innerHTML = `
<div class="test-name">${r.passed ? '✅' : '❌'} ${r.name}</div>
${assertHtml}
`;
body.appendChild(el);
updateSummary();
}
function updateSummary() {
const total = testResults.length;
const pass = testResults.filter(r => r.passed).length;
const fail = total - pass;
document.getElementById('passStat').textContent = `PASS ${pass}`;
document.getElementById('failStat').textContent = `FAIL ${fail}`;
document.getElementById('totalStat').textContent = `TOTAL ${total}`;
document.getElementById('summaryText').textContent =
fail === 0
? `🎉 全部通过! (${total}/${total})`
: `⚠️ ${fail} 个测试未通过 (${pass}/${total})`;
}
function getSnapshot() {
const c = window.cherry;
if (!c) return null;
const wrapper = c.wrapperDom;
const toolbarEl = wrapper?.querySelector('.cherry-toolbar');
return {
shouldHide: c.shouldHideToolbar(),
noToolbarClass: wrapper?.classList.contains('cherry--no-toolbar') ?? null,
toolbarInDOM: toolbarEl ? wrapper.contains(toolbarEl) : false,
toolbarExists: !!toolbarEl,
toolbarDisplay: toolbarEl ? getComputedStyle(toolbarEl).display : null,
toolbarConfig: c.options.toolbars.toolbar,
toolbarRightConfig: c.options.toolbars.toolbarRight,
previewerHidden: c.previewer?.isPreviewerHidden?.() ?? null,
childrenOrder: Array.from(wrapper?.children || []).map(el =>
el.tagName.toLowerCase() + (el.className ? '.' + Array.from(el.classList).slice(0,2).join('.') : '')
),
};
}
function snapStr(s) {
if (!s) return '(null)';
return JSON.stringify({
shouldHide: s.shouldHide,
noTBCss: s.noToolbarClass,
inDOM: s.toolbarInDOM,
display: s.toolbarDisplay,
tbLen: s.toolbarConfig?.length,
tbrLen: s.toolbarRightConfig?.length,
prevHidden: s.previewerHidden,
});
}
let cherryInstance = null;
function createCherry(configOverrides = {}) {
const base = {
id: 'markdown',
value: '# Toolbar API Test\n\n## 测试区域\n\n在上方面板操作或点击「运行全部测试」。\n',
externals: { echarts: window.echarts },
toolbars: {
toolbar: [],
toolbarRight: [],
bubble: false,
float: false,
},
editor: {
defaultModel: 'edit&preview',
codemirror: { placeholder: '输入内容测试...' },
},
};
if (cherryInstance) {
try { cherryInstance.destroy(); } catch(e) {}
document.getElementById('markdown').innerHTML = '';
}
cherryInstance = new Cherry(Object.assign({}, base, configOverrides, {
toolbars: Object.assign({}, base.toolbars, configOverrides.toolbars || {}),
editor: Object.assign({}, base.editor, configOverrides.editor || {}),
}));
window.cherry = cherryInstance;
return new Promise(resolve => setTimeout(resolve, 300));
}
window.resetEditor = async function() {
await createCherry();
console.log('[RESET] 编辑器已重置');
};
window.manual = async function(action) {
switch(action) {
case 'resetAdd':
cherryInstance.resetToolbar('toolbar', ['bold','italic','header','list']);
break;
case 'resetClear':
cherryInstance.resetToolbar('toolbar', []);
break;
case 'epShow': cherryInstance.switchModel('edit&preview', true); break;
case 'epHide': cherryInstance.switchModel('edit&preview', false); break;
case 'eoShow': cherryInstance.switchModel('editOnly', true); break;
case 'eoHide': cherryInstance.switchModel('editOnly', false); break;
case 'po': cherryInstance.switchModel('previewOnly'); break;
case 'recover': cherryInstance.switchModel('edit&preview', true); break;
}
logSnap(action);
};
function logSnap(label) {
const s = getSnapshot();
console.log(`[${label}]`, snapStr(s));
}
async function T01_初始空工具栏() {
await createCherry({ toolbars: { toolbar: [], toolbarRight: [] } });
const s = getSnapshot();
return [
assert(s.toolbarConfig.length === 0, `toolbar 长度应为 0,实际 ${s.toolbarConfig?.length}`),
assert(s.toolbarRightConfig.length === 0, `toolbarRight 长度应为 0`),
assert(!s.toolbarInDOM || !s.toolbarDisplay || s.toolbarDisplay === 'none',
`toolbar 不应在页面中可见 (inDOM=${s.toolbarInDOM}, display=${s.toolbarDisplay})`),
];
}
async function T02_resetToolbar从空添加按钮() {
await createCherry({ toolbars: { toolbar: [], toolbarRight: [] } });
cherryInstance.resetToolbar('toolbar', ['bold','italic']);
await delay(200);
const s = getSnapshot();
return [
assert(s.toolbarConfig.length > 0, `toolbar 应有按钮,实际长度 ${s.toolbarConfig?.length}`),
assert(s.toolbarInDOM, `toolbar 应在 DOM 树中`),
assert(s.toolbarDisplay !== 'none', `toolbar display 应不为 none,实际 ${s.toolbarDisplay}`),
];
}
async function T03_resetToolbar清空() {
await createCherry({ toolbars: { toolbar: ['bold'], toolbarRight: ['fullScreen'] } });
cherryInstance.resetToolbar('toolbar', []);
await delay(200);
const s = getSnapshot();
return [
assert(s.toolbarConfig.length === 0, `toolbar 应为空`),
assert(s.noToolbarClass, `应有 cherry--no-toolbar 类`),
];
}
async function T04_switchModel_editPreview_showToolbar() {
await createCherry({ toolbars: { toolbar: ['bold'] } });
cherryInstance.switchModel('edit&preview', true);
await delay(150);
const s = getSnapshot();
return [
assert(!s.noToolbarClass, `不应有 cherry--no-toolbar 类`),
assert(s.toolbarInDOM, `toolbar 应在 DOM 中`),
];
}
async function T05_switchModel_editPreview_hideToolbar() {
await createCherry({ toolbars: { toolbar: ['bold'] } });
cherryInstance.switchModel('edit&preview', false);
await delay(150);
const s = getSnapshot();
return [
assert(s.noToolbarClass, `应有 cherry--no-toolbar 类 (实际: ${s.noToolbarClass})`),
assert(s.toolbarInDOM, `toolbar DOM 应仍在树中(CSS隐藏方式)`),
];
}
async function T06_switchModel_editOnly_showToolbar() {
await createCherry({ toolbars: { toolbar: ['bold'] } });
cherryInstance.switchModel('editOnly', true);
await delay(150);
const s = getSnapshot();
return [
assert(s.previewerHidden === true, `预览区应被隐藏 (实际: ${s.previewerHidden})`),
assert(!s.noToolbarClass, `不应有 cherry--no-toolbar 类`),
assert(s.toolbarInDOM, `toolbar 应在 DOM 中`),
];
}
async function T07_switchModel_editOnly_hideToolbar() {
await createCherry({ toolbars: { toolbar: ['bold'] } });
cherryInstance.switchModel('editOnly', false);
await delay(150);
const s = getSnapshot();
return [
assert(s.previewerHidden === true, `预览区应被隐藏`),
assert(s.noToolbarClass, `应有 cherry--no-toolbar 类`),
];
}
async function T08_switchModel_previewOnly() {
await createCherry({ toolbars: { toolbar: ['bold'] } });
cherryInstance.switchModel('previewOnly');
await delay(150);
const s = getSnapshot();
return [
assert(s.noToolbarClass, `应有 cherry--no-toolbar 类`),
];
}
async function T09_先resetToolbar后switchModel显示() {
await createCherry({ toolbars: { toolbar: [], toolbarRight: [] } });
cherryInstance.resetToolbar('toolbar', ['bold','italic']);
await delay(100);
cherryInstance.switchModel('editOnly', true);
await delay(150);
const s = getSnapshot();
return [
assert(s.toolbarInDOM, `resetToolbar后 switchModel 显示: toolbar应在DOM中 (inDOM=${s.toolbarInDOM})`),
assert(!s.noToolbarClass, `不应有 no-toolbar 类`),
assert(s.previewerHidden === true, `预览区应隐藏`),
];
}
async function T10_先resetToolbar清空后switchModel隐藏() {
await createCherry({ toolbars: { toolbar: ['bold'] } });
cherryInstance.resetToolbar('toolbar', []);
await delay(100);
cherryInstance.switchModel('editOnly', false);
await delay(150);
const s = getSnapshot();
return [
assert(s.noToolbarClass,
`清空后隐藏: 应有 cherry--no-toolbar 类`),
];
}
async function T11_多次连续resetToolbar() {
await createCherry({ toolbars: { toolbar: [], toolbarRight: [] } });
cherryInstance.resetToolbar('toolbar', ['bold']);
await delay(50);
cherryInstance.resetToolbar('toolbar', ['bold','italic','header']);
await delay(50);
cherryInstance.resetToolbar('toolbar', []);
await delay(200);
const s = getSnapshot();
return [
assert(s.toolbarConfig.length === 0, `最终 toolbar 应为空 (长度=${s.toolbarConfig?.length})`),
assert(s.noToolbarClass, `最终应有 cherry--no-toolbar 类`),
];
}
async function T12_空工具栏时switchModel再resetToolbar() {
await createCherry({ toolbars: { toolbar: [], toolbarRight: [] } });
cherryInstance.switchModel('editOnly', true);
await delay(100);
const s1 = getSnapshot();
cherryInstance.resetToolbar('toolbar', ['bold']);
await delay(200);
const s2 = getSnapshot();
return [
assert(s2.toolbarInDOM,
`switchModel后再resetToolbar: toolbar 最终应在 DOM 中 (inDOM=${s2.toolbarInDOM})`),
assert(s2.toolbarConfig.length > 0,
`toolbar 配置应有内容 (长度=${s2.toolbarConfig?.length})`),
];
}
async function T13_toolbarRight单独控制() {
await createCherry({ toolbars: { toolbar: [], toolbarRight: ['fullScreen'] } });
await delay(100);
const s = getSnapshot();
return [
assert(s.toolbarRightConfig.length > 0, `toolbarRight 有内容`),
assert(s.shouldHide === false, `shouldHideToolbar 应为 false (实际: ${s.shouldHide})`),
assert(!s.noToolbarClass, `不应有 cherry--no-toolbar 类`),
];
}
async function T14_toolbarFalse回退defaultToolbar() {
await createCherry({ toolbars: { toolbar: false, toolbarRight: false } });
await delay(100);
const s = getSnapshot();
return [
assert(Array.isArray(s.toolbarConfig), `toolbar 应为数组`),
assert(s.toolbarConfig.length > 5,
`toolbar false 应回退 defaultToolbar (长度>=5, 实际:${s.toolbarConfig?.length})`),
assert(s.shouldHide === true, `toolbar:false 应隐藏 (实际: ${s.shouldHide})`),
assert(s.noToolbarClass, `应有 cherry--no-toolbar 类`),
];
}
async function T15_toolbar和toolbarRight都有内容() {
await createCherry({ toolbars: { toolbar: ['bold'], toolbarRight: ['theme'] } });
await delay(100);
const s = getSnapshot();
return [
assert(!s.noToolbarClass, `不应有 cherry--no-toolbar 类`),
assert(s.shouldHide === false, `shouldHide 应为 false`),
];
}
async function T16_toolbarFalse且toolbarRight为空() {
await createCherry({ toolbars: { toolbar: false, toolbarRight: [] } });
await delay(100);
const s = getSnapshot();
return [
assert(s.noToolbarClass, `应有 cherry--no-toolbar 类`),
assert(s.shouldHide === true, `shouldHide 应为 true`),
];
}
async function T17_toolbarFalse且toolbarRight有内容() {
await createCherry({ toolbars: { toolbar: false, toolbarRight: ['theme'] } });
await delay(100);
const s = getSnapshot();
return [
assert(s.noToolbarClass, `toolbar:false 禁用整个顶部栏,应有 cherry--no-toolbar`),
assert(s.shouldHide === true, `shouldHide 应为 true`),
];
}
async function T18_toolbar有内容且toolbarRightFalse() {
await createCherry({ toolbars: { toolbar: ['bold'], toolbarRight: false } });
await delay(100);
const s = getSnapshot();
return [
assert(!s.noToolbarClass, `左侧有按钮,不应有 cherry--no-toolbar`),
assert(s.shouldHide === false, `shouldHide 应为 false`),
assert(Array.isArray(s.toolbarRightConfig), `toolbarRight:false 应归一化为数组`),
];
}
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
window.runAllTests = async function() {
const body = document.getElementById('reportBody');
body.innerHTML = '<div class="test-case"><div class="test-name">⏳ 正在运行...</div></div>';
testResults = [];
updateSummary();
document.getElementById('reportPanel').classList.add('expanded');
document.getElementById('toggleLabel').textContent = '▼ 折叠';
const tests = [
['T01 初始空工具栏', T01_初始空工具栏],
['T02 resetToolbar 从空→有按钮', T02_resetToolbar从空添加按钮],
['T03 resetToolbar 清空', T03_resetToolbar清空],
['T04 switchModel edit&preview + show', T04_switchModel_editPreview_showToolbar],
['T05 switchModel edit&preview + hide', T05_switchModel_editPreview_hideToolbar],
['T06 switchModel editOnly + show', T06_switchModel_editOnly_showToolbar],
['T07 switchModel editOnly + hide', T07_switchModel_editOnly_hideToolbar],
['T08 switchModel previewOnly', T08_switchModel_previewOnly],
['T09 竞态: reset→switchModel显示', T09_先resetToolbar后switchModel显示],
['T10 竞态: reset清空→switchModel隐藏', T10_先resetToolbar清空后switchModel隐藏],
['T11 连续多次 resetToolbar', T11_多次连续resetToolbar],
['T12 空→switchModel→resetToolbar', T12_空工具栏时switchModel再resetToolbar],
['T13 toolbarRight 单独控制', T13_toolbarRight单独控制],
['T14 toolbar:false 回退逻辑', T14_toolbarFalse回退defaultToolbar],
['T15 toolbar+toolbarRight都有内容', T15_toolbar和toolbarRight都有内容],
['T16 toolbar:false+toolbarRight:[]', T16_toolbarFalse且toolbarRight为空],
['T17 toolbar:false+toolbarRight有内容', T17_toolbarFalse且toolbarRight有内容],
['T18 toolbar有内容+toolbarRight:false', T18_toolbar有内容且toolbarRightFalse],
];
for (const [name, fn] of tests) {
body.innerHTML += `<div class="test-case"><div class="test-name">⏳ 运行: ${name}...</div></div>`;
await testCase(name, fn);
await delay(150);
}
const passCount = testResults.filter(r => r.passed).length;
const failCount = tests.length - passCount;
console.log(`\n===== 测试完成 =====`);
console.log(`通过: ${passCount} / 失败: ${failCount} / 总计: ${tests.length}`);
};
window.toggleReport = function() {
const panel = document.getElementById('reportPanel');
const label = document.getElementById('toggleLabel');
panel.classList.toggle('expanded');
label.textContent = panel.classList.contains('expanded') ? '▼ 折叠' : '▲ 展开详情';
};
await createCherry();
console.log('[INIT] Cherry 实例已就绪,初始配置 toolbar:[], toolbarRight:[]');
console.log('[INFO] 点击 "运行全部测试" 开始自动测试');
</script>
</body>
</html>