release: v0.4.0 — React 适配器、loading链式id稳定、dragThreshold、RTL适配、dedupe插件
- React 适配器子包:@metona-team/metona-toast/react — useToast() hook + 声明式 <Toast /> 组件,组件卸载自动清理;react 为 optional peerDependency,主包保持零依赖 - loading 链式转换改原地 update:id 稳定不重建 DOM,转换后 duration 恢复默认自动关闭;update() 支持 duration 变更动态重启计时器 - _emit 幽灵实例修复:beforeShow 拦截后的 toast 不再注册进 _toasts - dragThreshold 配置项(默认 120px,原硬编码);RTL 容器 dir 属性 + 进度条/side 条/色条镜像 - dedupe 预设插件:相同 type+message 自动去重,支持 uninstall - jest 环境隔离测试独立成 tests/ssr.test.ts(resetModules 不再污染共享模块状态);新增 tests/setup.ts 补 TextEncoder - 文档:README/docs.html 补 React 适配器、dedupe、dragThreshold;CHANGELOG 更新;328 测试全过
This commit is contained in:
+126
-51
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast 覆盖率补充测试 — 目标 95%+
|
||||
* @module tests
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast, VERSION } from '../src/index';
|
||||
@@ -809,7 +809,7 @@ describe('api.ts 覆盖率', () => {
|
||||
|
||||
test('getStatus 返回完整状态', () => {
|
||||
const status = MeToast.getStatus();
|
||||
expect(status.version).toBe('0.3.0');
|
||||
expect(status.version).toBe('0.4.0');
|
||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||
expect(status.theme).toBeDefined();
|
||||
expect(status.locale).toBeDefined();
|
||||
@@ -1089,32 +1089,8 @@ describe('styles.ts 覆盖率', () => {
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 9. utils.ts 覆盖率补充
|
||||
// 9. utils.ts 覆盖率补充(SSR 分支见 tests/ssr.test.ts — 独立环境隔离)
|
||||
// ==================================================================
|
||||
describe('utils.ts 覆盖率', () => {
|
||||
test('escapeHTML SSR 路径(无 document)', () => {
|
||||
// 需要绕过 mock document
|
||||
const originalDoc = (global as Record<string, unknown>).document;
|
||||
(global as Record<string, unknown>).document = undefined;
|
||||
jest.resetModules();
|
||||
const { escapeHTML } = require('../src/utils.js');
|
||||
const result = escapeHTML('<script>alert("xss")</script>');
|
||||
expect(result).not.toContain('<script>');
|
||||
expect(result).toContain('<script>');
|
||||
(global as Record<string, unknown>).document = originalDoc;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('prefersDark 无 window 时返回 false', () => {
|
||||
const originalWindow = (global as Record<string, unknown>).window;
|
||||
(global as Record<string, unknown>).window = undefined;
|
||||
jest.resetModules();
|
||||
const { prefersDark } = require('../src/utils.js');
|
||||
expect(prefersDark()).toBe(false);
|
||||
(global as Record<string, unknown>).window = originalWindow;
|
||||
jest.resetModules();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 10. 最终补漏 — 针对性覆盖剩余未覆盖行
|
||||
@@ -1277,17 +1253,7 @@ describe('最终补漏', () => {
|
||||
unsub();
|
||||
});
|
||||
|
||||
// === themes.ts localStorage 异常路径 ===
|
||||
test('saveTheme localStorage 异常被捕获', () => {
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = undefined;
|
||||
jest.resetModules();
|
||||
const { saveTheme } = require('../src/themes.js');
|
||||
expect(() => saveTheme('dark')).not.toThrow();
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
// === themes.ts localStorage 异常路径(详见 tests/ssr.test.ts) ===
|
||||
test('loadTheme/loadLocale 异常路径不抛异常', () => {
|
||||
const { loadTheme } = require('../src/themes.js');
|
||||
const { loadLocale } = require('../src/i18n.js');
|
||||
@@ -1295,19 +1261,6 @@ describe('最终补漏', () => {
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
});
|
||||
|
||||
test('loadLocale localStorage 异常返回 default', () => {
|
||||
const i18n = require('../src/i18n.js');
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = {
|
||||
getItem: () => { throw new Error('quota'); },
|
||||
};
|
||||
jest.resetModules();
|
||||
const { loadLocale } = require('../src/i18n.js');
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
// === styles.ts 剩余 ===
|
||||
test('autoApplySystemTheme 内部逻辑不抛异常', () => {
|
||||
const styles = require('../src/styles.js');
|
||||
@@ -1493,3 +1446,125 @@ describe('v0.3.0 修复验证', () => {
|
||||
}, 120);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 12. v0.4.0 能力增强验证
|
||||
// ==================================================================
|
||||
describe('v0.4.0 能力增强', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
test('loading 链式转换 id 稳定(原地 update)', () => {
|
||||
const l = MeToast.loading('正在加载');
|
||||
const idBefore = l.id;
|
||||
const toast = l.success('加载完成');
|
||||
expect(toast).not.toBeNull();
|
||||
expect(toast!.id).toBe(idBefore);
|
||||
expect(toast!.type).toBe('success');
|
||||
expect(toast!.message).toBe('加载完成');
|
||||
expect(MeToast.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('loading 转换后 duration 恢复默认自动关闭', () => {
|
||||
MeToast.configure({ duration: 4000 });
|
||||
const l = MeToast.loading('loading');
|
||||
const toast = l.error('失败');
|
||||
expect(toast!.config.duration).toBe(4000);
|
||||
MeToast.resetConfig();
|
||||
});
|
||||
|
||||
test('loading 转换支持 opts 覆盖 duration', () => {
|
||||
const l = MeToast.loading('loading');
|
||||
const toast = l.warning('警告', { duration: 8000 });
|
||||
expect(toast!.config.duration).toBe(8000);
|
||||
});
|
||||
|
||||
test('update duration 变更重启计时器', () => {
|
||||
const t = MeToast.info('test', { duration: 0 });
|
||||
expect(t.rafId).toBeNull();
|
||||
t.update({ duration: 5000 });
|
||||
expect(t.config.duration).toBe(5000);
|
||||
expect(t.rafId).not.toBeNull();
|
||||
t.update({ duration: 0 });
|
||||
expect(t.config.duration).toBe(0);
|
||||
expect(t.rafId).toBeNull();
|
||||
});
|
||||
|
||||
test('dragThreshold 配置生效且默认 120', () => {
|
||||
expect(MeToast.getConfig().dragThreshold).toBe(120);
|
||||
const t = MeToast.info('x', { dragThreshold: 200 });
|
||||
expect(t.config.dragThreshold).toBe(200);
|
||||
});
|
||||
|
||||
test('RTL 容器设置 dir 属性', () => {
|
||||
const { getCurrentLocale, setCurrentLocale, addTranslations } = require('../src/i18n.js');
|
||||
const originalLocale = getCurrentLocale();
|
||||
// 'ar' 需先注册翻译,否则 setCurrentLocale fallback 回默认语言
|
||||
addTranslations('ar', { close: 'إغلاق' });
|
||||
setCurrentLocale('ar');
|
||||
try {
|
||||
const toast = new Toast({ message: 'test', position: 'top-left' });
|
||||
const container = toast._getContainer();
|
||||
expect(container.getAttribute('dir')).toBe('rtl');
|
||||
toast.close(true);
|
||||
} finally {
|
||||
setCurrentLocale(originalLocale);
|
||||
}
|
||||
});
|
||||
|
||||
test('LTR 容器不设置 dir 属性', () => {
|
||||
const { getCurrentLocale, setCurrentLocale } = require('../src/i18n.js');
|
||||
const originalLocale = getCurrentLocale();
|
||||
setCurrentLocale('zh-CN');
|
||||
try {
|
||||
const toast = new Toast({ message: 'test' });
|
||||
const container = toast._getContainer();
|
||||
expect(container.getAttribute('dir')).toBeNull();
|
||||
toast.close(true);
|
||||
} finally {
|
||||
setCurrentLocale(originalLocale);
|
||||
}
|
||||
});
|
||||
|
||||
test('dedupe 插件同消息去重', () => {
|
||||
MeToast.use('dedupe');
|
||||
MeToast.info('same message');
|
||||
MeToast.info('same message');
|
||||
expect(MeToast.count()).toBe(1);
|
||||
MeToast.info('different message');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
MeToast.plugins.unregister('dedupe');
|
||||
// 卸载后不再去重
|
||||
MeToast._toasts.clear();
|
||||
MeToast.info('same message');
|
||||
MeToast.info('same message');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
});
|
||||
|
||||
test('dedupe 插件不同 type 不去重', () => {
|
||||
MeToast.use('dedupe');
|
||||
MeToast.info('msg');
|
||||
MeToast.success('msg');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
MeToast.plugins.unregister('dedupe');
|
||||
});
|
||||
|
||||
test('dedupe 插件 uninstall 移除钩子', () => {
|
||||
MeToast.use('dedupe');
|
||||
MeToast.plugins.unregister('dedupe');
|
||||
MeToast._toasts.clear();
|
||||
MeToast.info('m');
|
||||
MeToast.info('m');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user