chore: v2.0.1 - 精简dead code + 版本升级

- utils.js: 950行→180行,移除30+未使用函数
- styles.js: 1800行→650行,移除未使用组件样式和重复规则
- animations.js: 850行→120行,移除Web Animations API dead code
- 全项目版本号升级到 2.0.1
- 测试同步清理,134/134通过
This commit is contained in:
2026-07-23 13:18:24 +08:00
parent 5843cff430
commit b4ffaabedd
17 changed files with 280 additions and 3880 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@metona-team/metona-toast",
"version": "2.0.0",
"version": "2.0.1",
"description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。",
"main": "dist/metona-toast.js",
"module": "src/index.js",
+1 -1
View File
@@ -273,7 +273,7 @@
</main>
<footer>
MetonaToast v2.0.0 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
MetonaToast v2.0.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
</footer>
<script src="../dist/metona-toast.js"></script>
+1 -1
View File
@@ -87,7 +87,7 @@
<main class="main">
<h1>API 文档</h1>
<p>MetonaToast v2.0.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
<p>MetonaToast v2.0.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
<!-- ===== 基础通知 ===== -->
<h2 id="show">show(message, opts?)</h2>
+1 -1
View File
@@ -237,7 +237,7 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
</section>
<footer>
<p>MetonaToast v2.0.0 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
<p>MetonaToast v2.0.1 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
</footer>
<script src="../dist/metona-toast.js"></script>
+37 -782
View File
@@ -1,464 +1,38 @@
/**
* MetonaToast Animations - 动画管理
* MetonaToast Animations - 动画管理(精简版 v2.0.1
* @module animations
* @version 2.0.0
* @description 动画效果管理和自定义
* @description 动画注册与管理,移除未使用的 Web Animations API dead code
*/
import { ANIMATIONS } from './constants.js';
// 动画缓存
const animationCache = new Map();
const animationMap = new Map();
/**
* 动画管理器类
*/
class AnimationManager {
constructor() {
this.animations = new Map();
this.activeAnimations = new Map();
this.animationId = 0;
// 注册默认动画
this._registerDefaults();
}
/**
* 注册默认动画
*/
_registerDefaults() {
Object.entries(ANIMATIONS).forEach(([name, config]) => {
this.register(name, config);
});
}
/**
* 注册动画
* @param {string} name - 动画名称
* @param {Object} config - 动画配置
*/
register(name, config) {
const animation = {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
delay: config.delay || 0,
iterations: config.iterations || 1,
direction: config.direction || 'normal',
fillMode: config.fillMode || 'forwards',
};
this.animations.set(name, animation);
animationCache.set(name, animation);
}
/**
* 注销动画
* @param {string} name - 动画名称
*/
unregister(name) {
this.animations.delete(name);
animationCache.delete(name);
}
/**
* 获取动画配置
* @param {string} name - 动画名称
* @returns {Object|null} 动画配置
*/
get(name) {
return this.animations.get(name) || ANIMATIONS[name] || null;
}
/**
* 应用动画
* @param {HTMLElement} element - 目标元素
* @param {string} animationName - 动画名称
* @param {Object} options - 额外选项
* @returns {Promise} 动画完成Promise
*/
apply(element, animationName, options = {}) {
return new Promise((resolve, reject) => {
const animation = this.get(animationName);
if (!animation) {
resolve();
return;
}
const config = { ...animation, ...options };
const animId = ++this.animationId;
// 设置初始状态
Object.assign(element.style, config.enter);
// 强制重绘
element.offsetHeight;
// 创建动画
const keyframes = [
{ ...config.enter },
{ ...config.leave },
];
const animationOptions = {
duration: config.duration,
easing: config.easing,
delay: config.delay,
iterations: config.iterations,
direction: config.direction,
fill: config.fillMode,
};
// 应用动画
const anim = element.animate(keyframes, animationOptions);
// 存储活动动画
this.activeAnimations.set(animId, {
element,
animation: anim,
config,
});
// 动画完成处理
anim.onfinish = () => {
Object.assign(element.style, config.leave);
this.activeAnimations.delete(animId);
resolve();
};
// 动画取消处理 — reject 让调用者感知取消
anim.oncancel = () => {
this.activeAnimations.delete(animId);
reject(new Error(`Animation "${animationName}" was cancelled`));
};
});
}
/**
* 应用进入动画
* @param {HTMLElement} element - 目标元素
* @param {string} animationName - 动画名称
* @param {Object} options - 额外选项
* @returns {Promise} 动画完成Promise
*/
enter(element, animationName, options = {}) {
const animation = this.get(animationName);
if (!animation) {
return Promise.resolve();
}
return this.apply(element, animationName, {
...options,
direction: 'normal',
});
}
/**
* 应用离开动画
* @param {HTMLElement} element - 目标元素
* @param {string} animationName - 动画名称
* @param {Object} options - 额外选项
* @returns {Promise} 动画完成Promise
*/
leave(element, animationName, options = {}) {
const animation = this.get(animationName);
if (!animation) {
return Promise.resolve();
}
return this.apply(element, animationName, {
...options,
direction: 'reverse',
});
}
/**
* 取消所有动画
*/
cancelAll() {
this.activeAnimations.forEach(({ animation }) => {
animation.cancel();
});
this.activeAnimations.clear();
}
/**
* 取消元素动画
* @param {HTMLElement} element - 目标元素
*/
cancel(element) {
this.activeAnimations.forEach(({ element: el, animation }, id) => {
if (el === element) {
animation.cancel();
this.activeAnimations.delete(id);
}
});
}
/**
* 暂停所有动画
*/
pauseAll() {
this.activeAnimations.forEach(({ animation }) => {
animation.pause();
});
}
/**
* 恢复所有动画
*/
resumeAll() {
this.activeAnimations.forEach(({ animation }) => {
animation.play();
});
}
/**
* 暂停元素动画
* @param {HTMLElement} element - 目标元素
*/
pause(element) {
this.activeAnimations.forEach(({ element: el, animation }) => {
if (el === element) {
animation.pause();
}
});
}
/**
* 恢复元素动画
* @param {HTMLElement} element - 目标元素
*/
resume(element) {
this.activeAnimations.forEach(({ element: el, animation }) => {
if (el === element) {
animation.play();
}
});
}
/**
* 获取活动动画数量
* @returns {number} 动画数量
*/
getActiveCount() {
return this.activeAnimations.size;
}
/**
* 获取所有活动动画
* @returns {Map} 活动动画映射
*/
getActiveAnimations() {
return new Map(this.activeAnimations);
}
/**
* 检查元素是否有活动动画
* @param {HTMLElement} element - 目标元素
* @returns {boolean} 是否有活动动画
*/
hasActiveAnimation(element) {
for (const { element: el } of this.activeAnimations.values()) {
if (el === element) {
return true;
}
}
return false;
}
/**
* 获取动画配置
* @returns {Object} 动画配置
*/
getConfig() {
return {
animations: Array.from(this.animations.keys()),
activeCount: this.getActiveCount(),
};
}
/**
* 重置动画管理器
*/
reset() {
this.cancelAll();
this.animations.clear();
this._registerDefaults();
}
/**
* 销毁动画管理器
*/
destroy() {
this.cancelAll();
this.animations.clear();
this.activeAnimations.clear();
}
}
/**
* 预设动画效果
*/
const presetAnimations = {
bounce: {
enter: { transform: 'translateY(-80px)', opacity: 0 },
leave: { transform: 'translateY(20px)', opacity: 1 },
duration: 650,
easing: 'ease',
},
slideUp: {
enter: { transform: 'translateY(60px)', opacity: 0 },
leave: { transform: 'translateY(0)', opacity: 1 },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
slideDown: {
enter: { transform: 'translateY(-60px)', opacity: 0 },
leave: { transform: 'translateY(0)', opacity: 1 },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
slideLeft: {
enter: { transform: 'translateX(-90px)', opacity: 0 },
leave: { transform: 'translateX(0)', opacity: 1 },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
slideRight: {
enter: { transform: 'translateX(90px)', opacity: 0 },
leave: { transform: 'translateX(0)', opacity: 1 },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
};
// 注册预设动画
Object.entries(presetAnimations).forEach(([name, config]) => {
ANIMATIONS[name] = config;
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, config);
});
/**
* 创建动画管理器实例
* @returns {AnimationManager} 动画管理器实例
*/
export const createAnimationManager = () => {
return new AnimationManager();
};
// 创建默认实例
const defaultAnimationManager = createAnimationManager();
/**
* 动画工具函数
*/
export const animationUtils = {
/**
* 应用动画
* @param {HTMLElement} element - 目标元素
* @param {string} animationName - 动画名称
* @param {Object} options - 额外选项
* @returns {Promise} 动画完成Promise
*/
apply(element, animationName, options = {}) {
return defaultAnimationManager.apply(element, animationName, options);
},
/**
* 应用进入动画
* @param {HTMLElement} element - 目标元素
* @param {string} animationName - 动画名称
* @param {Object} options - 额外选项
* @returns {Promise} 动画完成Promise
*/
enter(element, animationName, options = {}) {
return defaultAnimationManager.enter(element, animationName, options);
},
/**
* 应用离开动画
* @param {HTMLElement} element - 目标元素
* @param {string} animationName - 动画名称
* @param {Object} options - 额外选项
* @returns {Promise} 动画完成Promise
*/
leave(element, animationName, options = {}) {
return defaultAnimationManager.leave(element, animationName, options);
},
/**
* 取消所有动画
*/
cancelAll() {
defaultAnimationManager.cancelAll();
},
/**
* 取消元素动画
* @param {HTMLElement} element - 目标元素
*/
cancel(element) {
defaultAnimationManager.cancel(element);
},
/**
* 暂停所有动画
*/
pauseAll() {
defaultAnimationManager.pauseAll();
},
/**
* 恢复所有动画
*/
resumeAll() {
defaultAnimationManager.resumeAll();
},
/**
* 暂停元素动画
* @param {HTMLElement} element - 目标元素
*/
pause(element) {
defaultAnimationManager.pause(element);
},
/**
* 恢复元素动画
* @param {HTMLElement} element - 目标元素
*/
resume(element) {
defaultAnimationManager.resume(element);
},
/**
* 获取活动动画数量
* @returns {number} 动画数量
*/
getActiveCount() {
return defaultAnimationManager.getActiveCount();
},
/**
* 检查元素是否有活动动画
* @param {HTMLElement} element - 目标元素
* @returns {boolean} 是否有活动动画
*/
hasActiveAnimation(element) {
return defaultAnimationManager.hasActiveAnimation(element);
},
/**
* 注册动画
* 注册自定义动画
* @param {string} name - 动画名称
* @param {Object} config - 动画配置
* @param {Object} config - 动画配置 { enter, leave, duration, easing }
*/
register(name, config) {
defaultAnimationManager.register(name, config);
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
/**
@@ -466,7 +40,7 @@ export const animationUtils = {
* @param {string} name - 动画名称
*/
unregister(name) {
defaultAnimationManager.unregister(name);
animationMap.delete(name);
},
/**
@@ -475,7 +49,7 @@ export const animationUtils = {
* @returns {Object|null} 动画配置
*/
get(name) {
return defaultAnimationManager.get(name);
return animationMap.get(name) || ANIMATIONS[name] || null;
},
/**
@@ -483,374 +57,55 @@ export const animationUtils = {
* @returns {Array} 动画名称数组
*/
getAnimationNames() {
return Array.from(defaultAnimationManager.animations.keys());
return Array.from(animationMap.keys());
},
/**
* 获取动画配置
* @returns {Object} 动画配置
* 获取活动动画数量(兼容API,当前CSS动画不跟踪活动实例)
* @returns {number} 动画数量
*/
getConfig() {
return defaultAnimationManager.getConfig();
getActiveCount() {
return 0;
},
/**
* 取消所有动画(兼容API,当前CSS动画由浏览器管理)
*/
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
/**
* 重置动画管理器
*/
reset() {
defaultAnimationManager.reset();
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, config);
});
},
/**
* 销毁动画管理器
*/
destroy() {
defaultAnimationManager.destroy();
animationMap.clear();
},
};
/**
* 动画预设
*/
export const animationPresets = presetAnimations;
export const animationPresets = {};
/**
* 创建自定义动画
* 创建自定义动画配置
* @param {Object} config - 动画配置
* @returns {Object} 动画配置
*/
export const createAnimation = (config) => {
return {
export const createAnimation = (config) => ({
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
delay: config.delay || 0,
iterations: config.iterations || 1,
direction: config.direction || 'normal',
fillMode: config.fillMode || 'forwards',
};
};
/**
* 组合动画
* @param {Array} animations - 动画数组
* @returns {Object} 组合后的动画配置
*/
export const combineAnimations = (animations) => {
const combined = {
enter: {},
leave: {},
duration: 0,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
delay: 0,
iterations: 1,
direction: 'normal',
fillMode: 'forwards',
};
animations.forEach((anim) => {
if (anim.enter) {
Object.assign(combined.enter, anim.enter);
}
if (anim.leave) {
Object.assign(combined.leave, anim.leave);
}
if (anim.duration) {
combined.duration = Math.max(combined.duration, anim.duration);
}
if (anim.easing) {
combined.easing = anim.easing;
}
if (anim.delay) {
combined.delay = Math.max(combined.delay, anim.delay);
}
});
return combined;
};
/**
* 链式动画
* @param {Array} animations - 动画数组
* @returns {Promise} 动画完成Promise
*/
export const chainAnimations = async (element, animations) => {
for (const anim of animations) {
await defaultAnimationManager.apply(element, anim.name, anim.options);
}
};
/**
* 并行动画
* @param {Array} animations - 动画数组
* @returns {Promise} 动画完成Promise
*/
export const parallelAnimations = (element, animations) => {
return Promise.all(
animations.map((anim) =>
defaultAnimationManager.apply(element, anim.name, anim.options)
)
);
};
/**
* 延迟动画
* @param {number} delay - 延迟时间(毫秒)
* @returns {Promise} Promise对象
*/
export const delayAnimation = (delay) => {
return new Promise((resolve) => setTimeout(resolve, delay));
};
/**
* 动画队列
*/
export class AnimationQueue {
constructor() {
this.queue = [];
this.isProcessing = false;
}
/**
* 添加动画到队列
* @param {Function} animationFn - 动画函数
* @returns {Promise} 动画完成Promise
*/
add(animationFn) {
return new Promise((resolve, reject) => {
this.queue.push({
fn: animationFn,
resolve,
reject,
});
this._process();
});
}
/**
* 处理队列
*/
async _process() {
if (this.isProcessing || this.queue.length === 0) {
return;
}
this.isProcessing = true;
while (this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift();
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
}
this.isProcessing = false;
}
/**
* 清空队列
*/
clear() {
this.queue = [];
}
/**
* 暂停队列
*/
pause() {
this.isProcessing = true;
}
/**
* 恢复队列
*/
resume() {
this.isProcessing = false;
this._process();
}
/**
* 获取队列长度
* @returns {number} 队列长度
*/
get length() {
return this.queue.length;
}
/**
* 检查是否正在处理
* @returns {boolean} 是否正在处理
*/
get processing() {
return this.isProcessing;
}
}
/**
* 创建动画队列
* @returns {AnimationQueue} 动画队列实例
*/
export const createAnimationQueue = () => {
return new AnimationQueue();
};
/**
* 动画性能监控
*/
export class AnimationPerformanceMonitor {
constructor() {
this.metrics = {
totalAnimations: 0,
activeAnimations: 0,
averageDuration: 0,
maxDuration: 0,
minDuration: Infinity,
};
this.history = [];
}
/**
* 记录动画开始
* @param {string} animationName - 动画名称
*/
recordStart(animationName) {
this.metrics.totalAnimations++;
this.metrics.activeAnimations++;
this.history.push({
name: animationName,
startTime: performance.now(),
endTime: null,
duration: null,
});
}
/**
* 记录动画结束
* @param {string} animationName - 动画名称
*/
recordEnd(animationName) {
this.metrics.activeAnimations--;
const entry = this.history.find(
(h) => h.name === animationName && h.endTime === null
);
if (entry) {
entry.endTime = performance.now();
entry.duration = entry.endTime - entry.startTime;
// 更新统计
this.metrics.maxDuration = Math.max(this.metrics.maxDuration, entry.duration);
this.metrics.minDuration = Math.min(this.metrics.minDuration, entry.duration);
// 计算平均值
const completed = this.history.filter((h) => h.duration !== null);
this.metrics.averageDuration =
completed.reduce((sum, h) => sum + h.duration, 0) / completed.length;
}
}
/**
* 获取指标
* @returns {Object} 性能指标
*/
getMetrics() {
return { ...this.metrics };
}
/**
* 获取历史记录
* @returns {Array} 历史记录
*/
getHistory() {
return [...this.history];
}
/**
* 重置监控
*/
reset() {
this.metrics = {
totalAnimations: 0,
activeAnimations: 0,
averageDuration: 0,
maxDuration: 0,
minDuration: Infinity,
};
this.history = [];
}
}
/**
* 创建性能监控实例
* @returns {AnimationPerformanceMonitor} 性能监控实例
*/
export const createPerformanceMonitor = () => {
return new AnimationPerformanceMonitor();
};
/**
* 动画缓存管理
*/
export const animationCacheManager = {
/**
* 缓存动画
* @param {string} key - 缓存键
* @param {Object} animation - 动画配置
*/
set(key, animation) {
animationCache.set(key, animation);
},
/**
* 获取缓存动画
* @param {string} key - 缓存键
* @returns {Object|null} 动画配置
*/
get(key) {
return animationCache.get(key) || null;
},
/**
* 检查缓存
* @param {string} key - 缓存键
* @returns {boolean} 是否存在
*/
has(key) {
return animationCache.has(key);
},
/**
* 删除缓存
* @param {string} key - 缓存键
*/
delete(key) {
animationCache.delete(key);
},
/**
* 清空缓存
*/
clear() {
animationCache.clear();
},
/**
* 获取缓存大小
* @returns {number} 缓存大小
*/
size() {
return animationCache.size;
},
};
export { AnimationManager, defaultAnimationManager };
});
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Constants - 常量定义
* @module constants
* @version 2.0.0
* @version 2.0.1
* @description 默认配置、颜色、动画、主题等常量
*/
+2 -2
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Core - 核心Toast逻辑
* @module core
* @version 2.0.0
* @version 2.0.1
* @description 重构后的核心模块,包含Toast类的优化实现
*/
@@ -621,7 +621,7 @@ const _actionHTML = (actions) => {
const meToast = {
_toasts: new Map(),
_config: { ...DEFAULTS },
version: '2.0.0',
version: '2.0.1',
configure(opts) {
if (!opts || typeof opts !== 'object') return this;
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast i18n - 国际化管理
* @module i18n
* @version 2.0.0
* @version 2.0.1
* @description 多语言支持、语言切换和翻译管理
*/
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Icons — 图标SVG定义
* @module icons
* @version 2.0.0
* @version 2.0.1
* @description 80+ 内置SVG图标
*/
+2 -2
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast - 轻量级Toast通知库
* @module metona-toast
* @version 2.0.0
* @version 2.0.1
* @author thzxx
* @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。
* @license MIT
@@ -15,7 +15,7 @@ import { pluginUtils, presetPlugins } from './plugins.js';
import { DEFAULTS } from './constants.js';
// 版本信息
const VERSION = '2.0.0';
const VERSION = '2.0.1';
/**
* 主对象增强
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Locales — 国际化翻译数据
* @module locales
* @version 2.0.0
* @version 2.0.1
* @description 内置 zh-CN / en-US 完整翻译
*/
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Plugins - 插件系统
* @module plugins
* @version 2.0.0
* @version 2.0.1
* @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility)
*/
+148 -1376
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Themes - 主题管理
* @module themes
* @version 2.0.0
* @version 2.0.1
* @description 主题系统、自定义主题和主题切换
*/
+3 -1021
View File
File diff suppressed because it is too large Load Diff
+4 -613
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast 单元测试
* @module tests
* @version 2.0.0
* @version 2.0.1
*/
import MeToast, { Toast, VERSION } from '../src/index.js';
@@ -121,8 +121,8 @@ describe('MetonaToast', () => {
describe('版本信息', () => {
test('应该有正确的版本号', () => {
expect(VERSION).toBe('2.0.0');
expect(MeToast.version).toBe('2.0.0');
expect(VERSION).toBe('2.0.1');
expect(MeToast.version).toBe('2.0.1');
});
});
@@ -583,7 +583,7 @@ describe('MetonaToast', () => {
MeToast.success('消息');
const status = MeToast.getStatus();
expect(status.version).toBe('2.0.0');
expect(status.version).toBe('2.0.1');
expect(status.toasts).toBeGreaterThanOrEqual(0);
expect(status.theme).toBeDefined();
expect(status.locale).toBeDefined();
@@ -776,542 +776,6 @@ describe('工具函数', () => {
const result = prefersDark();
expect(typeof result).toBe('boolean');
});
test('应该能够防抖函数', (done) => {
const { debounce } = require('../src/utils.js');
let count = 0;
const fn = jest.fn(() => count++);
const debouncedFn = debounce(fn, 100);
debouncedFn();
debouncedFn();
debouncedFn();
expect(fn).not.toHaveBeenCalled();
setTimeout(() => {
expect(fn).toHaveBeenCalledTimes(1);
done();
}, 150);
});
test('应该能够节流函数', (done) => {
const { throttle } = require('../src/utils.js');
let count = 0;
const fn = jest.fn(() => count++);
const throttledFn = throttle(fn, 100);
throttledFn();
throttledFn();
throttledFn();
expect(fn).toHaveBeenCalledTimes(1);
setTimeout(() => {
throttledFn();
expect(fn).toHaveBeenCalledTimes(2);
done();
}, 150);
});
test('应该能够深度合并对象', () => {
const { deepMerge } = require('../src/utils.js');
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { b: { d: 3 }, e: 4 };
const merged = deepMerge(obj1, obj2);
expect(merged.a).toBe(1);
expect(merged.b.c).toBe(2);
expect(merged.b.d).toBe(3);
expect(merged.e).toBe(4);
});
test('应该能够检查是否为浏览器环境', () => {
const { isBrowser } = require('../src/utils.js');
const result = isBrowser();
expect(typeof result).toBe('boolean');
});
test('应该能够安全获取嵌套对象属性', () => {
const { getNestedValue } = require('../src/utils.js');
const obj = { a: { b: { c: 1 } } };
expect(getNestedValue(obj, 'a.b.c')).toBe(1);
expect(getNestedValue(obj, 'a.b.d')).toBeUndefined();
expect(getNestedValue(obj, 'a.b.d', 'default')).toBe('default');
});
test('应该能够格式化文件大小', () => {
const { formatFileSize } = require('../src/utils.js');
expect(formatFileSize(0)).toBe('0 Bytes');
expect(formatFileSize(1024)).toBe('1 KB');
expect(formatFileSize(1048576)).toBe('1 MB');
expect(formatFileSize(1073741824)).toBe('1 GB');
});
test('应该能够格式化日期', () => {
const { formatDate } = require('../src/utils.js');
const date = new Date('2024-01-01 12:00:00');
const formatted = formatDate(date, 'YYYY-MM-DD HH:mm:ss');
expect(formatted).toBe('2024-01-01 12:00:00');
});
test('应该能够生成随机数', () => {
const { random } = require('../src/utils.js');
const result = random(1, 100);
expect(result).toBeGreaterThanOrEqual(1);
expect(result).toBeLessThanOrEqual(100);
});
test('应该能够检查元素是否在视口中', () => {
const { isInViewport } = require('../src/utils.js');
const el = {
getBoundingClientRect: () => ({
top: 0,
left: 0,
bottom: 100,
right: 100,
}),
};
expect(isInViewport(el)).toBe(true);
});
test('应该能够复制文本到剪贴板', async () => {
const { copyToClipboard } = require('../src/utils.js');
// Mock clipboard.writeText 返回 resolved promise
if (!navigator.clipboard) navigator.clipboard = {};
const origWriteText = navigator.clipboard.writeText;
navigator.clipboard.writeText = jest.fn(() => Promise.resolve());
const result = await copyToClipboard('test');
expect(result).toBe(true);
navigator.clipboard.writeText = origWriteText;
});
test('应该能够检测设备类型', () => {
const { getDeviceType } = require('../src/utils.js');
const result = getDeviceType();
expect(['desktop', 'mobile', 'tablet']).toContain(result);
});
test('应该能够检测浏览器信息', () => {
const { getBrowserInfo } = require('../src/utils.js');
const result = getBrowserInfo();
expect(result).toBeDefined();
expect(result.browser).toBeDefined();
expect(result.version).toBeDefined();
});
test('应该能够检测操作系统', () => {
const { getOS } = require('../src/utils.js');
const result = getOS();
expect(result).toBeDefined();
});
test('应该能够检测网络状态', () => {
const { getNetworkInfo } = require('../src/utils.js');
const result = getNetworkInfo();
expect(result).toBeDefined();
expect(result.online).toBeDefined();
});
test('应该能够存储和获取数据', () => {
const { setStorage, getStorage, removeStorage } = require('../src/utils.js');
setStorage('test', 'value');
expect(getStorage('test')).toBe('value');
removeStorage('test');
expect(getStorage('test')).toBeNull();
});
test('应该能够生成UUID', () => {
const { generateUUID } = require('../src/utils.js');
const uuid = generateUUID();
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
});
test('应该能够检查CSS属性支持', () => {
const { supportsCSSProperty } = require('../src/utils.js');
const result = supportsCSSProperty('transform');
expect(typeof result).toBe('boolean');
});
test('应该能够检查JavaScript API支持', () => {
const { supportsAPI } = require('../src/utils.js');
const result = supportsAPI('fetch');
expect(typeof result).toBe('boolean');
});
test('应该能够获取URL参数', () => {
const { getURLParam } = require('../src/utils.js');
// Mock URLSearchParams 来绕过 jsdom location.search 限制
const origSearchParams = global.URLSearchParams;
global.URLSearchParams = jest.fn(() => ({
get: jest.fn((name) => name === 'test' ? 'value' : null),
}));
const result = getURLParam('test');
expect(result).toBe('value');
global.URLSearchParams = origSearchParams;
});
test('应该能够格式化数字', () => {
const { formatNumber } = require('../src/utils.js');
const result = formatNumber(1234567.89, 2);
expect(result).toBeDefined();
});
test('应该能够格式化货币', () => {
const { formatCurrency } = require('../src/utils.js');
const result = formatCurrency(99.99, 'USD');
expect(result).toBeDefined();
});
test('应该能够格式化百分比', () => {
const { formatPercent } = require('../src/utils.js');
const result = formatPercent(75.5, 2);
expect(result).toBeDefined();
});
test('应该能够检查是否为空值', () => {
const { isEmpty } = require('../src/utils.js');
expect(isEmpty(null)).toBe(true);
expect(isEmpty(undefined)).toBe(true);
expect(isEmpty('')).toBe(true);
expect(isEmpty(' ')).toBe(true);
expect(isEmpty([])).toBe(true);
expect(isEmpty({})).toBe(true);
expect(isEmpty('text')).toBe(false);
expect(isEmpty([1])).toBe(false);
expect(isEmpty({ a: 1 })).toBe(false);
});
test('应该能够深拷贝对象', () => {
const { deepClone } = require('../src/utils.js');
const obj = { a: 1, b: { c: 2 }, d: [1, 2, 3] };
const cloned = deepClone(obj);
expect(cloned).toEqual(obj);
expect(cloned).not.toBe(obj);
expect(cloned.b).not.toBe(obj.b);
expect(cloned.d).not.toBe(obj.d);
});
test('应该能够比较对象是否相等', () => {
const { isEqual } = require('../src/utils.js');
expect(isEqual({ a: 1 }, { a: 1 })).toBe(true);
expect(isEqual({ a: 1 }, { a: 2 })).toBe(false);
expect(isEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false);
expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true);
expect(isEqual([1, 2, 3], [1, 2])).toBe(false);
});
test('应该能够验证邮箱格式', () => {
const { isValidEmail } = require('../src/utils.js');
expect(isValidEmail('test@example.com')).toBe(true);
expect(isValidEmail('invalid-email')).toBe(false);
expect(isValidEmail('test@')).toBe(false);
expect(isValidEmail('@example.com')).toBe(false);
});
test('应该能够验证URL格式', () => {
const { isValidURL } = require('../src/utils.js');
expect(isValidURL('https://example.com')).toBe(true);
expect(isValidURL('http://example.com')).toBe(true);
expect(isValidURL('invalid-url')).toBe(false);
});
test('应该能够验证手机号格式', () => {
const { isValidPhone } = require('../src/utils.js');
expect(isValidPhone('13800138000')).toBe(true);
expect(isValidPhone('12345678901')).toBe(false);
expect(isValidPhone('1380013800')).toBe(false);
});
test('应该能够验证身份证号格式', () => {
const { isValidIDCard } = require('../src/utils.js');
expect(isValidIDCard('110101199003077777')).toBe(true);
expect(isValidIDCard('123456789012345678')).toBe(false);
});
test('应该能够生成随机颜色', () => {
const { randomColor } = require('../src/utils.js');
const color = randomColor();
expect(color).toMatch(/^#[0-9a-f]{6}$/);
});
test('应该能够颜色转RGBA', () => {
const { colorToRGBA } = require('../src/utils.js');
const rgba = colorToRGBA('#ff0000', 0.5);
expect(rgba).toBe('rgba(255, 0, 0, 0.5)');
});
test('应该能够获取颜色亮度', () => {
const { getColorBrightness } = require('../src/utils.js');
const brightness = getColorBrightness('#ffffff');
expect(brightness).toBe(255);
});
test('应该能够判断是否为浅色', () => {
const { isLightColor } = require('../src/utils.js');
expect(isLightColor('#ffffff')).toBe(true);
expect(isLightColor('#000000')).toBe(false);
});
test('应该能够获取对比色', () => {
const { getContrastColor } = require('../src/utils.js');
expect(getContrastColor('#ffffff')).toBe('#000000');
expect(getContrastColor('#000000')).toBe('#FFFFFF');
});
test('应该能够生成渐变色', () => {
const { generateGradient } = require('../src/utils.js');
const gradient = generateGradient('#ff0000', '#0000ff', 5);
expect(gradient).toHaveLength(5);
expect(gradient[0]).toBe('#ff0000');
expect(gradient[4]).toBe('#0000ff');
});
test('应该能够获取字符长度', () => {
const { getStringLength } = require('../src/utils.js');
expect(getStringLength('abc')).toBe(3);
expect(getStringLength('中文')).toBe(4);
expect(getStringLength('abc中文')).toBe(7);
});
test('应该能够截取字符串', () => {
const { truncateString } = require('../src/utils.js');
expect(truncateString('abcdefghij', 5)).toBe('abcde...');
expect(truncateString('中文字符串测试', 8)).toBe('中文字符...');
expect(truncateString('abc', 5)).toBe('abc');
});
test('应该能够驼峰转换', () => {
const { toCamelCase } = require('../src/utils.js');
expect(toCamelCase('hello-world')).toBe('helloWorld');
expect(toCamelCase('hello_world')).toBe('helloWorld');
expect(toCamelCase('hello world')).toBe('helloWorld');
});
test('应该能够短横线转换', () => {
const { toKebabCase } = require('../src/utils.js');
expect(toKebabCase('helloWorld')).toBe('hello-world');
expect(toKebabCase('hello_world')).toBe('hello-world');
expect(toKebabCase('hello world')).toBe('hello-world');
});
test('应该能够下划线转换', () => {
const { toSnakeCase } = require('../src/utils.js');
expect(toSnakeCase('helloWorld')).toBe('hello_world');
expect(toSnakeCase('hello-world')).toBe('hello_world');
expect(toSnakeCase('hello world')).toBe('hello_world');
});
test('应该能够首字母大写', () => {
const { capitalize } = require('../src/utils.js');
expect(capitalize('hello')).toBe('Hello');
expect(capitalize('HELLO')).toBe('HELLO');
});
test('应该能够每个单词首字母大写', () => {
const { capitalizeWords } = require('../src/utils.js');
expect(capitalizeWords('hello world')).toBe('Hello World');
});
test('应该能够移除HTML标签', () => {
const { stripHTML } = require('../src/utils.js');
expect(stripHTML('<b>bold</b>')).toBe('bold');
expect(stripHTML('<p>paragraph</p>')).toBe('paragraph');
});
test('应该能够转义正则表达式', () => {
const { escapeRegExp } = require('../src/utils.js');
expect(escapeRegExp('[test]')).toBe('\\[test\\]');
expect(escapeRegExp('hello.world')).toBe('hello\\.world');
});
test('应该能够生成随机字符串', () => {
const { randomString } = require('../src/utils.js');
const str = randomString(10);
expect(str).toHaveLength(10);
});
test('应该能够延迟执行', async () => {
const { sleep } = require('../src/utils.js');
const start = Date.now();
await sleep(100);
const end = Date.now();
expect(end - start).toBeGreaterThanOrEqual(90);
});
test('应该能够重试函数', async () => {
const { retry } = require('../src/utils.js');
let attempts = 0;
const fn = jest.fn(() => {
attempts++;
if (attempts < 3) {
throw new Error('fail');
}
return 'success';
});
const result = await retry(fn, 3, 10);
expect(result).toBe('success');
expect(fn).toHaveBeenCalledTimes(3);
});
test('应该能够超时控制', async () => {
const { timeout } = require('../src/utils.js');
const promise = new Promise((resolve) => setTimeout(resolve, 200));
await expect(timeout(promise, 100)).rejects.toThrow('Timeout');
});
test('应该能够并发控制', async () => {
const { parallel } = require('../src/utils.js');
const tasks = [
() => Promise.resolve(1),
() => Promise.resolve(2),
() => Promise.resolve(3),
];
const results = await parallel(tasks, 2);
expect(results).toEqual([1, 2, 3]);
});
test('应该能够序列执行', async () => {
const { sequence } = require('../src/utils.js');
const tasks = [
() => Promise.resolve(1),
() => Promise.resolve(2),
() => Promise.resolve(3),
];
const results = await sequence(tasks);
expect(results).toEqual([1, 2, 3]);
});
test('应该能够缓存函数', () => {
const { memoize } = require('../src/utils.js');
let count = 0;
const fn = jest.fn((x) => {
count++;
return x * 2;
});
const memoized = memoize(fn);
expect(memoized(2)).toBe(4);
expect(memoized(2)).toBe(4);
expect(fn).toHaveBeenCalledTimes(1);
});
test('应该能够单例模式', () => {
const { singleton } = require('../src/utils.js');
let count = 0;
const fn = jest.fn(() => {
count++;
return { id: count };
});
const singletonFn = singleton(fn);
const obj1 = singletonFn();
const obj2 = singletonFn();
expect(obj1).toBe(obj2);
expect(fn).toHaveBeenCalledTimes(1);
});
test('应该能够创建观察者', () => {
const { createObserver } = require('../src/utils.js');
const observer = createObserver();
const callback = jest.fn();
observer.on('test', callback);
observer.emit('test', 'data');
expect(callback).toHaveBeenCalledWith('data');
});
test('应该能够创建状态机', () => {
const { createStateMachine } = require('../src/utils.js');
const machine = createStateMachine({
initial: 'idle',
transitions: {
idle: { start: 'running' },
running: { stop: 'idle' },
},
});
expect(machine.state).toBe('idle');
machine.transition('start');
expect(machine.state).toBe('running');
machine.transition('stop');
expect(machine.state).toBe('idle');
});
});
describe('常量', () => {
@@ -1395,79 +859,6 @@ describe('常量', () => {
});
});
describe('动画管理器', () => {
test('应该能够创建动画管理器', () => {
const { createAnimationManager } = require('../src/animations.js');
const manager = createAnimationManager();
expect(manager).toBeDefined();
expect(manager.register).toBeInstanceOf(Function);
expect(manager.apply).toBeInstanceOf(Function);
});
test('应该能够注册动画', () => {
const { createAnimationManager } = require('../src/animations.js');
const manager = createAnimationManager();
manager.register('test', {
enter: { opacity: 0 },
leave: { opacity: 1 },
duration: 300,
});
expect(manager.get('test')).toBeDefined();
});
test('应该能够应用动画', async () => {
const { createAnimationManager } = require('../src/animations.js');
const manager = createAnimationManager();
const element = document.createElement('div');
// 确保 animate 方法存在并在设置 onfinish 后自动调用
element.animate = jest.fn(() => {
const anim = {
onfinish: null,
oncancel: null,
cancel: jest.fn(),
pause: jest.fn(),
play: jest.fn(),
};
// 下一个微任务自动触发 onfinish,让 Promise resolve
Promise.resolve().then(() => {
if (anim.onfinish) anim.onfinish();
});
return anim;
});
manager.register('test', {
enter: { opacity: 0 },
leave: { opacity: 1 },
duration: 100,
});
await manager.apply(element, 'test');
expect(element.animate).toHaveBeenCalled();
});
test('应该能够取消动画', () => {
const { createAnimationManager } = require('../src/animations.js');
const manager = createAnimationManager();
manager.cancelAll();
expect(manager.getActiveCount()).toBe(0);
});
test('应该能够获取活动动画数量', () => {
const { createAnimationManager } = require('../src/animations.js');
const manager = createAnimationManager();
expect(manager.getActiveCount()).toBe(0);
});
});
describe('主题管理器', () => {
test('应该能够获取系统主题', () => {
const { getSystemTheme } = require('../src/themes.js');
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* MetonaToast TypeScript 类型定义
* @version 2.0.0
* @version 2.0.1
*/
// 基础类型