diff --git a/package.json b/package.json index 340c5f1..ce16e5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metona-team/metona-toast", - "version": "0.1.1", + "version": "0.1.2", "description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。", "main": "dist/metona-toast.js", "module": "src/index.js", diff --git a/site/demo.html b/site/demo.html index 84e5203..b3094e4 100644 --- a/site/demo.html +++ b/site/demo.html @@ -273,7 +273,7 @@ diff --git a/site/docs.html b/site/docs.html index 75e3154..de8dab7 100644 --- a/site/docs.html +++ b/site/docs.html @@ -87,7 +87,7 @@

API 文档

-

MetonaToast v0.1.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 配置项 作为可选第二参数。

+

MetonaToast v0.1.2 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 配置项 作为可选第二参数。

show(message, opts?)

@@ -434,6 +434,7 @@ MeToast.use('accessibility'); resetTimerOnUpdatebooleanfalse调用 update() 时重置 duration 倒计时 notifyWhenHiddenbooleanfalse页面不可见时自动通过 Notification API 发送系统通知 renderfunction—自定义渲染函数 (toast) => htmlString,完全接管 DOM 构建 + onErrorfunction—全局错误回调 ({ hook, source, error, toast }) => void,钩子异常或定时器错误时触发

回调函数

diff --git a/site/index.html b/site/index.html index 78abea9..9dc3aa9 100644 --- a/site/index.html +++ b/site/index.html @@ -237,7 +237,7 @@ orderGroup.dismiss(); // 一键关闭
-

MetonaToast v0.1.1 · MIT License · Gitea

+

MetonaToast v0.1.2 · MIT License · Gitea

