/** * MetonaToast 覆盖率补充测试 — 目标 95%+ * @module tests * @version 0.2.1 */ import MeToast, { Toast, VERSION } from '../src/index'; import type { ToastInstance, ToastConfig } from '../src/types'; // ========== DOM Mock (与 index.test.ts 一致) ========== const createMockElement = (): Record => ({ className: '', style: {} as Record, setAttribute: jest.fn(), appendChild: jest.fn(), querySelector: jest.fn(() => null), querySelectorAll: jest.fn(() => []), execCommand: jest.fn(() => true), classList: { add: jest.fn(), remove: jest.fn(), contains: jest.fn() }, addEventListener: jest.fn(), removeEventListener: jest.fn(), setPointerCapture: jest.fn(), releasePointerCapture: jest.fn(), animate: jest.fn(() => ({ onfinish: null, oncancel: null, cancel: jest.fn(), pause: jest.fn(), play: jest.fn() })), innerHTML: '', textContent: '', dataset: {} as Record, parentNode: { removeChild: jest.fn() }, removeChild: jest.fn(), insertBefore: jest.fn(), closest: jest.fn(() => null), getBoundingClientRect: jest.fn(() => ({ top: 0, left: 0, width: 360, height: 60, right: 360, bottom: 60 })), setPointerCapture: jest.fn(), releasePointerCapture: jest.fn(), remove: jest.fn(), focus: jest.fn(), click: jest.fn(), contains: jest.fn(() => false), }); const mockDocument = { createElement: jest.fn(() => createMockElement()), getElementById: jest.fn(), querySelector: jest.fn(), querySelectorAll: jest.fn(() => []), head: { appendChild: jest.fn(), removeChild: jest.fn() }, body: { appendChild: jest.fn(), querySelectorAll: jest.fn(() => []), removeChild: jest.fn() }, readyState: 'complete', addEventListener: jest.fn(), execCommand: jest.fn(() => true), documentElement: { setAttribute: jest.fn(), classList: { add: jest.fn(), remove: jest.fn() }, style: { setProperty: jest.fn(), removeProperty: jest.fn() }, }, hidden: false, createEvent: jest.fn(() => ({ initEvent: jest.fn() })), }; let mediaMatch = false; const mockWindow = { matchMedia: jest.fn(() => ({ matches: mediaMatch, addEventListener: jest.fn(), removeEventListener: jest.fn(), })), requestAnimationFrame: jest.fn((cb: FrameRequestCallback) => setTimeout(cb, 0) as unknown as number), cancelAnimationFrame: jest.fn(), getComputedStyle: jest.fn(() => ({})), innerWidth: 1024, innerHeight: 768, navigator: { language: 'zh-CN', userLanguage: undefined, clipboard: { writeText: jest.fn() }, connection: { effectiveType: '4g', downlink: 10, rtt: 50 }, onLine: true, }, location: { search: '', href: 'http://localhost' }, history: { pushState: jest.fn() }, localStorage: { _store: {} as Record, getItem: jest.fn((k: string) => mockWindow.localStorage._store[k] || null), setItem: jest.fn((k: string, v: string) => { mockWindow.localStorage._store[k] = v; }), removeItem: jest.fn((k: string) => { delete mockWindow.localStorage._store[k]; }), clear: jest.fn(() => { mockWindow.localStorage._store = {}; }), }, Audio: jest.fn(() => ({ play: jest.fn(), volume: 0 })), Notification: undefined, PointerEvent: class {}, }; (global as Record).document = mockDocument; (global as Record).window = mockWindow; (global as Record).navigator = mockWindow.navigator; (global as Record).localStorage = mockWindow.localStorage; (global as Record).requestAnimationFrame = mockWindow.requestAnimationFrame; (global as Record).cancelAnimationFrame = mockWindow.cancelAnimationFrame; // ========== beforeEach cleanup ========== beforeEach(() => { jest.clearAllMocks(); mockWindow.localStorage._store = {}; MeToast._toasts.clear(); Toast._hooks.clear(); const { _containerCache } = require('../src/toast.js'); _containerCache.clear(); mockDocument.querySelectorAll = jest.fn(() => []); mockDocument.body.appendChild = jest.fn(); mediaMatch = false; }); // ================================================================== // 1. animations.ts 覆盖补充 // ================================================================== describe('animations.ts 覆盖率', () => { test('reset() 重置为默认动画', () => { MeToast.animations.register('custom', { enter: {}, leave: {} }); MeToast.animations.reset(); expect(MeToast.animations.get('slide')).toBeDefined(); expect(MeToast.animations.get('custom')).toBeNull(); }); test('destroy() 清空所有动画', () => { MeToast.animations.destroy(); expect(MeToast.animations.getAnimationNames()).toHaveLength(0); MeToast.animations.reset(); }); test('createAnimation 工具函数', () => { const { createAnimation } = require('../src/animations.js'); const anim = createAnimation({ enter: { opacity: '0' } }); expect(anim.enter).toEqual({ opacity: '0' }); expect(anim.leave).toEqual({}); expect(anim.duration).toBe(300); expect(anim.easing).toBe('cubic-bezier(0.4, 0, 0.2, 1)'); }); test('getActiveCount 返回动画数量', () => { expect(MeToast.animations.getActiveCount()).toBeGreaterThan(0); }); }); // ================================================================== // 2. themes.ts 覆盖率补充 // ================================================================== describe('themes.ts 覆盖率', () => { test('toggleTheme 在 light/dark 之间切换', () => { const { switchTheme, toggleTheme, getCurrentTheme } = require('../src/themes.js'); switchTheme('light'); toggleTheme(); expect(getCurrentTheme()).toBe('dark'); toggleTheme(); expect(getCurrentTheme()).toBe('light'); }); test('resetToAuto 重置为自动', () => { const { resetToAuto, getCurrentTheme } = require('../src/themes.js'); resetToAuto(); expect(getCurrentTheme()).toBe('auto'); }); test('getResolvedTheme 返回解析后的主题', () => { const { getResolvedTheme, switchTheme } = require('../src/themes.js'); switchTheme('dark'); expect(getResolvedTheme()).toBe('dark'); switchTheme('auto'); expect(['light', 'dark']).toContain(getResolvedTheme()); }); test('addThemeListener / removeThemeListener / clearThemeListeners', () => { const { addThemeListener, removeThemeListener, clearThemeListeners, switchTheme } = require('../src/themes.js'); const fn = jest.fn(); const unsub = addThemeListener(fn); switchTheme('dark'); expect(fn).toHaveBeenCalledWith('dark', 'dark'); removeThemeListener(fn); fn.mockClear(); switchTheme('light'); expect(fn).not.toHaveBeenCalled(); }); test('unregisterTheme 不能卸载内置主题', () => { const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); const { unregisterTheme } = require('../src/themes.js'); unregisterTheme('light'); expect(consoleWarn).toHaveBeenCalled(); consoleWarn.mockRestore(); }); test('unregisterTheme 卸载自定义主题', () => { const { registerTheme, unregisterTheme, hasTheme } = require('../src/themes.js'); registerTheme('custom-1', { bg: '#fff', text: '#000', border: '#ccc', shadow: 'none', hoverShadow: 'none', progressBg: '#eee', closeHoverBg: '#ddd' }); expect(hasTheme('custom-1')).toBe(true); unregisterTheme('custom-1'); expect(hasTheme('custom-1')).toBe(false); }); test('getThemePreview 返回预览信息', () => { const { getThemePreview } = require('../src/themes.js'); const preview = getThemePreview('dark'); expect(preview.name).toBe('dark'); expect(preview.isDark).toBe(true); expect(preview.colors.background).toBeDefined(); }); test('getThemePreview auto 模式', () => { const { getThemePreview } = require('../src/themes.js'); const preview = getThemePreview('auto'); expect(preview.isAuto).toBe(true); }); test('generateThemeCSS 生成 CSS', () => { const { generateThemeCSS } = require('../src/themes.js'); const css = generateThemeCSS('light'); expect(css).toContain('--met-bg'); expect(css).toContain('--met-text'); }); test('applyThemeCSS / removeThemeCSS 不抛异常', () => { const { applyThemeCSS, removeThemeCSS } = require('../src/themes.js'); expect(() => applyThemeCSS('dark')).not.toThrow(); expect(() => removeThemeCSS()).not.toThrow(); }); test('saveTheme / loadTheme 读写 localStorage', () => { const { saveTheme, loadTheme } = require('../src/themes.js'); expect(() => saveTheme('warm')).not.toThrow(); const loaded = loadTheme(); expect(typeof loaded).toBe('string'); }); test('initTheme 从 localStorage 恢复', () => { const { initTheme, getCurrentTheme, saveTheme } = require('../src/themes.js'); saveTheme('dark'); initTheme(); expect(typeof getCurrentTheme()).toBe('string'); }); test('watchSystemTheme / unwatchSystemTheme 不抛异常', () => { const { watchSystemTheme, unwatchSystemTheme } = require('../src/themes.js'); expect(() => watchSystemTheme()).not.toThrow(); expect(() => unwatchSystemTheme()).not.toThrow(); }); test('setThemeVariables / clearThemeVariables 不抛异常', () => { const { setThemeVariables, clearThemeVariables, getThemeConfig } = require('../src/themes.js'); const config = getThemeConfig('light'); expect(() => setThemeVariables(config)).not.toThrow(); expect(() => clearThemeVariables()).not.toThrow(); }); test('getAllThemes / getThemeNames / hasTheme', () => { const { getAllThemes, getThemeNames, hasTheme } = require('../src/themes.js'); expect(getAllThemes().light).toBeDefined(); expect(getThemeNames()).toContain('light'); expect(hasTheme('nonexistent')).toBe(false); }); }); // ================================================================== // 3. i18n.ts 覆盖率补充 // ================================================================== describe('i18n.ts 覆盖率', () => { const i18n = require('../src/i18n.js'); test('getFallbackLocale / setFallbackLocale', () => { i18n.setFallbackLocale('en-US'); expect(i18n.getFallbackLocale()).toBe('en-US'); i18n.setFallbackLocale('zh-CN'); }); test('setFallbackLocale 不支持的语言不改变', () => { const before = i18n.getFallbackLocale(); i18n.setFallbackLocale('nonexistent'); expect(i18n.getFallbackLocale()).toBe(before); }); test('getTranslations 返回翻译数据', () => { const trans = i18n.getTranslations('zh-CN'); expect(trans.close).toBe('关闭'); }); test('addTranslations 添加新语言', () => { i18n.addTranslations('ja', { close: '閉じる', confirm: '確認' }); expect(i18n.t('close')).toBeDefined(); }); test('addTranslations 深度合并', () => { i18n.addTranslations('ja', { nested: { key: 'value' } }); i18n.addTranslations('ja', { nested: { key2: 'value2' } }); const trans = i18n.getTranslations('ja'); expect((trans as Record).nested).toBeDefined(); }); test('removeTranslation 删除翻译 key', () => { i18n.addTranslations('ja', { test_key: 'テスト' }); i18n.removeTranslation('ja', 'test_key'); expect(i18n.hasTranslation('test_key')).toBe(false); }); test('removeTranslation 嵌套 key', () => { i18n.addTranslations('ja', { a: { b: { c: 'val' } } }); i18n.removeTranslation('ja', 'a.b.c'); // 不应抛异常 }); test('clearTranslations 清除语言', () => { i18n.addTranslations('ja', { test: 'テスト' }); i18n.clearTranslations('ja'); expect(i18n.getTranslations('ja')).toEqual({}); }); test('getLocaleInfo / getAllLocaleInfo', () => { const info = i18n.getLocaleInfo('zh-CN'); expect(info.code).toBe('zh-CN'); expect(info.isSupported).toBe(true); expect(info.direction).toBe('ltr'); const all = i18n.getAllLocaleInfo(); expect(all.length).toBeGreaterThan(0); }); test('saveLocale / loadLocale / getDefaultLocale 不抛异常', () => { expect(() => i18n.saveLocale('en-US')).not.toThrow(); const loaded = i18n.loadLocale(); expect(typeof loaded).toBe('string'); const def = i18n.getDefaultLocale(); expect(typeof def).toBe('string'); }); test('initI18n 从 localStorage 恢复', () => { i18n.saveLocale('en-US'); i18n.initI18n(); expect(typeof i18n.getCurrentLocale()).toBe('string'); i18n.switchLocale('zh-CN'); }); test('addLocaleListener / removeLocaleListener / clearLocaleListeners', () => { const fn = jest.fn(); const unsub = i18n.addLocaleListener(fn); i18n.switchLocale('en-US'); expect(fn).toHaveBeenCalledWith('en-US'); i18n.removeLocaleListener(fn); fn.mockClear(); i18n.switchLocale('zh-CN'); expect(fn).not.toHaveBeenCalled(); unsub?.(); }); test('formatPercent', () => { const result = i18n.formatPercent(75); expect(typeof result).toBe('string'); }); test('formatTime', () => { const result = i18n.formatTime(new Date()); expect(typeof result).toBe('string'); }); test('formatRelativeTime', () => { const future = new Date(Date.now() + 3600000); const result = i18n.formatRelativeTime(future); expect(typeof result).toBe('string'); }); test('formatRelativeTime with past date', () => { const past = new Date(Date.now() - 86400000); const result = i18n.formatRelativeTime(past); expect(typeof result).toBe('string'); }); test('formatList 使用 Intl.ListFormat', () => { const result = i18n.formatList(['苹果', '香蕉', '橙子']); expect(typeof result).toBe('string'); }); test('formatPlural', () => { const result1 = i18n.formatPlural(1); const result2 = i18n.formatPlural(5); expect(typeof result1).toBe('string'); expect(typeof result2).toBe('string'); }); test('plural 翻译 不抛异常', () => { i18n.addTranslations('zh-CN', { 'item.one': '{count} 个', 'item.other': '{count} 个' }); const result = i18n.plural('item', 5); expect(typeof result).toBe('string'); }); test('plural 无复数形式时 fallback', () => { const result = i18n.plural('success', 5); expect(typeof result).toBe('string'); }); test('setCurrentLocale 不支持的语言 fallback', () => { const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); i18n.setCurrentLocale('nonexistent'); expect(consoleWarn).toHaveBeenCalled(); consoleWarn.mockRestore(); }); test('dateTimeFormats 预设格式', () => { expect(i18n.dateTimeFormats.short).toBeDefined(); expect(i18n.dateTimeFormats.long).toBeDefined(); }); test('numberFormats 预设格式', () => { expect(i18n.numberFormats.integer).toBeDefined(); expect(i18n.numberFormats.currency).toBeDefined(); }); test('presetLocales 预设语言包', () => { expect(i18n.presetLocales['zh-CN']).toBeDefined(); expect(i18n.presetLocales['en-US']).toBeDefined(); }); test('formatCurrency 默认 USD', () => { const result = i18n.formatCurrency(100); expect(typeof result).toBe('string'); }); test('formatDate with timestamp', () => { const result = i18n.formatDate(Date.now()); expect(typeof result).toBe('string'); }); }); // ================================================================== // 4. plugins.ts 覆盖率补充 // ================================================================== describe('plugins.ts 覆盖率', () => { test('PluginManager.destroy 调用所有插件的 destroy', () => { const { PluginManager } = require('../src/plugins.js'); const manager = new PluginManager(); const destroyFn = jest.fn(); manager.register('p1', { name: 'p1', destroy: destroyFn }); manager.register('p2', { name: 'p2' }); manager.destroy(); expect(destroyFn).toHaveBeenCalled(); expect(manager.getAll()).toHaveLength(0); }); test('pluginUtils.createManager 创建独立管理器', () => { const { createManager } = MeToast.plugins as unknown as Record unknown>; const manager = createManager(); expect(manager).toBeDefined(); }); test('pluginUtils.createPlugin 创建插件对象', () => { const pluginUtils = require('../src/plugins.js').pluginUtils; const plugin = pluginUtils.createPlugin({ name: 'custom', version: '2.0' }); expect(plugin.name).toBe('custom'); expect(plugin.version).toBe('2.0'); expect(plugin.hooks).toEqual({}); }); test('pluginUtils.validatePlugin 验证插件', () => { const pluginUtils = require('../src/plugins.js').pluginUtils; const valid = pluginUtils.validatePlugin({ name: 'test' }); expect(valid.valid).toBe(true); const invalid = pluginUtils.validatePlugin({}); expect(invalid.valid).toBe(false); expect(invalid.errors.length).toBeGreaterThan(0); }); test('pluginUtils.getPreset / getAllPresets', () => { const pluginUtils = require('../src/plugins.js').pluginUtils; expect(pluginUtils.getPreset('keyboard')).toBeDefined(); expect(pluginUtils.getPreset('nonexistent')).toBeNull(); const all = pluginUtils.getAllPresets(); expect(all.keyboard).toBeDefined(); expect(all.persistence).toBeDefined(); expect(all.accessibility).toBeDefined(); }); test('预设插件 persistence 生命周期 不抛异常', () => { const pluginUtils = require('../src/plugins.js').pluginUtils; const preset = pluginUtils.getPreset('persistence'); // install 读取 localStorage(可能返回 null) const result = preset?.install?.(pluginUtils as unknown as import('../src/types.js').PluginManager); expect(result === null || typeof result === 'object').toBe(true); const saveFn = (preset as Record void>).save; expect(() => saveFn({ duration: 3000 })).not.toThrow(); expect(() => preset?.uninstall?.()).not.toThrow(); }); test('预设插件 keyboard install/uninstall 不抛异常', () => { const pluginUtils = require('../src/plugins.js').pluginUtils; const preset = pluginUtils.getPreset('keyboard'); expect(() => preset?.install?.()).not.toThrow(); expect(() => preset?.uninstall?.()).not.toThrow(); }); test('预设插件 accessibility.announce 不抛异常', () => { const pluginUtils = require('../src/plugins.js').pluginUtils; const preset = pluginUtils.getPreset('accessibility'); const announceFn = (preset as Record void>).announce; expect(() => announceFn({ type: 'success', title: '标题', message: '消息' })).not.toThrow(); }); test('register 无效插件打印错误', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); MeToast.plugins.register('bad', {} as Record as import('../src/types.js').Plugin); expect(consoleError).toHaveBeenCalled(); consoleError.mockRestore(); }); }); // ================================================================== // 5. templates.ts 覆盖率补充 // ================================================================== describe('templates.ts 覆盖率', () => { const templates = require('../src/templates.js'); test('btnRow 生成按钮行', () => { const html = templates.btnRow(''); expect(html).toContain('display: flex'); expect(html).toContain(''); }); test('confirmHTML 使用默认值', () => { const html = templates.confirmHTML({}); expect(html).toContain('met-confirm-btn'); expect(html).toContain('met-cancel-btn'); }); test('confirmHTML 自定义文本和颜色', () => { const html = templates.confirmHTML({ confirmText: '删除', confirmColor: '#ff0000', cancelText: '保留', cancelColor: '#00ff00', }); expect(html).toContain('删除'); expect(html).toContain('保留'); expect(html).toContain('#ff0000'); }); test('promptHTML 使用默认值', () => { const html = templates.promptHTML({}); expect(html).toContain('met-input'); expect(html).toContain('met-submit-btn'); }); test('promptHTML 自定义 inputType', () => { const html = templates.promptHTML({ inputType: 'password', placeholder: '请输入密码', defaultValue: '123', }); expect(html).toContain('type="password"'); expect(html).toContain('请输入密码'); expect(html).toContain('123'); }); test('progressHTML 使用默认值', () => { const html = templates.progressHTML({}); expect(html).toContain('met-progress-bar'); expect(html).toContain('met-progress-fill'); expect(html).toContain('met-progress-text'); }); test('progressHTML 自定义颜色', () => { const html = templates.progressHTML({ progressColor: '#10b981' }); expect(html).toContain('#10b981'); }); test('actionHTML 空数组返回空字符串', () => { const html = templates.actionHTML([]); expect(html).toBe(''); }); test('actionHTML 生成按钮', () => { const html = templates.actionHTML([ { text: '撤销', onClick: () => {}, color: '#3b82f6' }, { text: '查看', onClick: () => {}, close: false }, ]); expect(html).toContain('met-action-btn-0'); expect(html).toContain('met-action-btn-1'); expect(html).toContain('撤销'); expect(html).toContain('查看'); }); }); // ================================================================== // 6. api.ts 覆盖率补充 // ================================================================== describe('api.ts 覆盖率', () => { beforeEach(() => { MeToast.resetConfig(); }); test('promise reject 路径', async () => { const promise = Promise.reject(new Error('fail')); try { await MeToast.promise(promise, { loading: '加载中', success: '成功', error: '失败' }); } catch (e) { expect(e).toBeDefined(); } expect(MeToast.count()).toBeGreaterThan(0); }); test('queue 完整执行并 await', async () => { const q = MeToast.queue(['a', 'b'], { delay: 5, duration: 5 }); await q; expect(MeToast.count()).toBeGreaterThanOrEqual(0); }); test('queue 中途取消', async () => { const q = MeToast.queue(['a', 'b', 'c'], { delay: 5, duration: 5 }); q.cancel(); await q; }); test('queue 消息级 onClose 回调', async () => { const onClose1 = jest.fn(); const q = MeToast.queue([ { message: 'a', onClose: onClose1 }, 'b', ], { delay: 5, duration: 5 }); await q; // onClose 在 toast 关闭时被调用 }); test('stack 混合类型 不抛异常', () => { expect(() => MeToast.stack([ '普通消息', { message: '警告', type: 'warning' as const, duration: 500 }, ], { stagger: 5, type: 'info' as const })).not.toThrow(); }); test('group 所有方法', () => { const g = MeToast.group('all-methods'); const t1 = g.show('default'); expect(t1.group).toBe('all-methods'); const t2 = g.success('success'); expect(t2.group).toBe('all-methods'); const t3 = g.error('error'); expect(t3.group).toBe('all-methods'); const t4 = g.warning('warning'); expect(t4.group).toBe('all-methods'); const t5 = g.info('info', { duration: 5 }); expect(t5.group).toBe('all-methods'); const l = g.loading('loading'); expect(l.id).toBeDefined(); l.dismiss(); const a = g.action('action', [{ text: 'OK', onClick: () => {} }]); expect(a.id).toBeDefined(); a.dismiss(); expect(g.count()).toBeGreaterThanOrEqual(5); g.dismiss(); }); test('dismissGroup 关闭分组', () => { const g = MeToast.group('cleanup'); g.success('m1'); g.error('m2'); expect(MeToast._groupCount('cleanup')).toBe(2); MeToast.dismissGroup('cleanup'); }); test('clear 按位置清除', () => { MeToast.success('top-right', { position: 'top-right' }); MeToast.error('bottom-left', { position: 'bottom-left' }); MeToast.clear('bottom-left'); const remaining = MeToast.findByPosition('top-right'); expect(remaining.length).toBe(1); MeToast.clear(); }); test('getAll 返回 Map 副本', () => { MeToast.success('m1'); MeToast.error('m2'); const all = MeToast.getAll(); expect(all.size).toBe(2); expect(all instanceof Map).toBe(true); }); test('count / hasToasts / getToast / find', () => { const t = MeToast.info('test'); expect(MeToast.count()).toBe(1); expect(MeToast.hasToasts()).toBe(true); expect(MeToast.getToast(t.id)?.id).toBe(t.id); expect(MeToast.find(t.id)?.id).toBe(t.id); expect(MeToast.getToast(null as unknown as string)).toBeNull(); expect(MeToast.find(undefined as unknown as string)).toBeUndefined(); }); test('findByType / findByPosition', () => { MeToast.success('s1'); MeToast.success('s2'); MeToast.error('e1'); expect(MeToast.findByType('success')).toHaveLength(2); expect(MeToast.findByPosition('top-right')).toHaveLength(3); }); test('closeAll / clearAll / pauseAll / resumeAll / updateAll', () => { MeToast.success('m1', { duration: 5000 }); MeToast.error('m2', { duration: 5000 }); MeToast.pauseAll(); const toasts = MeToast.getToasts(); toasts.forEach(t => expect(t.paused).toBe(true)); MeToast.resumeAll(); toasts.forEach(t => expect(t.paused).toBe(false)); MeToast.updateAll({ type: 'info' }); toasts.forEach(t => expect(t.type).toBe('info')); MeToast.closeAll(); }); test('init 通过 options 配置', () => { MeToast.init({ config: { duration: 2000 }, theme: 'dark', locale: 'en-US', plugins: ['keyboard'], }); const cfg = MeToast.getConfig(); expect(cfg.duration).toBe(2000); expect(MeToast.themes.getCurrentTheme()).toBe('dark'); expect(MeToast.i18n.getCurrentLocale()).toBe('en-US'); MeToast.resetConfig(); MeToast.themes.switchTheme('auto'); MeToast.i18n.switchLocale('zh-CN'); }); test('updateConfig / resetConfig', () => { MeToast.updateConfig({ position: 'bottom-center' }); expect(MeToast.getConfig().position).toBe('bottom-center'); MeToast.resetConfig(); expect(MeToast.getConfig().position).toBe('top-right'); }); test('getStatus 返回完整状态', () => { const status = MeToast.getStatus(); expect(status.version).toBe('0.2.1'); expect(status.toasts).toBeGreaterThanOrEqual(0); expect(status.theme).toBeDefined(); expect(status.locale).toBeDefined(); expect(Array.isArray(status.plugins)).toBe(true); }); test('use 不存在的预设插件打印警告', () => { const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); MeToast.use('nonexistent'); expect(consoleWarn).toHaveBeenCalled(); consoleWarn.mockRestore(); }); test('use 自定义插件对象', () => { const plugin = { name: 'my-plugin', install: jest.fn() }; MeToast.use(plugin); expect(MeToast.plugins.has('my-plugin')).toBe(true); }); test('destroy 完整清理', () => { MeToast.success('test'); MeToast.destroy(); expect(MeToast.count()).toBeGreaterThanOrEqual(0); }); test('getConfig / configure with theme and locale side effects', () => { MeToast.configure({ theme: 'warm', locale: 'en-US' }); const cfg = MeToast.getConfig(); expect(cfg.theme).toBe('warm'); expect(cfg.locale).toBe('en-US'); MeToast.resetConfig(); MeToast.i18n.switchLocale('zh-CN'); }); test('confirm 创建并返回 Promise', async () => { const promise = MeToast.confirm('确定删除?', { confirmText: '是', confirmColor: '#ff0000' }); expect(promise).toBeInstanceOf(Promise); }); test('countdown onComplete 回调', (done) => { const c = MeToast.countdown('{seconds}s', 1, { onComplete: () => { done(); }, }); expect(c.id).toBeDefined(); // timer 每秒 tick,1s 后会完成 }, 3000); }); // ================================================================== // 7. toast.ts 覆盖率补充 // ================================================================== describe('toast.ts 覆盖率', () => { test('updatePosition 无 el 时仅早返回不抛异常', () => { const toast = new Toast({ message: 'test' }); expect(toast.el).toBeNull(); // 无 el 时 updatePosition 直接返回,不改变任何状态 expect(() => toast.updatePosition('bottom-left')).not.toThrow(); }); test('updatePosition 同位置不抛异常', () => { const t = MeToast.info('test'); const initialContainer = t.el?.parentNode; t.updatePosition('top-right'); // 同位置 expect(t.el?.parentNode).toBe(initialContainer || null); }); test('close immediate 模式', (done) => { const t = MeToast.info('test'); t.close(true); expect(t.closing).toBe(true); setTimeout(() => { expect(t.el).toBeNull(); done(); }, 50); }); test('_limitToasts 超出 max 时移除最早的', () => { MeToast.configure({ max: 3 }); const t1 = MeToast.info('m1'); const t2 = MeToast.info('m2'); const t3 = MeToast.info('m3'); // 第4个应触发移除最早的 const t4 = MeToast.info('m4'); expect(MeToast.count()).toBeLessThanOrEqual(4); MeToast.resetConfig(); }); test('_buildClassName 非标准动画 fallback 到 slide', () => { const t = new Toast({ message: 'test', animation: 'unknown-anim' }); t.create(); expect(t.el?.className).toContain('met-anim-slide'); }); test('_buildContent 自定义 render', () => { const t = MeToast.show({ render: (toast: ToastInstance) => `${toast.message}`, message: 'custom-render', } as Record); // render 函数接管了 innerHTML expect(t.el).toBeDefined(); }); test('_buildContent progressDirection vertical', () => { const t = MeToast.info({ message: 'test', duration: 5000, progressDirection: 'vertical' } as Record); expect(t.el).toBeDefined(); }); test('_buildContent 无图标且非 default 类型显示 side 条', () => { const t = new Toast({ message: 'test', type: 'star', icon: false }); t.create(); expect(t.el).toBeDefined(); }); test('_applyStyles 自定义 width', () => { const t = MeToast.info({ message: 'wide', width: 500 } as Record); expect(t.el).toBeDefined(); }); test('_applyStyles 自定义 style 对象', () => { const t = MeToast.info({ message: 'styled', style: { background: 'red', 'font-size': '20px' }, } as Record); expect(t.el).toBeDefined(); }); test('_bindEvents closeOnClick 触发 onClose', () => { const onClick = jest.fn(); const t = MeToast.info({ message: 'clickable', closeOnClick: true, onClick } as Record); expect(t.el).toBeDefined(); }); test('_startTimer duration <= 0 不启动', () => { const t = MeToast.info({ message: 'sticky', duration: 0 } as Record); expect(t.el).toBeDefined(); expect(t.rafId).toBeNull(); }); test('_pause/_resume duration <= 0 不操作', () => { const t = new Toast({ message: 'test', duration: 0 }); t._pause(); expect(t.paused).toBe(false); t._resume(); expect(t.paused).toBe(false); }); test('close 重复调用不触发', () => { const t = MeToast.info('test'); t.close(); t.close(); expect(t.closing).toBe(true); }); test('close 无 el 时直接 _removeToast', () => { const t = new Toast({ message: 'test' }); t.close(); expect(t.closing).toBe(true); }); test('_destroy 调用 onClose 回调', (done) => { const onClose = jest.fn(); const t = MeToast.success({ message: 'test', onClose } as Record); t.close(true); setTimeout(() => { expect(onClose).toHaveBeenCalledWith(t); done(); }, 100); }); test('_destroy onClose 异常被捕获', (done) => { const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); const t = MeToast.success({ message: 'test', onClose: () => { throw new Error('close error'); }, } as Record); t.close(true); setTimeout(() => { expect(consoleError).toHaveBeenCalled(); consoleError.mockRestore(); done(); }, 100); }); test('_palette 默认类型', () => { const t = new Toast({ message: 'test', type: 'default' }); const palette = t._palette(); expect(palette.theme).toBeDefined(); expect(palette.c.fg).toBe('#6b7280'); }); test('_palette 未知类型 fallback default', () => { const t = new Toast({ message: 'test', type: 'unknown-type' as string }); const palette = t._palette(); expect(palette.c.fg).toBe('#6b7280'); }); test('updatePosition 切换到不同容器', () => { const t = MeToast.info('move-me'); expect(t.config.position).toBe('top-right'); t.updatePosition('bottom-left'); expect(t.config.position).toBe('bottom-left'); }); test('_palette 未知主题回退 light', () => { // 设置一个未知主题,测试 fallback 到 light const t = new Toast({ message: 'test', theme: 'unknown-theme' }); const palette = t._palette(); // 应该 fallback 到 light theme 的配置 expect(palette.t.bg).toBeDefined(); }); test('_palette theme 为 auto', () => { const t = new Toast({ message: 'test', theme: 'auto' }); const palette = t._palette(); expect(['light', 'dark']).toContain(palette.theme); }); }); // ================================================================== // 8. styles.ts 覆盖率补充 // ================================================================== describe('styles.ts 覆盖率', () => { const styles = require('../src/styles.js'); test('injectStyles 不抛异常(幂等)', () => { expect(() => styles.injectStyles()).not.toThrow(); expect(() => styles.injectStyles()).not.toThrow(); // 二次调用幂等 }); test('updateStyles 不抛异常', () => { expect(() => styles.injectStyles()).not.toThrow(); expect(() => styles.updateStyles()).not.toThrow(); }); test('updateStyles 无缓存时重新注入', () => { styles.removeStyles(); expect(() => styles.updateStyles()).not.toThrow(); }); test('removeStyles 移除样式', () => { styles.injectStyles(); styles.removeStyles(); // 不应抛异常 }); test('generateThemeVariables 生成 CSS 变量', () => { const config = { bg: '#fff', text: '#000', border: '#ccc', shadow: 'none', hoverShadow: 'none', progressBg: '#eee', closeHoverBg: '#ddd' }; const css = styles.generateThemeVariables(config); expect(css).toContain('--met-theme-bg'); }); test('applyThemeVariables / clearThemeVariables 不抛异常', () => { const config = { bg: '#fff', text: '#000', border: '#ccc', shadow: 'none', hoverShadow: 'none', progressBg: '#eee', closeHoverBg: '#ddd' }; expect(() => styles.applyThemeVariables(config)).not.toThrow(); expect(() => styles.clearThemeVariables()).not.toThrow(); }); test('getSystemTheme 返回 light/dark', () => { const theme = styles.getSystemTheme(); expect(['light', 'dark']).toContain(theme); }); test('watchSystemTheme 注册监听', () => { const cb = jest.fn(); const unsub = styles.watchSystemTheme(cb); expect(typeof unsub).toBe('function'); unsub(); }); test('autoApplySystemTheme 返回取消函数', () => { const unsub = styles.autoApplySystemTheme(); expect(typeof unsub).toBe('function'); unsub(); }); }); // ================================================================== // 9. utils.ts 覆盖率补充 // ================================================================== describe('utils.ts 覆盖率', () => { test('escapeHTML SSR 路径(无 document)', () => { // 需要绕过 mock document const originalDoc = (global as Record).document; (global as Record).document = undefined; jest.resetModules(); const { escapeHTML } = require('../src/utils.js'); const result = escapeHTML(''); expect(result).not.toContain('