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)
This commit is contained in:
+187
-14
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast 覆盖率补充测试 — 目标 95%+
|
||||
* @module tests
|
||||
* @version 0.4.0
|
||||
* @version 0.5.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.4.0');
|
||||
expect(status.version).toBe('0.5.0');
|
||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||
expect(status.theme).toBeDefined();
|
||||
expect(status.locale).toBeDefined();
|
||||
@@ -1080,12 +1080,6 @@ describe('styles.ts 覆盖率', () => {
|
||||
expect(typeof unsub).toBe('function');
|
||||
unsub();
|
||||
});
|
||||
|
||||
test('autoApplySystemTheme 返回取消函数', () => {
|
||||
const unsub = styles.autoApplySystemTheme();
|
||||
expect(typeof unsub).toBe('function');
|
||||
unsub();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
@@ -1261,12 +1255,6 @@ describe('最终补漏', () => {
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
});
|
||||
|
||||
// === styles.ts 剩余 ===
|
||||
test('autoApplySystemTheme 内部逻辑不抛异常', () => {
|
||||
const styles = require('../src/styles.js');
|
||||
expect(() => styles.autoApplySystemTheme()).not.toThrow();
|
||||
});
|
||||
|
||||
// === api.ts prompt 流程 ===
|
||||
test('prompt 创建后按钮事件绑定', async () => {
|
||||
const promise = MeToast.prompt('name?', {
|
||||
@@ -1568,3 +1556,188 @@ describe('v0.4.0 能力增强', () => {
|
||||
expect(MeToast.count()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 13. v0.5.0 异常路径与边界覆盖
|
||||
// ==================================================================
|
||||
describe('v0.5.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();
|
||||
});
|
||||
|
||||
describe('i18n.ts 异常与 fallback', () => {
|
||||
const i18n = require('../src/i18n.js');
|
||||
|
||||
test('t fallback 到 fallbackLocale', () => {
|
||||
i18n.setFallbackLocale('en-US');
|
||||
i18n.removeTranslation('zh-CN', 'retry');
|
||||
expect(i18n.t('retry')).toBe('Retry');
|
||||
i18n.addTranslations('zh-CN', { retry: '重试' });
|
||||
i18n.setFallbackLocale('zh-CN');
|
||||
});
|
||||
|
||||
test('removeTranslation 中间层级缺失提前返回', () => {
|
||||
i18n.addTranslations('ja', { a: { b: 'x' } });
|
||||
expect(() => i18n.removeTranslation('ja', 'a.nonexistent.c')).not.toThrow();
|
||||
});
|
||||
|
||||
test('saveLocale localStorage 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(() => i18n.saveLocale('en-US')).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('loadLocale localStorage 异常返回默认', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(typeof i18n.loadLocale()).toBe('string');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('getDefaultLocale 不支持浏览器语言时回退', () => {
|
||||
const orig = Object.getOwnPropertyDescriptor(navigator, 'language');
|
||||
Object.defineProperty(navigator, 'language', { value: 'zz-ZZ', configurable: true });
|
||||
expect(typeof i18n.getDefaultLocale()).toBe('string');
|
||||
if (orig) Object.defineProperty(navigator, 'language', orig);
|
||||
});
|
||||
|
||||
test('locale listener 异常不中断其他监听器', () => {
|
||||
const bad = () => { throw new Error('listener-err'); };
|
||||
const good = jest.fn();
|
||||
i18n.addLocaleListener(bad);
|
||||
i18n.addLocaleListener(good);
|
||||
expect(() => i18n.switchLocale('en-US')).not.toThrow();
|
||||
expect(good).toHaveBeenCalled();
|
||||
i18n.removeLocaleListener(bad);
|
||||
i18n.removeLocaleListener(good);
|
||||
i18n.switchLocale('zh-CN');
|
||||
});
|
||||
|
||||
test('Intl 异常时 format 系列降级返回原始值', () => {
|
||||
const origIntl = (global as Record<string, unknown>).Intl;
|
||||
(global as Record<string, unknown>).Intl = new Proxy(origIntl as object, {
|
||||
get: (t: Record<string, unknown>, p: string) => {
|
||||
if (['NumberFormat', 'DateTimeFormat', 'RelativeTimeFormat', 'PluralRules'].includes(p)) {
|
||||
return class { constructor() { throw new RangeError('invalid'); } };
|
||||
}
|
||||
return (t as Record<string, unknown>)[p];
|
||||
},
|
||||
});
|
||||
expect(i18n.formatNumber(1)).toBe('1');
|
||||
expect(i18n.formatCurrency(1)).toBe('1');
|
||||
expect(i18n.formatPercent(50)).toBe('50%');
|
||||
expect(i18n.formatDate('2026-01-01')).toBe('2026-01-01');
|
||||
expect(typeof i18n.formatRelativeTime(Date.now())).toBe('string');
|
||||
expect(i18n.formatPlural(5)).toBe('other');
|
||||
(global as Record<string, unknown>).Intl = origIntl;
|
||||
});
|
||||
|
||||
test('formatList Intl.ListFormat 缺失时逗号拼接', () => {
|
||||
const origLF = (Intl as unknown as Record<string, unknown>).ListFormat;
|
||||
(Intl as unknown as Record<string, unknown>).ListFormat = undefined;
|
||||
const result = i18n.formatList(['a', 'b']);
|
||||
expect(result).toBe('a, b');
|
||||
(Intl as unknown as Record<string, unknown>).ListFormat = origLF;
|
||||
});
|
||||
|
||||
test('plural 缺失时返回原 key', () => {
|
||||
expect(i18n.plural('missing_plural_key_xyz', 5)).toBe('missing_plural_key_xyz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('themes.ts 异常与监听', () => {
|
||||
const themes = require('../src/themes.js');
|
||||
|
||||
test('saveTheme/loadTheme localStorage 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(() => themes.saveTheme('dark')).not.toThrow();
|
||||
spy.mockRestore();
|
||||
const spy2 = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(themes.loadTheme()).toBe('auto');
|
||||
spy2.mockRestore();
|
||||
});
|
||||
|
||||
test('watchSystemTheme 重复调用先移除旧监听', () => {
|
||||
expect(() => { themes.watchSystemTheme(); themes.watchSystemTheme(); }).not.toThrow();
|
||||
themes.unwatchSystemTheme();
|
||||
});
|
||||
|
||||
test('unwatchSystemTheme 清理监听', () => {
|
||||
themes.watchSystemTheme();
|
||||
expect(() => themes.unwatchSystemTheme()).not.toThrow();
|
||||
});
|
||||
|
||||
test('theme listener 异常不中断其他监听器', () => {
|
||||
const bad = () => { throw new Error('theme-listener-err'); };
|
||||
const good = jest.fn();
|
||||
themes.addThemeListener(bad);
|
||||
themes.addThemeListener(good);
|
||||
expect(() => themes.switchTheme('dark')).not.toThrow();
|
||||
expect(good).toHaveBeenCalled();
|
||||
themes.removeThemeListener(bad);
|
||||
themes.removeThemeListener(good);
|
||||
themes.switchTheme('auto');
|
||||
});
|
||||
|
||||
test('removeThemeCSS 移除已存在样式', () => {
|
||||
themes.applyThemeCSS('dark');
|
||||
expect(() => themes.removeThemeCSS()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('styles.ts 边界', () => {
|
||||
const styles = require('../src/styles.js');
|
||||
|
||||
test('applyThemeVariables 先移除旧样式再追加', () => {
|
||||
const config = { bg: '#fff', text: '#000', border: '#ccc', shadow: 'none', hoverShadow: 'none', progressBg: '#eee', closeHoverBg: '#ddd' };
|
||||
styles.applyThemeVariables(config);
|
||||
styles.applyThemeVariables(config);
|
||||
const els = document.querySelectorAll('#metona-toast-custom-styles');
|
||||
expect(els.length).toBeLessThanOrEqual(1);
|
||||
styles.clearThemeVariables();
|
||||
});
|
||||
|
||||
test('watchSystemTheme 无 matchMedia 时返回空函数', () => {
|
||||
const orig = (window as unknown as Record<string, unknown>).matchMedia;
|
||||
(window as unknown as Record<string, unknown>).matchMedia = undefined;
|
||||
const unsub = styles.watchSystemTheme(jest.fn());
|
||||
expect(unsub).toBeInstanceOf(Function);
|
||||
(window as unknown as Record<string, unknown>).matchMedia = orig;
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.ts use 预设插件', () => {
|
||||
test('use persistence 从 localStorage 恢复配置', () => {
|
||||
localStorage.setItem('metona-toast-config', JSON.stringify({ duration: 3000 }));
|
||||
MeToast.use('persistence');
|
||||
expect(MeToast.getConfig().duration).toBe(3000);
|
||||
MeToast.plugins.unregister('persistence');
|
||||
localStorage.removeItem('metona-toast-config');
|
||||
MeToast.resetConfig();
|
||||
});
|
||||
|
||||
test('use accessibility 注册朗读钩子', () => {
|
||||
const announceSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
MeToast.use('accessibility');
|
||||
MeToast.success('announce-test');
|
||||
MeToast.plugins.unregister('accessibility');
|
||||
announceSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('重复 use persistence 不重复注册保存钩子', () => {
|
||||
localStorage.removeItem('metona-toast-config');
|
||||
MeToast.use('persistence');
|
||||
MeToast.use('persistence');
|
||||
MeToast.plugins.unregister('persistence');
|
||||
expect(Toast._hooks.has('afterClose')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user