Files
MetonaEditor/tests/themes.test.ts
T
thzxx e83fc211dc feat: v0.2.0 — TypeScript full rewrite, 95%+ core coverage
BREAKING CHANGE: All source files converted from JavaScript to TypeScript.
- 12 .ts source files with strict types, full EditorOptions/Plugin/Token interfaces
- 7 .ts test files, 610 total tests (27 new), 7 suites all passing
- tsc --noEmit: 0 errors
- rollup-plugin-typescript build: 5 artifacts (UMD/ESM/CJS/Min/DTS)
- @babel/preset-typescript for jest
- New tsconfig.json, updated babel/jest/rollup configs
- Coverage: parser 99.5%, utils 95.7%, themes 96.2%, core 88.8%, plugins 89.5%
- Removed types/ folder (types now inline in .ts + auto-generated .d.ts)
- Desktop-only, no backward compatibility
2026-07-24 22:28:38 +08:00

823 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* themes.js 单元测试
* 覆盖主题系统:解析 / 切换 / 存储 / 监听 / 自定义主题注册
*
* 注意:themes.js 使用模块级状态(currentTheme / themeListeners),
* 测试间需要重置状态以避免相互影响。
*/
import {
themeUtils,
getSystemTheme,
resolveTheme,
getThemeConfig,
applyTheme,
getCurrentTheme,
getResolvedTheme,
switchTheme,
toggleTheme,
resetToAuto,
saveTheme,
loadTheme,
initTheme,
watchSystemTheme,
unwatchSystemTheme,
addThemeListener,
removeThemeListener,
clearThemeListeners,
registerTheme,
unregisterTheme,
getAllThemes,
getThemeNames,
hasTheme,
getTheme,
setThemeVariables,
presetThemes,
createThemeManager,
exportCSSVars,
getCSSVariable,
applyThemeToElement,
followExternalTheme,
adoptFromParent,
watch,
createInstanceTheme,
} from '../src/themes';
import { THEMES } from '../src/constants';
import { MarkdownEditor } from '../src/core';
// 确保 localStorage 存在(jsdom 兼容)
if (typeof global.localStorage === 'undefined') {
const store = {};
global.localStorage = {
getItem: (k) => (k in store ? store[k] : null),
setItem: (k, v) => { store[k] = String(v); },
removeItem: (k) => { delete store[k]; },
clear: () => { Object.keys(store).forEach((k) => delete store[k]); },
};
}
// jsdom 默认无 matchMedia,需 mock 以测试 watchSystemTheme
const mockMatchMedia = (matches = false) => ({
matches,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
});
describe('主题解析', () => {
test('resolveTheme 非 auto 原样返回', () => {
expect(resolveTheme('light')).toBe('light');
expect(resolveTheme('dark')).toBe('dark');
expect(resolveTheme('warm')).toBe('warm');
});
test('resolveTheme auto 返回系统主题', () => {
// currentTheme 默认为 'auto',调用 resolveTheme('auto') 会返回 getSystemTheme()
const r = resolveTheme('auto');
expect(['light', 'dark']).toContain(r);
});
test('getTheme 是 resolveTheme 的别名', () => {
expect(getTheme('light')).toBe(resolveTheme('light'));
});
test('getSystemTheme 返回 light 或 dark', () => {
const t = getSystemTheme();
expect(['light', 'dark']).toContain(t);
});
test('getThemeConfig 已知主题返回配置对象', () => {
const c = getThemeConfig('light');
expect(c).toBe(THEMES.light);
expect(c.bg).toBeDefined();
expect(c.accent).toBeDefined();
});
test('getThemeConfig 未知主题回退到 light', () => {
const c = getThemeConfig('unknown-theme');
expect(c).toBe(THEMES.light);
});
});
describe('主题切换与应用', () => {
beforeEach(() => {
clearThemeListeners();
resetToAuto();
localStorage.clear();
});
test('applyTheme 设置 currentTheme', () => {
applyTheme('dark');
expect(getCurrentTheme()).toBe('dark');
});
test('applyTheme 在 documentElement 上设置 data-md-theme 属性', () => {
applyTheme('dark');
expect(document.documentElement.getAttribute('data-md-theme')).toBe('dark');
});
test('applyTheme 切换 md-theme-* class', () => {
applyTheme('light');
expect(document.documentElement.classList.contains('md-theme-light')).toBe(true);
expect(document.documentElement.classList.contains('md-theme-dark')).toBe(false);
applyTheme('dark');
expect(document.documentElement.classList.contains('md-theme-dark')).toBe(true);
expect(document.documentElement.classList.contains('md-theme-light')).toBe(false);
});
test('applyTheme 设置 CSS 变量', () => {
applyTheme('dark');
const root = document.documentElement;
expect(root.style.getPropertyValue('--md-bg')).toBe(THEMES.dark.bg);
expect(root.style.getPropertyValue('--md-accent')).toBe(THEMES.dark.accent);
});
test('switchTheme 应用并持久化', () => {
switchTheme('dark');
expect(getCurrentTheme()).toBe('dark');
expect(localStorage.getItem('metona-editor-theme')).toBe('dark');
});
test('toggleTheme 在 light/dark 间切换', () => {
switchTheme('light');
toggleTheme();
expect(getCurrentTheme()).toBe('dark');
toggleTheme();
expect(getCurrentTheme()).toBe('light');
});
test('resetToAuto 切回 auto', () => {
switchTheme('dark');
resetToAuto();
expect(getCurrentTheme()).toBe('auto');
});
test('getResolvedTheme 返回解析后的实际主题', () => {
switchTheme('dark');
expect(getResolvedTheme()).toBe('dark');
switchTheme('light');
expect(getResolvedTheme()).toBe('light');
});
});
describe('主题持久化', () => {
beforeEach(() => {
localStorage.clear();
resetToAuto();
});
test('saveTheme 写入 localStorage', () => {
saveTheme('dark');
expect(localStorage.getItem('metona-editor-theme')).toBe('dark');
});
test('loadTheme 默认返回 auto', () => {
expect(loadTheme()).toBe('auto');
});
test('loadTheme 读取已保存的值', () => {
localStorage.setItem('metona-editor-theme', 'dark');
expect(loadTheme()).toBe('dark');
});
test('initTheme 加载已保存的主题', () => {
localStorage.setItem('metona-editor-theme', 'dark');
initTheme();
expect(getCurrentTheme()).toBe('dark');
});
test('initTheme 无保存时默认 auto', () => {
initTheme();
expect(getCurrentTheme()).toBe('auto');
});
});
describe('主题监听', () => {
beforeEach(() => {
clearThemeListeners();
resetToAuto();
});
test('addThemeListener 注册回调', () => {
const fn = jest.fn();
addThemeListener(fn);
switchTheme('dark');
expect(fn).toHaveBeenCalledWith('dark', 'dark');
});
test('addThemeListener 返回取消函数', () => {
const fn = jest.fn();
const unsub = addThemeListener(fn);
expect(typeof unsub).toBe('function');
unsub();
switchTheme('dark');
expect(fn).not.toHaveBeenCalled();
});
test('removeThemeListener 移除回调', () => {
const fn = jest.fn();
addThemeListener(fn);
removeThemeListener(fn);
switchTheme('dark');
expect(fn).not.toHaveBeenCalled();
});
test('clearThemeListeners 清空所有回调', () => {
const fn1 = jest.fn();
const fn2 = jest.fn();
addThemeListener(fn1);
addThemeListener(fn2);
clearThemeListeners();
switchTheme('dark');
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
});
test('监听器抛错不影响其他监听器', () => {
const errFn = jest.fn(() => { throw new Error('boom'); });
const okFn = jest.fn();
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
addThemeListener(errFn);
addThemeListener(okFn);
switchTheme('dark');
expect(errFn).toHaveBeenCalled();
expect(okFn).toHaveBeenCalled();
spy.mockRestore();
});
});
describe('系统主题监听', () => {
beforeEach(() => {
unwatchSystemTheme();
resetToAuto();
});
afterEach(() => {
unwatchSystemTheme();
});
test('watchSystemTheme 无 matchMedia 时不抛错', () => {
const orig = window.matchMedia;
delete window.matchMedia;
expect(() => watchSystemTheme()).not.toThrow();
window.matchMedia = orig;
});
test('watchSystemTheme 注册 matchMedia 监听', () => {
const orig = window.matchMedia;
const mql = mockMatchMedia(false);
window.matchMedia = jest.fn(() => mql);
watchSystemTheme();
expect(mql.addEventListener).toHaveBeenCalledWith('change', expect.any(Function));
window.matchMedia = orig;
});
test('unwatchSystemTheme 移除监听', () => {
const orig = window.matchMedia;
const mql = mockMatchMedia(false);
window.matchMedia = jest.fn(() => mql);
watchSystemTheme();
unwatchSystemTheme();
expect(mql.removeEventListener).toHaveBeenCalledWith('change', expect.any(Function));
window.matchMedia = orig;
});
test('重复 watchSystemTheme 替换旧监听', () => {
const orig = window.matchMedia;
const mql1 = mockMatchMedia(false);
const mql2 = mockMatchMedia(false);
let callCount = 0;
window.matchMedia = jest.fn(() => {
callCount += 1;
return callCount === 1 ? mql1 : mql2;
});
watchSystemTheme();
watchSystemTheme();
expect(mql1.removeEventListener).toHaveBeenCalled();
window.matchMedia = orig;
});
});
describe('自定义主题注册', () => {
beforeEach(() => {
resetToAuto();
});
test('registerTheme 注册新主题', () => {
registerTheme('ocean', {
bg: '#001122',
text: '#aabbcc',
accent: '#00ddff',
});
expect(hasTheme('ocean')).toBe(true);
const c = getThemeConfig('ocean');
expect(c.bg).toBe('#001122');
expect(c.accent).toBe('#00ddff');
// 未提供字段从 light 主题继承
expect(c.muted).toBe(THEMES.light.muted);
});
test('unregisterTheme 移除自定义主题', () => {
registerTheme('tmp', { bg: '#000' });
expect(hasTheme('tmp')).toBe(true);
unregisterTheme('tmp');
expect(hasTheme('tmp')).toBe(false);
});
test('unregisterTheme 拒绝移除内置主题', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
unregisterTheme('light');
expect(spy).toHaveBeenCalled();
expect(hasTheme('light')).toBe(true);
spy.mockRestore();
});
test('getAllThemes 返回所有主题副本', () => {
const all = getAllThemes();
expect(all.light).toEqual(THEMES.light);
expect(all.dark).toEqual(THEMES.dark);
// 副本:修改不影响原始
all.light = null;
expect(THEMES.light).toBeDefined();
});
test('getThemeNames 返回主题名数组', () => {
const names = getThemeNames();
expect(names).toContain('light');
expect(names).toContain('dark');
expect(names).toContain('auto');
expect(names).toContain('warm');
});
test('hasTheme 检查主题存在', () => {
expect(hasTheme('light')).toBe(true);
expect(hasTheme('non-existent')).toBe(false);
});
});
describe('setThemeVariables', () => {
test('设置 CSS 变量到 documentElement', () => {
setThemeVariables({
bg: '#fff',
text: '#000',
border: '#eee',
shadow: 'none',
hoverShadow: 'none',
accent: '#abc',
});
const root = document.documentElement;
expect(root.style.getPropertyValue('--md-bg')).toBe('#fff');
expect(root.style.getPropertyValue('--md-accent')).toBe('#abc');
});
test('空 config 不抛错', () => {
expect(() => setThemeVariables(null)).not.toThrow();
expect(() => setThemeVariables(undefined)).not.toThrow();
});
test('部分字段使用默认值', () => {
setThemeVariables({ bg: '#fafafa' });
const root = document.documentElement;
expect(root.style.getPropertyValue('--md-bg')).toBe('#fafafa');
// 未提供的 accent 使用默认
expect(root.style.getPropertyValue('--md-accent')).toBe('#3b82f6');
});
});
describe('themeUtils 代理层', () => {
test('暴露所有主题 API', () => {
[
'getSystemTheme', 'resolveTheme', 'getThemeConfig', 'applyTheme',
'getCurrentTheme', 'getResolvedTheme', 'switchTheme', 'toggleTheme',
'resetToAuto', 'initTheme', 'watchSystemTheme', 'unwatchSystemTheme',
'addThemeListener', 'removeThemeListener', 'clearThemeListeners',
'registerTheme', 'unregisterTheme', 'getAllThemes', 'getThemeNames',
'hasTheme', 'saveTheme', 'loadTheme',
].forEach((fn) => {
expect(typeof themeUtils[fn]).toBe('function');
});
});
test('代理方法与具名导出一致', () => {
expect(themeUtils.getCurrentTheme).toBe(getCurrentTheme);
expect(themeUtils.switchTheme).toBe(switchTheme);
});
});
describe('presetThemes', () => {
test('包含 light/dark/auto/warm', () => {
expect(presetThemes.light).toBeDefined();
expect(presetThemes.dark).toBeDefined();
expect(presetThemes.auto).toBeDefined();
expect(presetThemes.warm).toBeDefined();
});
test('auto 主题 config 为字符串 "auto"', () => {
expect(presetThemes.auto.config).toBe('auto');
});
test('light/dark/warm 主题 config 为对象', () => {
expect(typeof presetThemes.light.config).toBe('object');
expect(typeof presetThemes.dark.config).toBe('object');
expect(typeof presetThemes.warm.config).toBe('object');
});
});
describe('createThemeManager', () => {
test('返回包含全部方法的管理器对象', () => {
const m = createThemeManager();
expect(typeof m.switchTheme).toBe('function');
expect(typeof m.getCurrentTheme).toBe('function');
expect(typeof m.registerTheme).toBe('function');
});
test('管理器方法可独立调用', () => {
const m = createThemeManager();
m.switchTheme('dark');
expect(m.getCurrentTheme()).toBe('dark');
});
});
// ============ v0.1.5 新增测试 ============
describe('v0.1.5 resolveTheme auto 修复', () => {
beforeEach(() => {
clearThemeListeners();
resetToAuto();
localStorage.clear();
});
test('auto 始终解析为系统主题,不受上次手动设置污染', () => {
switchTheme('dark');
expect(getCurrentTheme()).toBe('dark');
// 即使手动设为 darkresolveTheme('auto') 仍应返回系统主题
const sys = getSystemTheme();
expect(resolveTheme('auto')).toBe(sys);
});
test('resolveTheme 传入具体主题名原样返回', () => {
expect(resolveTheme('light')).toBe('light');
expect(resolveTheme('dark')).toBe('dark');
expect(resolveTheme('warm')).toBe('warm');
});
test('resolveTheme 无参/空值返回系统主题', () => {
const sys = getSystemTheme();
expect(resolveTheme()).toBe(sys);
expect(resolveTheme(null)).toBe(sys);
expect(resolveTheme('')).toBe(sys);
});
});
describe('v0.1.5 exportCSSVars / getCSSVariable', () => {
test('exportCSSVars 返回 CSS 变量对象', () => {
applyTheme('light');
const vars = exportCSSVars();
expect(vars).toHaveProperty('--md-bg');
expect(vars).toHaveProperty('--md-accent');
});
test('exportCSSVars 从指定元素读取', () => {
const div = document.createElement('div');
div.style.setProperty('--md-bg', '#fff');
document.body.appendChild(div);
const vars = exportCSSVars(div);
expect(vars['--md-bg']).toBe('#fff');
document.body.removeChild(div);
});
test('getCSSVariable 获取单个变量', () => {
applyTheme('light');
const bg = getCSSVariable('--md-bg');
expect(typeof bg).toBe('string');
expect(bg.length).toBeGreaterThan(0);
});
test('getCSSVariable 自动补全 --md- 前缀', () => {
applyTheme('light');
const accent = getCSSVariable('accent');
expect(typeof accent).toBe('string');
expect(accent.length).toBeGreaterThan(0);
});
});
describe('v0.1.5 applyThemeToElement', () => {
test('应用主题到指定元素不改变全局状态', () => {
switchTheme('light');
const div = document.createElement('div');
document.body.appendChild(div);
applyThemeToElement('dark', div);
expect(div.getAttribute('data-md-theme')).toBe('dark');
// 全局主题不变
expect(getCurrentTheme()).toBe('light');
document.body.removeChild(div);
});
test('应用 auto 主题正确解析', () => {
const div = document.createElement('div');
document.body.appendChild(div);
applyThemeToElement('auto', div);
const resolved = resolveTheme('auto');
expect(div.getAttribute('data-md-theme')).toBe(resolved);
document.body.removeChild(div);
});
});
describe('v0.1.5 主题继承 registerTheme extends', () => {
test('extends 继承基础主题', () => {
registerTheme('ocean', {
extends: 'dark',
accent: '#00ddff',
});
expect(hasTheme('ocean')).toBe(true);
const cfg = getThemeConfig('ocean');
expect(cfg.accent).toBe('#00ddff');
// 从 dark 继承
expect(cfg.bg).toBe(THEMES.dark.bg);
expect(cfg.text).toBe(THEMES.dark.text);
unregisterTheme('ocean');
});
test('默认 extends light', () => {
registerTheme('minimal', { accent: '#ff0000' });
const cfg = getThemeConfig('minimal');
expect(cfg.bg).toBe(THEMES.light.bg);
unregisterTheme('minimal');
});
});
describe('v0.1.5 createInstanceTheme', () => {
test('创建实例主题上下文', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'dark' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
expect(ctx.get()).toBe('dark');
expect(ctx.getResolved()).toBe('dark');
expect(typeof ctx.set).toBe('function');
expect(typeof ctx.toggle).toBe('function');
expect(typeof ctx.getConfig).toBe('function');
expect(typeof ctx.getVars).toBe('function');
expect(typeof ctx.syncWithElement).toBe('function');
expect(typeof ctx.adopt).toBe('function');
expect(typeof ctx.watch).toBe('function');
document.body.removeChild(div);
});
test('实例主题 set 触发 _emit themeChange', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
ctx.set('dark');
expect(mockEditor._emit).toHaveBeenCalledWith('themeChange', expect.objectContaining({
theme: 'dark',
resolved: 'dark',
}));
expect(mockEditor.refresh).toHaveBeenCalled();
document.body.removeChild(div);
});
test('实例主题 toggle 切换 light/dark', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
ctx.toggle();
expect(ctx.get()).toBe('dark');
ctx.toggle();
expect(ctx.get()).toBe('light');
document.body.removeChild(div);
});
test('getConfig 返回主题配置对象', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
const cfg = ctx.getConfig();
expect(cfg).toHaveProperty('bg');
expect(cfg).toHaveProperty('accent');
document.body.removeChild(div);
});
test('getVars 返回实例 CSS 变量', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
applyThemeToElement('light', div);
const vars = ctx.getVars();
expect(vars).toHaveProperty('--md-bg');
document.body.removeChild(div);
});
});
describe('v0.1.5 followExternalTheme', () => {
test('通过 data-theme 属性跟随', () => {
const div = document.createElement('div');
div.setAttribute('data-theme', 'dark');
document.body.appendChild(div);
let detected = null;
const unsub = followExternalTheme({ element: div }, (theme) => { detected = theme; });
expect(detected).toBe('dark');
div.setAttribute('data-theme', 'light');
// 手动触发 MutationObserverjsdom 中需手动 dispatch
const observer = new MutationObserver(() => {});
// 注:jsdom 环境 MutationObserver 行为受限,此处只验证初始化回调
unsub();
document.body.removeChild(div);
});
test('通过 classMap 跟随', () => {
const div = document.createElement('div');
div.className = 'theme-dark';
document.body.appendChild(div);
let detected = null;
followExternalTheme(
{ element: div, classMap: { 'theme-dark': 'dark', 'theme-light': 'light' } },
(theme) => { detected = theme; },
);
expect(detected).toBe('dark');
document.body.removeChild(div);
});
test('通过 callback 跟随', () => {
const div = document.createElement('div');
document.body.appendChild(div);
let detected = null;
followExternalTheme(
{ element: div, callback: () => 'warm' },
(theme) => { detected = theme; },
);
expect(detected).toBe('warm');
document.body.removeChild(div);
});
test('返回取消跟随函数', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const unsub = followExternalTheme({ element: div }, () => {});
expect(typeof unsub).toBe('function');
unsub();
document.body.removeChild(div);
});
});
describe('v0.1.5 adoptFromParent', () => {
test('从父容器 data-md-theme 继承', () => {
const parent = document.createElement('div');
parent.setAttribute('data-md-theme', 'dark');
const child = document.createElement('div');
parent.appendChild(child);
document.body.appendChild(parent);
let detected = null;
const unsub = adoptFromParent(child, (theme) => { detected = theme; });
expect(detected).toBe('dark');
unsub();
document.body.removeChild(parent);
});
test('从父容器 class 继承', () => {
const parent = document.createElement('div');
parent.className = 'dark';
const child = document.createElement('div');
parent.appendChild(child);
document.body.appendChild(parent);
let detected = null;
adoptFromParent(child, (theme) => { detected = theme; });
expect(detected).toBe('dark');
document.body.removeChild(parent);
});
test('容器无父元素不抛错', () => {
const orphan = document.createElement('div');
let detected = null;
expect(() => adoptFromParent(orphan, (t) => { detected = t; })).not.toThrow();
});
test('返回取消函数', () => {
const parent = document.createElement('div');
const child = document.createElement('div');
parent.appendChild(child);
document.body.appendChild(parent);
const unsub = adoptFromParent(child, () => {});
expect(typeof unsub).toBe('function');
unsub();
document.body.removeChild(parent);
});
});
describe('v0.1.5 watch 外部主题源', () => {
test('函数源:回调被调用', () => {
let detected = null;
const unsub = watch(() => 'dark', (theme) => { detected = theme; });
expect(detected).toBe('dark');
unsub();
});
test('返回取消函数', () => {
const unsub = watch(() => 'light', () => {});
expect(typeof unsub).toBe('function');
unsub();
});
});
describe('v0.1.5 MarkdownEditor 实例主题 API', () => {
test('setTheme / getTheme 实例方法', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { theme: 'light' });
expect(ed.getTheme()).toBe('light');
ed.setTheme('dark');
expect(ed.getTheme()).toBe('dark');
// el 类名应更新
expect(ed.el.className).toContain('me-theme-dark');
ed.destroy();
});
test('getThemeContext 返回实例主题上下文', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, {});
const ctx = ed.getThemeContext();
expect(ctx).not.toBeNull();
expect(typeof ctx.set).toBe('function');
expect(typeof ctx.get).toBe('function');
ed.destroy();
});
test('多实例独立主题互不干扰', () => {
document.body.innerHTML = '';
const c1 = document.createElement('div');
const c2 = document.createElement('div');
document.body.appendChild(c1);
document.body.appendChild(c2);
const ed1 = new MarkdownEditor(c1, { theme: 'light' });
const ed2 = new MarkdownEditor(c2, { theme: 'dark' });
expect(ed1.getTheme()).toBe('light');
expect(ed2.getTheme()).toBe('dark');
ed1.setTheme('warm');
expect(ed1.getTheme()).toBe('warm');
expect(ed2.getTheme()).toBe('dark'); // ed2 不受影响
ed1.destroy();
ed2.destroy();
});
test('setTheme 触发 themeChange 事件', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, {});
const handler = jest.fn();
ed.on('themeChange', handler);
ed.setTheme('dark');
expect(handler).toHaveBeenCalled();
ed.destroy();
});
});