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
823 lines
24 KiB
TypeScript
823 lines
24 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|
||
});
|