diff --git a/src/animations.js b/src/animations.js index 8fb101a..f70242d 100644 --- a/src/animations.js +++ b/src/animations.js @@ -1,5 +1,5 @@ /** - * MetonaToast Animations - 动画管理 v0.1.1 + * MetonaToast Animations - 动画管理 v0.1.2 * @module animations * @description 动画注册与管理,移除未使用的 Web Animations API dead code */ @@ -61,11 +61,11 @@ export const animationUtils = { }, /** - * 获取活动动画数量(兼容API,当前CSS动画不跟踪活动实例) - * @returns {number} 动画数量 + * 获取已注册动画总数 + * @returns {number} 已注册动画数量 */ getActiveCount() { - return 0; + return animationMap.size; }, /** diff --git a/src/constants.js b/src/constants.js index 3e13a20..57bf979 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,7 +1,7 @@ /** * MetonaToast Constants - 常量定义 * @module constants - * @version 0.1.1 + * @version 0.1.2 * @description 默认配置、颜色、动画、主题等常量 */ @@ -48,7 +48,10 @@ export const DEFAULTS = Object.freeze({ // 高级 resetTimerOnUpdate: false, notifyWhenHidden: false, - + + // 错误处理 + onError: null, + // 国际化 locale: 'zh-CN', @@ -267,7 +270,15 @@ export const THEMES = { progressBg: 'rgba(255, 255, 255, 0.08)', closeHoverBg: 'rgba(255, 255, 255, 0.08)', }, - auto: 'auto', + auto: { + bg: 'auto', + text: 'auto', + border: 'auto', + shadow: 'auto', + hoverShadow: 'auto', + progressBg: 'auto', + closeHoverBg: 'auto', + }, warm: { bg: 'rgba(255, 251, 235, 0.96)', text: '#78350f', diff --git a/src/core.js b/src/core.js index 4155d15..6a645cd 100644 --- a/src/core.js +++ b/src/core.js @@ -1,7 +1,7 @@ /** * MetonaToast Core - 核心Toast逻辑 * @module core - * @version 0.1.1 + * @version 0.1.2 * @description 重构后的核心模块,包含Toast类的优化实现 */ @@ -68,7 +68,15 @@ class Toast { } static trigger(name, toast) { const list = this._hooks.get(name); - if (list) list.forEach(fn => { try { fn(toast); } catch (e) { console.error('Hook error:', name, e); } }); + if (list) list.forEach(fn => { + try { fn(toast); } + catch (e) { + console.error('Hook error:', name, e); + if (meToast._config.onError) { + try { meToast._config.onError({ hook: name, error: e, toast }); } catch (_) {} + } + } + }); } constructor(opts) { @@ -392,33 +400,43 @@ class Toast { }); } - _startTimer() { + _startTimer(resuming = false) { if (this.config.duration <= 0) return; - - this.startedAt = Date.now(); - this.remaining = this.config.duration; - + + // 仅在首次启动时重置时间基准;resume 场景由 _resume() 预先调整 startedAt + if (!resuming) { + this.startedAt = Date.now(); + this.remaining = this.config.duration; + } + const tick = () => { if (this.paused || this.closing) return; - - const elapsed = Date.now() - this.startedAt; - this.remaining = Math.max(0, this.config.duration - elapsed); - - if (this.barEl) { - const ratio = this.remaining / this.config.duration; - const t = this.config.progressDirection === 'vertical' - ? `scaleY(${ratio})` : `scaleX(${ratio})`; - this.barEl.style.transform = t; + + try { + const elapsed = Date.now() - this.startedAt; + this.remaining = Math.max(0, this.config.duration - elapsed); + + if (this.barEl) { + const ratio = this.remaining / this.config.duration; + const t = this.config.progressDirection === 'vertical' + ? `scaleY(${ratio})` : `scaleX(${ratio})`; + this.barEl.style.transform = t; + } + + if (this.remaining <= 0) { + this.close(); + return; + } + } catch (e) { + console.error('Timer tick error:', e); + if (meToast._config.onError) { + try { meToast._config.onError({ source: 'timer', error: e, toast: this }); } catch (_) {} + } } - - if (this.remaining <= 0) { - this.close(); - return; - } - + this.rafId = requestAnimationFrame(tick); }; - + this.rafId = requestAnimationFrame(tick); } @@ -432,19 +450,37 @@ class Toast { _resume() { if (!this.paused) return; this.paused = false; + // 校准时间基准,补偿暂停期间经过的时间 this.startedAt = Date.now() - (this.config.duration - this.remaining); - this._startTimer(); + this._startTimer(true); } update(partial) { Toast.trigger('beforeUpdate', this); + const typeChanged = partial.type && partial.type !== this.type; if (partial.type) this.type = partial.type; if (partial.title !== undefined) this.title = partial.title; if (partial.message !== undefined) this.message = partial.message; if (partial.html !== undefined) this.html = partial.html; - + if (!this.el) { Toast.trigger('afterUpdate', this); return this; } - + + // 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色 + if (typeChanged) { + const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default']; + typeClasses.forEach(c => this.el.classList.remove(c)); + this.el.classList.add(`met-${this.type}`); + if (this.barEl) { + const c = (TYPE_COLORS[this.type] || TYPE_COLORS.default); + this.barEl.style.background = c.fg; + } + const side = this.el.querySelector('.met-side'); + if (side) { + const c = (TYPE_COLORS[this.type] || TYPE_COLORS.default); + side.style.background = c.fg; + } + } + const content = this.el.querySelector('.met-content'); if (content) { const safeTitle = this.title ? `
${escapeHTML(this.title)}
` : ''; @@ -453,7 +489,7 @@ class Toast { : (this.message ? `
${escapeHTML(this.message)}
` : ''); content.innerHTML = `${safeTitle}${safeMessage}`; } - + // resetTimerOnUpdate: 更新内容后重置计时器 if (this.config.resetTimerOnUpdate && this.config.duration > 0) { cancelAnimationFrame(this.rafId); @@ -461,11 +497,11 @@ class Toast { this.remaining = this.config.duration; this._startTimer(); } - + if (typeof this.config.onUpdate === 'function') { try { this.config.onUpdate(this); } catch (e) { console.error('onUpdate callback error:', e); } } - + Toast.trigger('afterUpdate', this); return this; } @@ -621,11 +657,12 @@ const _actionHTML = (actions) => { const meToast = { _toasts: new Map(), _config: { ...DEFAULTS }, - version: '0.1.1', + version: '0.1.2', configure(opts) { if (!opts || typeof opts !== 'object') return this; - this._config = { ...this._config, ...opts }; + // 原地更新,保持 _config 引用一致(index.js 的增强对象共享同一个 _config) + Object.assign(this._config, opts); if (opts.theme) { applyTheme(opts.theme); diff --git a/src/i18n.js b/src/i18n.js index fcd408c..fe7f8d9 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,7 +1,7 @@ /** * MetonaToast i18n - 国际化管理 * @module i18n - * @version 0.1.1 + * @version 0.1.2 * @description 多语言支持、语言切换和翻译管理 */ diff --git a/src/icons.js b/src/icons.js index 70be2c1..e239388 100644 --- a/src/icons.js +++ b/src/icons.js @@ -1,7 +1,7 @@ /** * MetonaToast Icons — 图标SVG定义 * @module icons - * @version 0.1.1 + * @version 0.1.2 * @description 80+ 内置SVG图标 */ diff --git a/src/index.js b/src/index.js index e645c88..3b6a879 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ /** * MetonaToast - 轻量级Toast通知库 * @module metona-toast - * @version 0.1.1 + * @version 0.1.2 * @author thzxx * @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。 * @license MIT @@ -15,7 +15,7 @@ import { pluginUtils, presetPlugins } from './plugins.js'; import { DEFAULTS } from './constants.js'; // 版本信息 -const VERSION = '0.1.1'; +const VERSION = '0.1.2'; /** * 主对象增强 @@ -89,6 +89,8 @@ const enhancedMeToast = { * 销毁 */ destroy() { + if (this._destroyed) return; + this._destroyed = true; this.dismiss(); if (this.plugins && typeof this.plugins.destroy === 'function') { @@ -149,16 +151,19 @@ const enhancedMeToast = { */ updateConfig(config) { if (config && typeof config === 'object') { - this._config = { ...this._config, ...config }; + Object.assign(this._config, config); } return this; }, - + /** * 重置配置 */ resetConfig() { - this._config = { ...DEFAULTS }; + // 原地重置:先清除所有自有属性,再回填默认值 + const keys = Object.keys(this._config); + keys.forEach(k => delete this._config[k]); + Object.assign(this._config, DEFAULTS); return this; }, diff --git a/src/locales.js b/src/locales.js index 2cd98cf..38bc1f9 100644 --- a/src/locales.js +++ b/src/locales.js @@ -1,12 +1,13 @@ /** * MetonaToast Locales — 国际化翻译数据 * @module locales - * @version 0.1.1 - * @description 内置 zh-CN / en-US 完整翻译 + * @version 0.1.2 + * @description 内置 zh-CN / en-US 完整翻译(已去重优化) */ export const LOCALES = { 'zh-CN': { + // === 操作 === close: '关闭', loading: '加载中...', success: '操作成功', @@ -87,6 +88,8 @@ export const LOCALES = { hide: '隐藏', visible: '可见', hidden: '隐藏', + + // === 状态 === enabled: '已启用', disabled: '已禁用', active: '活跃', @@ -95,7 +98,6 @@ export const LOCALES = { offline: '离线', connected: '已连接', disconnected: '已断开', - loading: '加载中', loaded: '已加载', saving: '保存中', saved: '已保存', @@ -114,12 +116,9 @@ export const LOCALES = { receiving: '接收中', received: '已接收', connecting: '连接中', - error: '错误', - warning: '警告', - info: '信息', - success: '成功', + + // === 交互 === question: '问题', - help: '帮助', feedback: '反馈', report: '报告', bug: '错误', @@ -133,7 +132,6 @@ export const LOCALES = { complete: '完成', incomplete: '未完成', pending: '待处理', - processing: '处理中', failed: '失败', cancelled: '已取消', timeout: '超时', @@ -142,8 +140,6 @@ export const LOCALES = { valid: '有效', required: '必填', optional: '可选', - enabled: '启用', - disabled: '禁用', allowed: '允许', denied: '拒绝', approved: '已批准', @@ -158,6 +154,8 @@ export const LOCALES = { unauthenticated: '未认证', authorized: '已授权', unauthorized: '未授权', + + // === 范围 / 角色 === public: '公开', private: '私有', protected: '受保护', @@ -181,6 +179,8 @@ export const LOCALES = { following: '关注中', friend: '朋友', contact: '联系人', + + // === 组织 === group: '群组', team: '团队', organization: '组织', @@ -190,8 +190,6 @@ export const LOCALES = { task: '任务', issue: '问题', ticket: '工单', - request: '请求', - response: '响应', message: '消息', notification: '通知', alert: '警报', @@ -199,7 +197,6 @@ export const LOCALES = { event: '事件', activity: '活动', log: '日志', - report: '报告', analytics: '分析', statistics: '统计', metrics: '指标', @@ -251,134 +248,10 @@ export const LOCALES = { which: '哪个', whose: '谁的', whom: '谁', - myself: '我自己', - yourself: '你自己', - himself: '他自己', - herself: '她自己', - itself: '它自己', - ourselves: '我们自己', - yourselves: '你们自己', - themselves: '他们自己', - mine: '我的', - yours: '你的', - his: '他的', - hers: '她的', - its: '它的', - ours: '我们的', - yours: '你们的', - theirs: '他们的', - me: '我', - you: '你', - him: '他', - her: '她', - it: '它', - us: '我们', - them: '他们', - my: '我的', - your: '你的', - his: '他的', - her: '她的', - its: '它的', - our: '我们的', - your: '你们的', - their: '他们的', - i: '我', - you: '你', - he: '他', - she: '她', - it: '它', - we: '我们', - they: '他们', - am: '是', - is: '是', - are: '是', - was: '是', - were: '是', - be: '是', - been: '是', - being: '是', - have: '有', - has: '有', - had: '有', - having: '有', - do: '做', - does: '做', - did: '做', - doing: '做', - will: '会', - would: '会', - shall: '将', - should: '应该', - may: '可以', - might: '可能', - can: '能', - could: '能', - must: '必须', - need: '需要', - dare: '敢', - ought: '应该', - used: '过去常常', - to: '到', - of: '的', - in: '在', - for: '为了', - on: '在', - with: '和', - at: '在', - by: '通过', - from: '从', - into: '进入', - during: '在...期间', - before: '在...之前', - after: '在...之后', - above: '在...上面', - below: '在...下面', - between: '在...之间', - under: '在...下面', - over: '在...上面', - across: '穿过', - through: '通过', - into: '进入', - towards: '朝向', - upon: '在...上面', - about: '关于', - against: '反对', - among: '在...之中', - along: '沿着', - around: '周围', - beyond: '超出', - but: '但是', - despite: '尽管', - except: '除了', - inside: '里面', - outside: '外面', - since: '自从', - until: '直到', - unless: '除非', - whether: '是否', - while: '当...时候', - although: '虽然', - because: '因为', - if: '如果', - once: '一旦', - since: '自从', - so: '所以', - that: '那个', - though: '虽然', - till: '直到', - unless: '除非', - until: '直到', - when: '当...时候', - whenever: '每当', - where: '哪里', - wherever: '无论哪里', - whereas: '然而', - wherever: '无论哪里', - while: '当...时候', - why: '为什么', }, - + 'en-US': { + // === Actions === close: 'Close', loading: 'Loading...', success: 'Success', @@ -459,6 +332,8 @@ export const LOCALES = { hide: 'Hide', visible: 'Visible', hidden: 'Hidden', + + // === Status === enabled: 'Enabled', disabled: 'Disabled', active: 'Active', @@ -467,7 +342,6 @@ export const LOCALES = { offline: 'Offline', connected: 'Connected', disconnected: 'Disconnected', - loading: 'Loading', loaded: 'Loaded', saving: 'Saving', saved: 'Saved', @@ -486,12 +360,9 @@ export const LOCALES = { receiving: 'Receiving', received: 'Received', connecting: 'Connecting', - error: 'Error', - warning: 'Warning', - info: 'Info', - success: 'Success', + + // === Interaction === question: 'Question', - help: 'Help', feedback: 'Feedback', report: 'Report', bug: 'Bug', @@ -505,7 +376,6 @@ export const LOCALES = { complete: 'Complete', incomplete: 'Incomplete', pending: 'Pending', - processing: 'Processing', failed: 'Failed', cancelled: 'Cancelled', timeout: 'Timeout', @@ -514,8 +384,6 @@ export const LOCALES = { valid: 'Valid', required: 'Required', optional: 'Optional', - enabled: 'Enabled', - disabled: 'Disabled', allowed: 'Allowed', denied: 'Denied', approved: 'Approved', @@ -530,6 +398,8 @@ export const LOCALES = { unauthenticated: 'Unauthenticated', authorized: 'Authorized', unauthorized: 'Unauthorized', + + // === Scope / Role === public: 'Public', private: 'Private', protected: 'Protected', @@ -553,6 +423,8 @@ export const LOCALES = { following: 'Following', friend: 'Friend', contact: 'Contact', + + // === Organization === group: 'Group', team: 'Team', organization: 'Organization', @@ -562,8 +434,6 @@ export const LOCALES = { task: 'Task', issue: 'Issue', ticket: 'Ticket', - request: 'Request', - response: 'Response', message: 'Message', notification: 'Notification', alert: 'Alert', @@ -571,7 +441,6 @@ export const LOCALES = { event: 'Event', activity: 'Activity', log: 'Log', - report: 'Report', analytics: 'Analytics', statistics: 'Statistics', metrics: 'Metrics', @@ -623,130 +492,5 @@ export const LOCALES = { which: 'Which', whose: 'Whose', whom: 'Whom', - myself: 'Myself', - yourself: 'Yourself', - himself: 'Himself', - herself: 'Herself', - itself: 'Itself', - ourselves: 'Ourselves', - yourselves: 'Yourselves', - themselves: 'Themselves', - mine: 'Mine', - yours: 'Yours', - his: 'His', - hers: 'Hers', - its: 'Its', - ours: 'Ours', - yours: 'Yours', - theirs: 'Theirs', - me: 'Me', - you: 'You', - him: 'Him', - her: 'Her', - it: 'It', - us: 'Us', - them: 'Them', - my: 'My', - your: 'Your', - his: 'His', - her: 'Her', - its: 'Its', - our: 'Our', - your: 'Your', - their: 'Their', - i: 'I', - you: 'You', - he: 'He', - she: 'She', - it: 'It', - we: 'We', - they: 'They', - am: 'Am', - is: 'Is', - are: 'Are', - was: 'Was', - were: 'Were', - be: 'Be', - been: 'Been', - being: 'Being', - have: 'Have', - has: 'Has', - had: 'Had', - having: 'Having', - do: 'Do', - does: 'Does', - did: 'Did', - doing: 'Doing', - will: 'Will', - would: 'Would', - shall: 'Shall', - should: 'Should', - may: 'May', - might: 'Might', - can: 'Can', - could: 'Could', - must: 'Must', - need: 'Need', - dare: 'Dare', - ought: 'Ought', - used: 'Used', - to: 'To', - of: 'Of', - in: 'In', - for: 'For', - on: 'On', - with: 'With', - at: 'At', - by: 'By', - from: 'From', - into: 'Into', - during: 'During', - before: 'Before', - after: 'After', - above: 'Above', - below: 'Below', - between: 'Between', - under: 'Under', - over: 'Over', - across: 'Across', - through: 'Through', - into: 'Into', - towards: 'Towards', - upon: 'Upon', - about: 'About', - against: 'Against', - among: 'Among', - along: 'Along', - around: 'Around', - beyond: 'Beyond', - but: 'But', - despite: 'Despite', - except: 'Except', - inside: 'Inside', - outside: 'Outside', - since: 'Since', - until: 'Until', - unless: 'Unless', - whether: 'Whether', - while: 'While', - although: 'Although', - because: 'Because', - if: 'If', - once: 'Once', - since: 'Since', - so: 'So', - that: 'That', - though: 'Though', - till: 'Till', - unless: 'Unless', - until: 'Until', - when: 'When', - whenever: 'Whenever', - where: 'Where', - wherever: 'Wherever', - whereas: 'Whereas', - wherever: 'Wherever', - while: 'While', - why: 'Why', }, }; diff --git a/src/plugins.js b/src/plugins.js index 8aa1ec0..dc97a54 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,7 +1,7 @@ /** * MetonaToast Plugins - 插件系统 * @module plugins - * @version 0.1.1 + * @version 0.1.2 * @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility) */ @@ -21,15 +21,19 @@ class PluginManager { console.warn(`Plugin "${name}" is already registered`); return this; } - if (!plugin || typeof plugin !== 'object' || (!plugin.name && !plugin.version)) { - console.error(`Invalid plugin "${name}"`); + if (!plugin || typeof plugin !== 'object' || !plugin.name) { + console.error(`Invalid plugin "${name}": must be an object with a "name" property`); return this; } - this.plugins.set(name, { name, ...plugin, installed: false, enabled: true }); + // 先存储再 install,确保 install 内通过 this 设置的实例属性保留在存储对象上 + // 保留插件自带的 name(若有),否则使用注册 key 作为 name + const stored = { ...plugin, installed: false, enabled: true }; + if (!stored.name) stored.name = name; + this.plugins.set(name, stored); - if (plugin.install) { - try { plugin.install(this); this.plugins.get(name).installed = true; } + if (stored.install) { + try { stored.install(this); stored.installed = true; } catch (e) { console.error(`Failed to install plugin "${name}":`, e); } } return this; diff --git a/src/styles.js b/src/styles.js index e506285..ee3f0c1 100644 --- a/src/styles.js +++ b/src/styles.js @@ -1,7 +1,7 @@ /** - * MetonaToast Styles - 样式管理(精简版 v0.1.1) + * MetonaToast Styles - 样式管理(精简版 v0.1.2) * @module styles - * @version 0.1.1 + * @version 0.1.2 * @description 样式注入与主题管理,移除未使用的组件样式 */ diff --git a/src/themes.js b/src/themes.js index 0545950..bfe8b94 100644 --- a/src/themes.js +++ b/src/themes.js @@ -1,7 +1,7 @@ /** * MetonaToast Themes - 主题管理 * @module themes - * @version 0.1.1 + * @version 0.1.2 * @description 主题系统、自定义主题和主题切换 */ @@ -458,84 +458,4 @@ export const themeUtils = { loadTheme, }; -/** - * 预设主题 - */ -export const presetThemes = { - /** - * 浅色主题 - */ - light: { - name: '浅色', - description: '明亮清晰的主题', - config: THEMES.light, - }, - - /** - * 深色主题 - */ - dark: { - name: '深色', - description: '护眼舒适的暗色主题', - config: THEMES.dark, - }, - - /** - * 自动主题 - */ - auto: { - name: '自动', - description: '跟随系统主题设置', - config: 'auto', - }, - - warm: { - name: '暖色', - description: '温馨舒适的暖色主题', - config: THEMES.warm, - }, -}; - -// 注册预设主题 -Object.entries(presetThemes).forEach(([name, theme]) => { - if (name !== 'auto' && theme.config !== 'auto') { - THEMES[name] = theme.config; - } -}); - -/** - * 创建主题管理器 - * @returns {Object} 主题管理器 - */ -export const createThemeManager = () => { - return { - getSystemTheme, - resolveTheme, - getThemeConfig, - applyTheme, - getCurrentTheme, - getResolvedTheme, - switchTheme, - toggleTheme, - resetToAuto, - initTheme, - watchSystemTheme, - unwatchSystemTheme, - addThemeListener, - removeThemeListener, - clearThemeListeners, - registerTheme, - unregisterTheme, - getAllThemes, - getThemeNames, - hasTheme, - getThemePreview, - generateThemeCSS, - applyThemeCSS, - removeThemeCSS, - saveTheme, - loadTheme, - }; -}; - export { themeUtils as default }; diff --git a/src/utils.js b/src/utils.js index 767484d..965e99d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,7 +1,7 @@ /** * MetonaToast Utils - 精简工具函数 * @module utils - * @version 0.1.1 + * @version 0.1.2 * @description 仅保留核心模块实际使用的工具函数 */ diff --git a/tests/index.test.js b/tests/index.test.js index 6c2cda1..bb7ce02 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,7 +1,7 @@ /** * MetonaToast 单元测试 * @module tests - * @version 0.1.1 + * @version 0.1.2 */ import MeToast, { Toast, VERSION } from '../src/index.js'; @@ -121,8 +121,8 @@ describe('MetonaToast', () => { describe('版本信息', () => { test('应该有正确的版本号', () => { - expect(VERSION).toBe('0.1.1'); - expect(MeToast.version).toBe('0.1.1'); + expect(VERSION).toBe('0.1.2'); + expect(MeToast.version).toBe('0.1.2'); }); }); @@ -558,13 +558,52 @@ describe('MetonaToast', () => { message: '消息', duration: 5000, }); - + toast._pause(); expect(toast.paused).toBe(true); - + toast._resume(); expect(toast.paused).toBe(false); }); + + test('暂停恢复应保持 remaining 时间连续性', () => { + const toast = MeToast.success({ + message: '消息', + duration: 5000, + }); + + // 模拟计时开始 + toast.config.duration = 5000; + // 直接设置内部状态模拟已运行 2 秒 + 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); + // resume 后 remaining 应保持与暂停时一致(允许微小误差) + 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'); + // DOM 验证需要实际浏览器环境,此处验证状态变更 + }); + + 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({ @@ -583,7 +622,7 @@ describe('MetonaToast', () => { MeToast.success('消息'); const status = MeToast.getStatus(); - expect(status.version).toBe('0.1.1'); + expect(status.version).toBe('0.1.2'); expect(status.toasts).toBeGreaterThanOrEqual(0); expect(status.theme).toBeDefined(); expect(status.locale).toBeDefined(); @@ -1391,4 +1430,23 @@ describe('新增功能', () => { }); expect(t.config.render).toBeInstanceOf(Function); }); + + test('onError 回调在钩子异常时触发', () => { + const errors = []; + MeToast.configure({ onError: (e) => errors.push(e) }); + const badHandler = () => { throw new Error('test-error'); }; + Toast.on('afterShow', badHandler); + const toast = MeToast.success('test'); + Toast.trigger('afterShow', toast); + expect(errors.length).toBeGreaterThanOrEqual(1); + expect(errors[0].error.message).toBe('test-error'); + Toast.off('afterShow', badHandler); + MeToast.configure({ onError: null }); + }); + + test('destroy 重复调用不抛异常', () => { + MeToast.success('test'); + MeToast.destroy(); + expect(() => MeToast.destroy()).not.toThrow(); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index a28695c..58dba7a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,6 @@ /** * MetonaToast TypeScript 类型定义 - * @version 0.1.1 + * @version 0.1.2 */ // 基础类型 @@ -276,6 +276,8 @@ export interface ToastConfig { resetTimerOnUpdate?: boolean; /** 后台发送系统通知 */ notifyWhenHidden?: boolean; + /** 全局错误回调 */ + onError?: (errorInfo: { hook?: string; source?: string; error: Error; toast?: ToastInstance }) => void; /** 分组名称 */ group?: string; /** 语言 */