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
This commit is contained in:
@@ -0,0 +1,822 @@
|
||||
/**
|
||||
* 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');
|
||||
// 即使手动设为 dark,resolveTheme('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');
|
||||
// 手动触发 MutationObserver(jsdom 中需手动 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user