Files
MetonaToast/tests/index.test.ts
T
tianhao 1f718c4ef8 release: v0.5.0 — 覆盖率96.42%达标、lint零警告、覆盖率门禁、CI强化、发布自动化
- 覆盖率 96.42%(行):新增 59 测试共 385 个,覆盖 DOM 交互事件、对话框按钮交互、Intl/localStorage 异常路径、React 渲染生命周期
- 测试环境重构:SSR 分支用 defineProperty 真正覆盖 document/window;新增 tests/dom.test.ts、tests/setup.ts
- jest 覆盖率门禁 lines >= 95%;CI lint 改必过;新增 publish.yml(v* tag 自动发布到 Gitea registry)
- 修复:Toast.off 空数组残留、Toast.on 取消函数 this 绑定、action close:false 冒泡关闭、use() 重复注册钩子、i18n catch 引用错误
- 删除死代码 autoApplySystemTheme;lint 零警告(no-console 策略 + caughtErrorsIgnorePattern)
2026-08-08 15:02:51 +08:00

1287 lines
42 KiB
TypeScript

/**
* MetonaToast 单元测试
* @module tests
* @version 0.5.0
*/
import MeToast, { Toast, VERSION } from '../src/index';
import type { ToastInstance, ToastConfig } from '../src/types';
// 模拟DOM元素工厂
const createMockElement = (): Record<string, unknown> => ({
className: '',
style: {} as Record<string, string>,
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<string, string>,
parentNode: {
removeChild: jest.fn(),
},
});
// 模拟DOM环境
const mockDocument = {
createElement: jest.fn(() => createMockElement()),
getElementById: jest.fn(),
querySelector: jest.fn(),
querySelectorAll: jest.fn(() => []),
head: { appendChild: jest.fn() },
body: { appendChild: jest.fn(), querySelectorAll: jest.fn(() => []) },
readyState: 'complete',
addEventListener: jest.fn(),
execCommand: jest.fn(() => true),
};
const mockWindow = {
matchMedia: jest.fn(() => ({
matches: false,
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: {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
},
Audio: jest.fn(() => ({ play: jest.fn(), volume: 0 })),
Notification: undefined,
};
// 设置全局变量
(global as Record<string, unknown>).document = mockDocument;
(global as Record<string, unknown>).window = mockWindow;
(global as Record<string, unknown>).navigator = mockWindow.navigator;
(global as Record<string, unknown>).localStorage = mockWindow.localStorage;
(global as Record<string, unknown>).requestAnimationFrame = mockWindow.requestAnimationFrame;
(global as Record<string, unknown>).cancelAnimationFrame = mockWindow.cancelAnimationFrame;
// 测试套件
describe('MetonaToast', () => {
beforeEach(() => {
jest.clearAllMocks();
MeToast._toasts.clear();
// 清理钩子残留
Toast._hooks.clear();
// 重置 _removeToast 回调
const { _containerCache } = require('../src/toast.js');
_containerCache.clear();
});
describe('版本信息', () => {
test('应该有正确的版本号', () => {
expect(VERSION).toBe('0.5.0');
expect(MeToast.version).toBe('0.5.0');
});
});
describe('基础功能', () => {
test('应该能够显示成功Toast', () => {
const toast = MeToast.success('成功消息');
expect(toast).toBeDefined();
expect(toast.id).toBeDefined();
expect(toast.type).toBe('success');
expect(toast.message).toBe('成功消息');
});
test('应该能够显示错误Toast', () => {
const toast = MeToast.error('错误消息');
expect(toast).toBeDefined();
expect(toast.type).toBe('error');
expect(toast.message).toBe('错误消息');
});
test('应该能够显示警告Toast', () => {
const toast = MeToast.warning('警告消息');
expect(toast).toBeDefined();
expect(toast.type).toBe('warning');
expect(toast.message).toBe('警告消息');
});
test('应该能够显示信息Toast', () => {
const toast = MeToast.info('信息消息');
expect(toast).toBeDefined();
expect(toast.type).toBe('info');
expect(toast.message).toBe('信息消息');
});
test('应该能够显示默认Toast', () => {
const toast = MeToast.show('默认消息');
expect(toast).toBeDefined();
expect(toast.type).toBe('default');
expect(toast.message).toBe('默认消息');
});
test('应该能够显示带标题的Toast', () => {
const toast = MeToast.success({
title: '成功',
message: '操作成功',
} as Record<string, unknown>);
expect(toast.title).toBe('成功');
expect(toast.message).toBe('操作成功');
});
test('应该能够显示加载Toast', () => {
const loading = MeToast.loading('加载中...');
expect(loading).toBeDefined();
expect(loading.id).toBeDefined();
expect(loading.success).toBeInstanceOf(Function);
expect(loading.error).toBeInstanceOf(Function);
expect(loading.info).toBeInstanceOf(Function);
expect(loading.warning).toBeInstanceOf(Function);
expect(loading.update).toBeInstanceOf(Function);
expect(loading.dismiss).toBeInstanceOf(Function);
});
});
describe('Toast管理', () => {
test('应该能够获取所有Toast', () => {
MeToast.success('消息1');
MeToast.error('消息2');
const toasts = MeToast.getToasts();
expect(toasts).toHaveLength(2);
});
test('应该能够获取Toast数量', () => {
MeToast.success('消息1');
MeToast.error('消息2');
MeToast.warning('消息3');
expect(MeToast.count()).toBe(3);
});
test('应该能够检查是否有Toast', () => {
expect(MeToast.hasToasts()).toBe(false);
MeToast.success('消息');
expect(MeToast.hasToasts()).toBe(true);
});
test('应该能够查找Toast', () => {
const toast = MeToast.success('消息');
const found = MeToast.find(toast.id);
expect(found).toBeDefined();
expect(found!.id).toBe(toast.id);
});
test('应该能够关闭Toast', () => {
const toast = MeToast.success('消息');
toast.close();
// 等待关闭动画
setTimeout(() => {
expect(MeToast.count()).toBe(0);
}, 300);
});
test('应该能够关闭所有Toast', () => {
MeToast.success('消息1');
MeToast.error('消息2');
MeToast.warning('消息3');
MeToast.dismiss();
setTimeout(() => {
expect(MeToast.count()).toBe(0);
}, 300);
});
test('应该能够清除所有Toast', () => {
MeToast.success('消息1');
MeToast.error('消息2');
MeToast.clear();
setTimeout(() => {
expect(MeToast.count()).toBe(0);
}, 300);
});
});
describe('配置管理', () => {
test('应该能够配置全局选项', () => {
MeToast.configure({
position: 'bottom-center',
duration: 5000,
theme: 'dark',
});
const config = MeToast.getConfig();
expect(config.position).toBe('bottom-center');
expect(config.duration).toBe(5000);
expect(config.theme).toBe('dark');
});
test('应该能够重置配置', () => {
MeToast.configure({
position: 'bottom-center',
duration: 5000,
});
MeToast.resetConfig();
const config = MeToast.getConfig();
expect(config.position).toBe('top-right');
expect(config.duration).toBe(4000);
});
test('应该能够更新配置', () => {
MeToast.updateConfig({ position: 'top-left' });
const config = MeToast.getConfig();
expect(config.position).toBe('top-left');
});
});
describe('主题系统', () => {
test('应该能够获取当前主题', () => {
const theme = MeToast.themes.getCurrentTheme();
expect(theme).toBeDefined();
});
test('应该能够切换主题', () => {
MeToast.themes.switchTheme('dark');
expect(MeToast.themes.getCurrentTheme()).toBe('dark');
MeToast.themes.switchTheme('light');
expect(MeToast.themes.getCurrentTheme()).toBe('light');
});
test('应该能够获取主题配置', () => {
const config = MeToast.themes.getThemeConfig('light');
expect(config).toBeDefined();
expect(config.bg).toBeDefined();
expect(config.text).toBeDefined();
expect(config.border).toBeDefined();
});
test('应该能够获取所有主题', () => {
const themes = MeToast.themes.getAllThemes();
expect(themes).toBeDefined();
expect(themes.light).toBeDefined();
expect(themes.dark).toBeDefined();
});
test('应该能够注册自定义主题', () => {
MeToast.themes.registerTheme('custom', {
bg: 'rgba(255, 255, 255, 0.96)',
text: '#1f2937',
border: 'rgba(0, 0, 0, 0.06)',
shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18)',
hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22)',
progressBg: 'rgba(0, 0, 0, 0.06)',
closeHoverBg: 'rgba(0, 0, 0, 0.06)',
});
expect(MeToast.themes.hasTheme('custom')).toBe(true);
});
});
describe('国际化系统', () => {
test('应该能够获取当前语言', () => {
const locale = MeToast.i18n.getCurrentLocale();
expect(locale).toBeDefined();
});
test('应该能够切换语言', () => {
MeToast.i18n.switchLocale('en-US');
expect(MeToast.i18n.getCurrentLocale()).toBe('en-US');
MeToast.i18n.switchLocale('zh-CN');
expect(MeToast.i18n.getCurrentLocale()).toBe('zh-CN');
});
test('应该能够获取翻译', () => {
const text = MeToast.i18n.t('success');
expect(text).toBeDefined();
});
test('应该能够检查翻译是否存在', () => {
expect(MeToast.i18n.hasTranslation('success')).toBe(true);
expect(MeToast.i18n.hasTranslation('nonexistent')).toBe(false);
});
test('应该能够获取支持的语言列表', () => {
const locales = MeToast.i18n.getSupportedLocales();
expect(locales).toContain('zh-CN');
expect(locales).toContain('en-US');
});
test('应该能够获取语言名称', () => {
const name = MeToast.i18n.getLocaleName('zh-CN');
expect(name).toBe('简体中文');
});
test('应该能够格式化数字', () => {
const formatted = MeToast.i18n.formatNumber(1234567.89);
expect(formatted).toBeDefined();
});
test('应该能够格式化货币', () => {
const formatted = MeToast.i18n.formatCurrency(99.99, 'USD');
expect(formatted).toBeDefined();
});
test('应该能够格式化日期', () => {
const formatted = MeToast.i18n.formatDate(new Date());
expect(formatted).toBeDefined();
});
});
describe('插件系统', () => {
test('应该能够注册插件', () => {
const plugin = { name: 'test-plugin', version: '1.0.0', install: jest.fn() };
MeToast.plugins.register('test', plugin);
expect(MeToast.plugins.has('test')).toBe(true);
});
test('应该能够获取插件', () => {
const plugin = { name: 'test-plugin', version: '1.0.0' };
MeToast.plugins.register('test', plugin);
const retrieved = MeToast.plugins.get('test');
expect(retrieved).toBeDefined();
expect(retrieved!.name).toBe('test-plugin');
});
test('应该能够注销插件', () => {
const plugin = { name: 'test-plugin', version: '1.0.0', uninstall: jest.fn() };
MeToast.plugins.register('test', plugin);
MeToast.plugins.unregister('test');
expect(MeToast.plugins.has('test')).toBe(false);
});
test('应该能够启用/禁用插件', () => {
const plugin = { name: 'test-plugin', version: '1.0.0' };
MeToast.plugins.register('test', plugin);
MeToast.plugins.disable('test');
expect(MeToast.plugins.isEnabled('test')).toBe(false);
MeToast.plugins.enable('test');
expect(MeToast.plugins.isEnabled('test')).toBe(true);
});
test('应该能够获取所有插件', () => {
['test', 'test-plugin', 'custom'].forEach(n => {
if (MeToast.plugins.has(n)) MeToast.plugins.unregister(n);
});
MeToast.plugins.register('test1', { name: 'test1' });
MeToast.plugins.register('test2', { name: 'test2' });
const plugins = MeToast.plugins.getAll();
expect(plugins).toHaveLength(2);
});
test('应该能够获取插件名称', () => {
MeToast.plugins.register('test1', { name: 'test1' });
MeToast.plugins.register('test2', { name: 'test2' });
const names = MeToast.plugins.getNames();
expect(names).toContain('test1');
expect(names).toContain('test2');
});
test('应该能够使用预设插件', () => {
MeToast.use('keyboard');
expect(MeToast.plugins.has('keyboard')).toBe(true);
});
});
describe('动画系统', () => {
test('应该能够获取所有动画名称', () => {
const names = MeToast.animations.getAnimationNames();
expect(names).toContain('slide');
expect(names).toContain('fade');
expect(names).toContain('scale');
expect(names).toContain('bounce');
});
test('应该能够获取动画配置', () => {
const config = MeToast.animations.get('slide');
expect(config).toBeDefined();
expect(config!.enter).toBeDefined();
expect(config!.leave).toBeDefined();
expect(config!.duration).toBeDefined();
expect(config!.easing).toBeDefined();
});
test('应该能够注册自定义动画', () => {
MeToast.animations.register('custom', {
enter: { transform: 'scale(0)', opacity: '0' },
leave: { transform: 'scale(1)', opacity: '1' },
duration: 500,
});
expect(MeToast.animations.get('custom')).toBeDefined();
});
test('应该能够注销动画', () => {
MeToast.animations.register('custom', { enter: {}, leave: {} });
MeToast.animations.unregister('custom');
expect(MeToast.animations.get('custom')).toBeNull();
});
});
describe('Promise支持', () => {
test('应该能够使用Promise风格', async () => {
const promise = Promise.resolve('success');
await MeToast.promise(promise, {
loading: '加载中...',
success: '成功',
error: '失败',
});
expect(MeToast.count()).toBeGreaterThan(0);
});
test('应该能够处理Promise拒绝', async () => {
const promise = Promise.reject(new Error('error'));
try {
await MeToast.promise(promise, {
loading: '加载中...',
success: '成功',
error: '失败',
});
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe('Toast实例', () => {
test('应该能够更新Toast', () => {
const toast = MeToast.success('原始消息');
toast.update({ title: '新标题', message: '新消息', type: 'info' });
expect(toast.title).toBe('新标题');
expect(toast.message).toBe('新消息');
expect(toast.type).toBe('info');
});
test('应该能够暂停和恢复Toast', () => {
const toast = MeToast.success({ message: '消息', duration: 5000 } as Record<string, unknown>);
toast._pause();
expect(toast.paused).toBe(true);
toast._resume();
expect(toast.paused).toBe(false);
});
test('暂停恢复应保持 remaining 时间连续性', () => {
const toast = MeToast.success({ message: '消息', duration: 5000 } as Record<string, unknown>);
toast.config.duration = 5000;
toast.startedAt = Date.now() - 2000;
toast.remaining = 3000;
toast.paused = false;
const remBefore = toast.remaining;
toast._pause();
expect(toast.paused).toBe(true);
expect(toast.remaining).toBeLessThanOrEqual(remBefore);
const pausedRemaining = toast.remaining;
toast._resume();
expect(toast.paused).toBe(false);
expect(toast.remaining).toBeGreaterThanOrEqual(pausedRemaining - 100);
});
test('update 类型变更应同步更新 DOM 类名', () => {
const toast = MeToast.info('test');
expect(toast.type).toBe('info');
toast.update({ type: 'error', message: 'changed' });
expect(toast.type).toBe('error');
});
test('update 类型变更无 DOM 时不抛异常', () => {
const toast = new Toast({ message: 'test', type: 'info' });
expect(() => toast.update({ type: 'success', message: 'ok' })).not.toThrow();
expect(toast.type).toBe('success');
});
test('应该能够获取Toast配置', () => {
const toast = MeToast.success({
message: '消息',
position: 'bottom-center',
duration: 5000,
} as Record<string, unknown>);
expect(toast.config.position).toBe('bottom-center');
expect(toast.config.duration).toBe(5000);
});
});
describe('状态信息', () => {
test('应该能够获取状态信息', () => {
MeToast.success('消息');
const status = MeToast.getStatus();
expect(status.version).toBe('0.5.0');
expect(status.toasts).toBeGreaterThanOrEqual(0);
expect(status.theme).toBeDefined();
expect(status.locale).toBeDefined();
expect(status.plugins).toBeInstanceOf(Array);
expect(status.animations).toBeGreaterThanOrEqual(0);
});
});
describe('销毁功能', () => {
test('应该能够销毁所有Toast', () => {
MeToast.success('消息1');
MeToast.error('消息2');
MeToast.destroy();
setTimeout(() => {
expect(MeToast.count()).toBe(0);
}, 300);
});
});
});
describe('Toast类', () => {
test('应该能够创建Toast实例', () => {
const toast = new Toast({ type: 'success', title: '成功', message: '操作成功' });
expect(toast).toBeDefined();
expect(toast.type).toBe('success');
expect(toast.title).toBe('成功');
expect(toast.message).toBe('操作成功');
});
test('应该能够生成唯一ID', () => {
const toast1 = new Toast({ message: '消息1' });
const toast2 = new Toast({ message: '消息2' });
expect(toast1.id).not.toBe(toast2.id);
});
test('应该能够设置默认配置', () => {
const toast = new Toast({ message: '消息' });
expect(toast.config).toBeDefined();
expect(toast.config.position).toBe('top-right');
expect(toast.config.duration).toBe(4000);
expect(toast.config.max).toBe(6);
});
test('应该能够合并配置', () => {
const toast = new Toast({ message: '消息', position: 'bottom-center', duration: 5000 });
expect(toast.config.position).toBe('bottom-center');
expect(toast.config.duration).toBe(5000);
});
test('应该能够设置类型', () => {
const types = ['success', 'error', 'warning', 'info', 'loading', 'default'];
types.forEach((type) => {
const toast = new Toast({ type, message: '消息' });
expect(toast.type).toBe(type);
});
});
test('应该能够设置标题', () => {
const toast = new Toast({ title: '标题', message: '消息' });
expect(toast.title).toBe('标题');
});
test('应该能够设置消息', () => {
const toast = new Toast({ message: '消息内容' });
expect(toast.message).toBe('消息内容');
});
test('应该能够设置HTML内容', () => {
const toast = new Toast({ html: '<b>加粗</b>' });
expect(toast.html).toBe('<b>加粗</b>');
});
test('应该能够设置自定义图标', () => {
const toast = new Toast({ iconHTML: '<svg>...</svg>' });
expect(toast.iconHTML).toBe('<svg>...</svg>');
});
test('应该能够更新Toast', () => {
const toast = new Toast({ message: '原始消息' });
toast.update({ title: '新标题', message: '新消息', type: 'success' });
expect(toast.title).toBe('新标题');
expect(toast.message).toBe('新消息');
expect(toast.type).toBe('success');
});
test('应该能够关闭Toast', (done) => {
const toast = new Toast({ message: '消息' });
toast.close();
expect(toast.closing).toBe(true);
setTimeout(() => {
expect(toast.el).toBeNull();
done();
}, 350);
});
test('应该能够暂停计时', () => {
const toast = new Toast({ message: '消息', duration: 5000 });
toast._pause();
expect(toast.paused).toBe(true);
});
test('应该能够恢复计时', () => {
const toast = new Toast({ message: '消息', duration: 5000 });
toast._pause();
toast._resume();
expect(toast.paused).toBe(false);
});
test('应该能够获取配色方案', () => {
const toast = new Toast({ type: 'success' });
const palette = toast._palette();
expect(palette).toBeDefined();
expect(palette.theme).toBeDefined();
expect(palette.c).toBeDefined();
expect(palette.t).toBeDefined();
});
});
describe('工具函数', () => {
test('应该能够生成唯一ID', () => {
const { generateId } = require('../src/utils.js');
const id1 = generateId();
const id2 = generateId();
expect(id1).not.toBe(id2);
expect(id1).toMatch(/^met-/);
});
test('应该能够转义HTML', () => {
const { escapeHTML } = require('../src/utils.js');
const escaped = escapeHTML('<script>alert("xss")</script>');
expect(escaped).not.toContain('<script>');
expect(escaped).toContain('&lt;script&gt;');
});
test('应该能够检测暗色模式偏好', () => {
const { prefersDark } = require('../src/utils.js');
const result = prefersDark();
expect(typeof result).toBe('boolean');
});
});
describe('常量', () => {
test('应该有默认配置', () => {
const { DEFAULTS } = require('../src/constants.js');
expect(DEFAULTS).toBeDefined();
expect(DEFAULTS.position).toBe('top-right');
expect(DEFAULTS.duration).toBe(4000);
expect(DEFAULTS.max).toBe(6);
});
test('应该有图标', () => {
const { ICONS } = require('../src/constants.js');
expect(ICONS).toBeDefined();
expect(ICONS.success).toBeDefined();
expect(ICONS.error).toBeDefined();
expect(ICONS.warning).toBeDefined();
expect(ICONS.info).toBeDefined();
expect(ICONS.loading).toBeDefined();
});
test('应该有类型颜色', () => {
const { TYPE_COLORS } = require('../src/constants.js');
expect(TYPE_COLORS).toBeDefined();
expect(TYPE_COLORS.success).toBeDefined();
expect(TYPE_COLORS.error).toBeDefined();
expect(TYPE_COLORS.warning).toBeDefined();
expect(TYPE_COLORS.info).toBeDefined();
expect(TYPE_COLORS.loading).toBeDefined();
});
test('应该有动画配置', () => {
const { ANIMATIONS } = require('../src/constants.js');
expect(ANIMATIONS).toBeDefined();
expect(ANIMATIONS.slide).toBeDefined();
expect(ANIMATIONS.fade).toBeDefined();
expect(ANIMATIONS.scale).toBeDefined();
expect(ANIMATIONS.bounce).toBeDefined();
});
test('应该有主题配置', () => {
const { THEMES } = require('../src/constants.js');
expect(THEMES).toBeDefined();
expect(THEMES.light).toBeDefined();
expect(THEMES.dark).toBeDefined();
});
test('应该有位置配置', () => {
const { POSITIONS } = require('../src/constants.js');
expect(POSITIONS).toContain('top-left');
expect(POSITIONS).toContain('top-center');
expect(POSITIONS).toContain('top-right');
expect(POSITIONS).toContain('bottom-left');
expect(POSITIONS).toContain('bottom-center');
expect(POSITIONS).toContain('bottom-right');
});
test('应该有类型配置', () => {
const { TYPES } = require('../src/constants.js');
expect(TYPES).toContain('default');
expect(TYPES).toContain('success');
expect(TYPES).toContain('error');
expect(TYPES).toContain('warning');
expect(TYPES).toContain('info');
expect(TYPES).toContain('loading');
});
test('应该有国际化配置', () => {
const { LOCALES } = require('../src/constants.js');
expect(LOCALES).toBeDefined();
expect(LOCALES['zh-CN']).toBeDefined();
expect(LOCALES['en-US']).toBeDefined();
});
});
describe('主题管理器', () => {
test('应该能够获取系统主题', () => {
const { getSystemTheme } = require('../src/themes.js');
const theme = getSystemTheme();
expect(['light', 'dark']).toContain(theme);
});
test('应该能够解析主题', () => {
const { resolveTheme } = require('../src/themes.js');
expect(resolveTheme('light')).toBe('light');
expect(resolveTheme('dark')).toBe('dark');
expect(resolveTheme('auto')).toBeDefined();
});
test('应该能够获取主题配置', () => {
const { getThemeConfig } = require('../src/themes.js');
const config = getThemeConfig('light');
expect(config).toBeDefined();
expect(config.bg).toBeDefined();
expect(config.text).toBeDefined();
});
test('应该能够应用主题', () => {
const { applyTheme } = require('../src/themes.js');
applyTheme('light');
applyTheme('dark');
applyTheme('auto');
});
test('应该能够获取当前主题', () => {
const { getCurrentTheme } = require('../src/themes.js');
const theme = getCurrentTheme();
expect(theme).toBeDefined();
});
test('应该能够切换主题', () => {
const { switchTheme } = require('../src/themes.js');
switchTheme('light');
switchTheme('dark');
switchTheme('auto');
});
test('应该能够获取所有主题', () => {
const { getAllThemes } = require('../src/themes.js');
const themes = getAllThemes();
expect(themes).toBeDefined();
expect(themes.light).toBeDefined();
expect(themes.dark).toBeDefined();
});
test('应该能够获取主题名称列表', () => {
const { getThemeNames } = require('../src/themes.js');
const names = getThemeNames();
expect(names).toContain('light');
expect(names).toContain('dark');
});
test('应该能够检查主题是否存在', () => {
const { hasTheme } = require('../src/themes.js');
expect(hasTheme('light')).toBe(true);
expect(hasTheme('dark')).toBe(true);
expect(hasTheme('nonexistent')).toBe(false);
});
});
describe('国际化管理器', () => {
test('应该能够获取当前语言', () => {
const { getCurrentLocale } = require('../src/i18n.js');
const locale = getCurrentLocale();
expect(locale).toBeDefined();
});
test('应该能够设置当前语言', () => {
const { setCurrentLocale } = require('../src/i18n.js');
setCurrentLocale('en-US');
setCurrentLocale('zh-CN');
});
test('应该能够获取翻译', () => {
const { t } = require('../src/i18n.js');
const text = t('success');
expect(text).toBeDefined();
});
test('应该能够检查翻译是否存在', () => {
const { hasTranslation } = require('../src/i18n.js');
expect(hasTranslation('success')).toBe(true);
expect(hasTranslation('nonexistent')).toBe(false);
});
test('应该能够获取支持的语言列表', () => {
const { getSupportedLocales } = require('../src/i18n.js');
const locales = getSupportedLocales();
expect(locales).toContain('zh-CN');
expect(locales).toContain('en-US');
});
test('应该能够检查语言是否支持', () => {
const { isLocaleSupported } = require('../src/i18n.js');
expect(isLocaleSupported('zh-CN')).toBe(true);
expect(isLocaleSupported('en-US')).toBe(true);
expect(isLocaleSupported('nonexistent')).toBe(false);
});
test('应该能够获取语言名称', () => {
const { getLocaleName } = require('../src/i18n.js');
expect(getLocaleName('zh-CN')).toBe('简体中文');
expect(getLocaleName('en-US')).toBe('English (US)');
});
test('应该能够获取语言方向', () => {
const { getLocaleDirection } = require('../src/i18n.js');
expect(getLocaleDirection('zh-CN')).toBe('ltr');
expect(getLocaleDirection('ar')).toBe('rtl');
});
test('应该能够格式化数字', () => {
const { formatNumber } = require('../src/i18n.js');
const formatted = formatNumber(1234567.89);
expect(formatted).toBeDefined();
});
test('应该能够格式化货币', () => {
const { formatCurrency } = require('../src/i18n.js');
const formatted = formatCurrency(99.99, 'USD');
expect(formatted).toBeDefined();
});
test('应该能够格式化日期', () => {
const { formatDate } = require('../src/i18n.js');
const formatted = formatDate(new Date());
expect(formatted).toBeDefined();
});
});
describe('插件管理器', () => {
test('应该能够创建插件管理器', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
expect(manager).toBeDefined();
expect(manager.register).toBeInstanceOf(Function);
expect(manager.unregister).toBeInstanceOf(Function);
});
test('应该能够注册插件', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
manager.register('test', { name: 'test', version: '1.0.0' });
expect(manager.has('test')).toBe(true);
});
test('应该能够获取插件', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
manager.register('test', { name: 'test', version: '1.0.0' });
const plugin = manager.get('test');
expect(plugin).toBeDefined();
expect(plugin!.name).toBe('test');
});
test('应该能够注销插件', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
manager.register('test', { name: 'test', version: '1.0.0', uninstall: jest.fn() });
manager.unregister('test');
expect(manager.has('test')).toBe(false);
});
test('应该能够启用/禁用插件', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
manager.register('test', { name: 'test', version: '1.0.0' });
manager.disable('test');
expect(manager.isEnabled('test')).toBe(false);
manager.enable('test');
expect(manager.isEnabled('test')).toBe(true);
});
test('应该能够获取所有插件', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
manager.register('test1', { name: 'test1' });
manager.register('test2', { name: 'test2' });
const plugins = manager.getAll();
expect(plugins).toHaveLength(2);
});
test('应该能够获取插件名称', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
manager.register('test1', { name: 'test1' });
manager.register('test2', { name: 'test2' });
const names = manager.getNames();
expect(names).toContain('test1');
expect(names).toContain('test2');
});
test('应该能够注册和注销插件', () => {
const { PluginManager } = require('../src/plugins.js');
const manager = new PluginManager();
manager.register('p1', { name: 'p1' });
manager.register('p2', { name: 'p2' });
expect(manager.getAll()).toHaveLength(2);
manager.unregister('p1');
expect(manager.getAll()).toHaveLength(1);
});
});
// ========== 高级功能 & 钩子系统 & 边界情况 ==========
describe('高级功能', () => {
test('confirm 非字符串参数返回 false', async () => {
const result = await MeToast.confirm(123 as unknown as string);
expect(result).toBe(false);
});
test('prompt 非字符串参数返回 null', async () => {
const result = await MeToast.prompt(123 as unknown as string);
expect(result).toBeNull();
});
test('progress 返回控制对象', () => {
const p = MeToast.progress('上传中...');
expect(p).toBeDefined();
expect(p.id).toBeDefined();
expect(p.setProgress).toBeInstanceOf(Function);
expect(p.complete).toBeInstanceOf(Function);
expect(p.error).toBeInstanceOf(Function);
expect(p.dismiss).toBeInstanceOf(Function);
});
test('countdown 返回控制对象', () => {
const c = MeToast.countdown('{seconds} 秒后执行', 3);
expect(c).toBeDefined();
expect(c.id).toBeDefined();
expect(c.cancel).toBeInstanceOf(Function);
expect(c.pause).toBeInstanceOf(Function);
expect(c.resume).toBeInstanceOf(Function);
c.cancel();
});
test('countdown 非字符串参数返回空控制对象', () => {
const c = MeToast.countdown(123 as unknown as string, 5);
expect(c.cancel).toBeInstanceOf(Function);
expect(c.pause).toBeInstanceOf(Function);
});
test('queue 返回 thenable', () => {
const result = MeToast.queue(['a', 'b', 'c'], { delay: 10, duration: 10 });
expect(result.then).toBeInstanceOf(Function);
expect(result.cancel).toBeInstanceOf(Function);
});
test('stack 不抛出异常', () => {
expect(() => MeToast.stack(['a', 'b', 'c'], { stagger: 10 })).not.toThrow();
});
test('stack 非数组不影响', () => {
expect(() => MeToast.stack('not-an-array' as unknown as string[])).not.toThrow();
});
});
describe('Toast钩子系统', () => {
beforeEach(() => {
MeToast._toasts.clear();
});
test('Toast.on 注册钩子并返回取消函数', () => {
const handler = jest.fn();
const unsubscribe = Toast.on('afterShow', handler);
expect(typeof unsubscribe).toBe('function');
Toast.off('afterShow', handler);
});
test('Toast.trigger 触发已注册钩子', () => {
const handler = jest.fn();
Toast.on('afterShow', handler);
const toast = MeToast.success('test');
Toast.trigger('afterShow', toast as unknown as import('../src/toast.js').Toast);
expect(handler).toHaveBeenCalled();
Toast.off('afterShow', handler);
});
test('Toast.off 移除钩子后不再触发', () => {
const handler = jest.fn();
Toast.on('beforeShow', handler);
Toast.off('beforeShow', handler);
const toast = MeToast.success('test');
Toast.trigger('beforeShow', toast as unknown as import('../src/toast.js').Toast);
expect(handler).not.toHaveBeenCalled();
});
test('钩子错误不中断其他钩子', () => {
const goodHandler = jest.fn();
const badHandler = jest.fn(() => { throw new Error('hook error'); });
Toast.on('afterClose', badHandler);
Toast.on('afterClose', goodHandler);
const toast = MeToast.success('test');
Toast.trigger('afterClose', toast as unknown as import('../src/toast.js').Toast);
expect(badHandler).toHaveBeenCalled();
expect(goodHandler).toHaveBeenCalled();
Toast.off('afterClose', badHandler);
Toast.off('afterClose', goodHandler);
});
test('完整生命周期钩子触发顺序', () => {
const calls: string[] = [];
const beforeShow = jest.fn(() => calls.push('beforeShow'));
const afterShow = jest.fn(() => calls.push('afterShow'));
Toast.on('beforeShow', beforeShow);
Toast.on('afterShow', afterShow);
const toast = new Toast({ message: 'test' });
Toast.trigger('beforeShow', toast);
Toast.trigger('afterShow', toast);
expect(calls).toEqual(['beforeShow', 'afterShow']);
Toast.off('beforeShow', beforeShow);
Toast.off('afterShow', afterShow);
});
});
describe('边界情况', () => {
beforeEach(() => {
MeToast._toasts.clear();
const { _containerCache } = require('../src/toast.js');
_containerCache.clear();
});
test('空消息 toast 不抛异常', () => {
expect(() => MeToast.success('')).not.toThrow();
expect(() => MeToast.show()).not.toThrow();
});
test('configure 空参数不抛异常', () => {
expect(() => MeToast.configure()).not.toThrow();
expect(() => MeToast.configure(null as unknown as Partial<ToastConfig>)).not.toThrow();
});
test('max 限制 toast 数量', () => {
MeToast.configure({ max: 3 });
for (let i = 0; i < 10; i++) {
MeToast.info('msg ' + i);
}
expect(() => MeToast.configure({ max: 6 })).not.toThrow();
MeToast.resetConfig();
});
test('dismiss 指定 id 关闭单个 toast', () => {
const t1 = MeToast.success('msg1');
const t2 = MeToast.error('msg2');
expect(MeToast.count()).toBe(2);
MeToast.dismiss(t1.id);
expect(t1.closing).toBe(true);
});
test('find 不存在的 id 返回 undefined', () => {
expect(MeToast.find('nonexistent-id')).toBeUndefined();
});
test('findByType 按类型过滤', () => {
MeToast.success('s1');
MeToast.success('s2');
MeToast.error('e1');
const successToasts = MeToast.findByType('success');
expect(successToasts.length).toBe(2);
const errorToasts = MeToast.findByType('error');
expect(errorToasts.length).toBe(1);
});
test('promise 非 Promise 参数返回 rejected Promise', async () => {
try {
await MeToast.promise('not-a-promise' as unknown as Promise<unknown>, {});
fail('should have thrown');
} catch (e) {
expect(e).toBeDefined();
}
});
test('getToast 不存在的 id 返回 null', () => {
expect(MeToast.getToast('nonexistent')).toBeNull();
expect(MeToast.getToast(null as unknown as string)).toBeNull();
expect(MeToast.getToast(undefined as unknown as string)).toBeNull();
});
});
// ========== 新增功能测试 ==========
describe('新增功能', () => {
beforeEach(() => {
MeToast._toasts.clear();
const { _containerCache } = require('../src/toast.js');
_containerCache.clear();
});
test('resetTimerOnUpdate — update后重置计时器', () => {
const t = MeToast.info('test', { duration: 5000, resetTimerOnUpdate: true });
t.update({ message: 'updated' });
expect(t.remaining).toBe(5000);
expect(t.paused).toBe(false);
});
test('onUpdate 回调触发', () => {
const fn = jest.fn();
const t = MeToast.success('test', { onUpdate: fn } as Record<string, unknown>);
t.update({ message: 'changed' });
expect(fn).toHaveBeenCalledWith(t);
});
test('action 返回控制对象', () => {
const a = MeToast.action('test', [{ text: 'OK', onClick: () => {} }]);
expect(a).toBeDefined();
expect(a.id).toBeDefined();
expect(a.toast).toBeDefined();
expect(a.dismiss).toBeInstanceOf(Function);
});
test('queue 返回可取消对象', () => {
const q = MeToast.queue(['a', 'b'], { delay: 10, duration: 10 });
expect(q.then).toBeInstanceOf(Function);
expect(q.catch).toBeInstanceOf(Function);
expect(q.cancel).toBeInstanceOf(Function);
q.cancel();
});
test('group 创建分组并正常工作', () => {
const g = MeToast.group('test-group');
expect(g._group).toBe('test-group');
expect(g.dismiss).toBeInstanceOf(Function);
expect(g.count).toBeInstanceOf(Function);
const t = g.info('grouped message');
expect(t.group).toBe('test-group');
});
test('dismissGroup 按组关闭', () => {
const g = MeToast.group('batch');
g.success('m1');
g.error('m2');
expect(MeToast._groupCount('batch')).toBe(2);
MeToast.dismissGroup('batch');
});
test('自定义渲染函数', () => {
const t = MeToast.show({
render: (toast: ToastInstance) => `<b>${toast.message}</b>`,
message: 'rendered',
} as Record<string, unknown>);
expect(t.config.render).toBeInstanceOf(Function);
});
test('onError 回调在钩子异常时触发', () => {
const errors: Array<Record<string, unknown>> = [];
MeToast.configure({ onError: (e: Record<string, unknown>) => errors.push(e) } as Record<string, unknown>);
const badHandler = () => { throw new Error('test-error'); };
Toast.on('afterShow', badHandler);
const toast = MeToast.success('test');
Toast.trigger('afterShow', toast as unknown as import('../src/toast.js').Toast);
expect(errors.length).toBeGreaterThanOrEqual(1);
expect((errors[0].error as Error).message).toBe('test-error');
Toast.off('afterShow', badHandler);
MeToast.configure({ onError: null } as unknown as Partial<ToastConfig>);
});
test('destroy 重复调用不抛异常', () => {
MeToast.success('test');
MeToast.destroy();
expect(() => MeToast.destroy()).not.toThrow();
});
});
// ========== v0.4.0 新增功能测试 ==========
describe('v0.4.0 新增功能', () => {
beforeEach(() => {
MeToast._toasts.clear();
const { _containerCache } = require('../src/toast.js');
_containerCache.clear();
});
test('beforeShow 钩子返回 false 阻止显示', () => {
const handler = jest.fn(() => false);
Toast.on('beforeShow', handler);
const toast = MeToast.success('should-not-show');
expect(handler).toHaveBeenCalled();
// 钩子返回 false 后不应该创建 DOM 元素
expect(toast.el).toBeNull();
Toast.off('beforeShow', handler);
});
test('onBeforeShow 配置返回 false 阻止显示', () => {
const toast = MeToast.success({
message: 'test',
onBeforeShow: () => false,
} as Record<string, unknown>);
expect(toast.el).toBeNull();
});
test('updatePosition 移动 Toast 到新位置', () => {
const toast = MeToast.info('movable');
expect(toast.config.position).toBe('top-right');
toast.updatePosition('bottom-left');
expect(toast.config.position).toBe('bottom-left');
});
test('remove() 立即移除不触发离场动画', () => {
const toast = MeToast.info('test');
expect(MeToast.count()).toBe(1);
toast.remove();
expect(toast.el).toBeNull();
expect(MeToast.count()).toBe(0);
});
test('removeToast() 公开 API', () => {
const toast = MeToast.warning('test');
expect(MeToast.count()).toBe(1);
MeToast.removeToast(toast.id);
expect(MeToast.count()).toBe(0);
});
test('removeToast 不存在的 id 不抛异常', () => {
expect(() => MeToast.removeToast('nonexistent')).not.toThrow();
});
test('RTL 语言自动翻转容器位置', () => {
// 直接测试 _getContainer 的 RTL 翻转逻辑
const { getLocaleDirection, getCurrentLocale, setCurrentLocale, addTranslations } = require('../src/i18n.js');
const originalLocale = getCurrentLocale();
// 先为 'ar' 注册空翻译数据,然后切换
addTranslations('ar', { close: 'إغلاق' });
setCurrentLocale('ar');
try {
const toast = new Toast({ message: 'test', position: 'top-left' });
const container = toast._getContainer();
// RTL 下 top-left 应翻转为 top-right
expect(container.className).toContain('top-right');
toast.close();
} finally {
setCurrentLocale(originalLocale);
}
});
});