- 包声明 type:module 时 .js 产物被 Node 按 ESM 解析:Node 16/18 下 require 报 ERR_REQUIRE_ESM,Node 20+ 拿到 namespace 对象(success/version 等属性 undefined) - dist/metona-toast.cjs.js → .cjs,dist/react.cjs.js → .cjs - 主包 CJS footer 将 default 提升为 module.exports:require() 直接返回 MeToast(兼容 .default / .MeToast / .Met / .Toast / .VERSION 所有用法) - 版本 0.5.1(0.5.0 已发布带问题产物) - 真实消费者验证:CJS/ESM/React 子包四路加载全部正常
150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
/**
|
|
* MetonaToast React 适配器测试
|
|
* @module tests
|
|
* @version 0.5.1
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { renderToString } from 'react-dom/server';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { act } from 'react';
|
|
import MeToast, { Toast } from '../src/index';
|
|
import { createBoundApi, Toast as ToastComponent, useToast } from '../src/react';
|
|
import type { ToastInstance } from '../src/types';
|
|
|
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
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.reset();
|
|
});
|
|
|
|
describe('React 适配器 — createBoundApi', () => {
|
|
test('跟踪创建的 toast id', () => {
|
|
const cleanup = new Set<string>();
|
|
const api = createBoundApi(cleanup);
|
|
(api.info as (m: string) => ToastInstance)('hello');
|
|
expect(cleanup.size).toBe(1);
|
|
});
|
|
|
|
test('success/error/warning/show 均被跟踪', () => {
|
|
const cleanup = new Set<string>();
|
|
const api = createBoundApi(cleanup);
|
|
(api.success as (m: string) => ToastInstance)('a');
|
|
(api.error as (m: string) => ToastInstance)('b');
|
|
(api.warning as (m: string) => ToastInstance)('c');
|
|
(api.show as (m: string) => ToastInstance)('d');
|
|
expect(cleanup.size).toBe(4);
|
|
});
|
|
|
|
test('loading 控制对象也被跟踪', () => {
|
|
const cleanup = new Set<string>();
|
|
const api = createBoundApi(cleanup);
|
|
(api.loading as (m: string) => { id: string })('loading...');
|
|
expect(cleanup.size).toBe(1);
|
|
});
|
|
|
|
test('dismiss 同步清理集合', () => {
|
|
const cleanup = new Set<string>();
|
|
const api = createBoundApi(cleanup);
|
|
const t = (api.success as (m: string) => ToastInstance)('hi');
|
|
expect(cleanup.has(t.id)).toBe(true);
|
|
(api.dismiss as (id?: string) => void)(t.id);
|
|
expect(cleanup.has(t.id)).toBe(false);
|
|
});
|
|
|
|
test('removeToast 同步清理集合', () => {
|
|
const cleanup = new Set<string>();
|
|
const api = createBoundApi(cleanup);
|
|
const t = (api.info as (m: string) => ToastInstance)('hi');
|
|
(api.removeToast as (id: string) => void)(t.id);
|
|
expect(cleanup.size).toBe(0);
|
|
});
|
|
|
|
test('未跟踪的方法直接透传', () => {
|
|
const cleanup = new Set<string>();
|
|
const api = createBoundApi(cleanup);
|
|
expect(typeof (api as Record<string, unknown>).count).toBe('function');
|
|
expect((api as { count(): number }).count()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('React 适配器 — Toast 组件', () => {
|
|
test('SSR 渲染为空(effect 不执行)', () => {
|
|
const html = renderToString(React.createElement(ToastComponent, { message: 'ssr message' }));
|
|
expect(html).toBe('');
|
|
});
|
|
|
|
test('SSR 带 type 不抛异常', () => {
|
|
const html = renderToString(React.createElement(ToastComponent, { message: 'x', type: 'success' }));
|
|
expect(html).toBe('');
|
|
});
|
|
|
|
test('SSR 未知 type fallback 到 show 不抛异常', () => {
|
|
const html = renderToString(React.createElement(ToastComponent, { message: 'y', type: 'custom-type' }));
|
|
expect(html).toBe('');
|
|
});
|
|
|
|
test('组件挂载创建 toast,卸载自动移除', () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'mounted', type: 'success' })); });
|
|
expect(MeToast.count()).toBe(1);
|
|
act(() => { root.unmount(); });
|
|
expect(MeToast.count()).toBe(0);
|
|
host.remove();
|
|
});
|
|
|
|
test('组件 autoClose:false 卸载不移除', () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'keep', type: 'error', autoClose: false })); });
|
|
expect(MeToast.count()).toBe(1);
|
|
act(() => { root.unmount(); });
|
|
expect(MeToast.count()).toBe(1);
|
|
MeToast.dismiss();
|
|
host.remove();
|
|
});
|
|
|
|
test('组件 message 变化重新创建实例', () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'v1', type: 'info' })); });
|
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'v2', type: 'warning' })); });
|
|
expect(MeToast.count()).toBe(1);
|
|
const t = MeToast.getToasts()[0];
|
|
expect(t.message).toBe('v2');
|
|
expect(t.type).toBe('warning');
|
|
act(() => { root.unmount(); });
|
|
expect(MeToast.count()).toBe(0);
|
|
host.remove();
|
|
});
|
|
|
|
test('useToast 组件内创建的 toast 在卸载时清理', () => {
|
|
const Harness = (): React.ReactElement => {
|
|
const toast = useToast() as Record<string, (m: string) => ToastInstance>;
|
|
return React.createElement('button', { onClick: () => toast.info('tracked') });
|
|
};
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
act(() => { root.render(React.createElement(Harness)); });
|
|
const btn = host.querySelector('button')!;
|
|
act(() => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
|
expect(MeToast.count()).toBe(1);
|
|
act(() => { root.unmount(); });
|
|
expect(MeToast.count()).toBe(0);
|
|
host.remove();
|
|
});
|
|
});
|