Skip to content

Commit 1c9c936

Browse files
test(Alert): improve test coverage of alert (#6048)
* test(alert): 完善 Alert 组件 API/分支用例,提升覆盖率至 100% * test(unit): optimization * test(unit): optimization * test(unit): optimization * test(unit): spy window.prompt * test(unit): optimization --------- Co-authored-by: zhangpaopao <zhangpaopao0609@gmail.com>
1 parent 39858ff commit 1c9c936

File tree

2 files changed

+265
-56
lines changed

2 files changed

+265
-56
lines changed
Lines changed: 261 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { mount } from '@vue/test-utils';
2-
import { describe, expect, it, vi } from 'vitest';
2+
import type { VueWrapper } from '@vue/test-utils';
3+
import { describe, expect, it, vi, beforeEach } from 'vitest';
34
import {
45
CloseIcon,
56
AppIcon,
@@ -8,89 +9,148 @@ import {
89
InfoCircleFilledIcon,
910
} from 'tdesign-icons-vue-next';
1011
import { Fragment, nextTick } from 'vue';
12+
import log from '@tdesign/common-js/log/index';
1113
import { Alert } from '@tdesign/components/alert';
1214

15+
const mock = vi.hoisted(() => {
16+
const store = { handler: null } as { handler: ((e: any) => void) | null };
17+
const onMock = vi.fn((el: Element, evt: string, handler: (e: any) => void) => {
18+
if (evt === 'transitionend') {
19+
store.handler = handler;
20+
}
21+
});
22+
const addClassMock = vi.fn((el: Element | null, cls: string) => {
23+
if (el) {
24+
el.classList.add(cls);
25+
}
26+
});
27+
return {
28+
store,
29+
onMock,
30+
addClassMock,
31+
};
32+
});
33+
34+
vi.mock('@tdesign/shared-utils', async (importOriginal) => {
35+
const actual = await importOriginal<any>();
36+
return {
37+
...actual,
38+
on: mock.onMock,
39+
addClass: mock.addClassMock,
40+
};
41+
});
42+
43+
function setOffsetHeight(el: Element, height: number) {
44+
Object.defineProperty(el, 'offsetHeight', {
45+
value: height,
46+
configurable: true,
47+
});
48+
}
49+
1350
describe('Alert', () => {
14-
describe(':props', () => {
15-
it(':default', () => {
16-
const wrapper = mount(() => <Alert>text</Alert>);
17-
expect(wrapper.find('.t-alert__description').text()).toBe('text');
51+
describe('props', () => {
52+
let wrapper: VueWrapper | null = null;
53+
54+
beforeEach(() => {
55+
wrapper = mount(() => <Alert>text</Alert>);
56+
});
57+
58+
it('default', () => {
59+
expect(wrapper.find('.t-alert__description').text()).eq('text');
1860
});
1961

20-
it(':closeBtn', () => {
21-
const wrapper = mount(() => <Alert closeBtn>text</Alert>);
62+
it('closeBtn[boolean]', async () => {
63+
expect(wrapper.find('.t-alert__close').exists()).eq(false);
64+
65+
const wrapperWithClose = mount(() => <Alert closeBtn>text</Alert>);
66+
const close = wrapperWithClose.find('.t-alert__close');
67+
expect(close.exists()).eq(true);
68+
expect(wrapperWithClose.findComponent(CloseIcon).exists()).eq(true);
69+
});
70+
71+
it('closeBtn[string]', () => {
72+
const wrapper = mount(() => <Alert closeBtn="关闭">text</Alert>);
73+
const close = wrapper.find('.t-alert__close');
74+
expect(close.exists()).eq(true);
75+
expect(close.text()).eq('关闭');
76+
});
77+
78+
it('closeBtn[slot]', () => {
79+
const slots = {
80+
closeBtn: () => <span>自定义关闭</span>,
81+
};
82+
const wrapper = mount(() => <Alert v-slots={slots}>text</Alert>);
2283
const close = wrapper.find('.t-alert__close');
23-
expect(close.exists()).toBeTruthy();
24-
expect(wrapper.findComponent(CloseIcon).exists()).toBeTruthy();
84+
expect(close.exists()).eq(true);
85+
expect(close.text()).eq('自定义关闭');
2586
});
2687

27-
it(':icon', () => {
88+
it('icon[slot]', () => {
2889
const slots = {
2990
icon: () => <AppIcon />,
3091
};
3192
const wrapper = mount(() => <Alert v-slots={slots}>text</Alert>);
3293
const icon = wrapper.find('.t-alert__icon');
33-
expect(icon.exists()).toBeTruthy();
34-
expect(wrapper.findComponent(AppIcon).exists()).toBeTruthy();
94+
expect(icon.exists()).eq(true);
95+
expect(wrapper.findComponent(AppIcon).exists()).eq(true);
96+
});
97+
98+
it('icon[function]', () => {
99+
const wrapper = mount(() => <Alert icon={() => <AppIcon />}>text</Alert>);
100+
const icon = wrapper.find('.t-alert__icon');
101+
expect(icon.exists()).eq(true);
102+
expect(wrapper.findComponent(AppIcon).exists()).eq(true);
35103
});
36104

37-
it(':message', () => {
105+
it('message[string]', () => {
38106
const wrapper = mount(() => <Alert message="this is message"></Alert>);
39107
const description = wrapper.find('.t-alert__message .t-alert__description');
40-
expect(description.exists()).toBeTruthy();
41-
expect(description.text()).toBe('this is message');
108+
expect(description.exists()).eq(true);
109+
expect(description.text()).eq('this is message');
42110
});
43111

44-
it(':title', () => {
112+
it('title[string]', () => {
45113
const wrapper = mount(() => <Alert title="this is title">text</Alert>);
46114
const title = wrapper.find('.t-alert__title');
47-
expect(title.exists()).toBeTruthy();
48-
expect(title.text()).toBe('this is title');
115+
expect(title.exists()).eq(true);
116+
expect(title.text()).eq('this is title');
49117
});
50118

51-
it(':operation', () => {
119+
it('operation[slot]', () => {
52120
const slots = {
53121
operation: () => <Fragment>this is operation</Fragment>,
54122
};
55123
const wrapper = mount(() => <Alert v-slots={slots}>text</Alert>);
56124
const operation = wrapper.find('.t-alert__operation');
57-
expect(operation.exists()).toBeTruthy();
58-
expect(operation.text()).toBe('this is operation');
59-
});
60-
61-
it(':theme:info', () => {
62-
const wrapper = mount(() => <Alert theme="info" message="text" />);
63-
const alert = wrapper.find('.t-alert');
64-
const icon = wrapper.find('.t-alert__icon');
65-
expect(icon.findComponent(InfoCircleFilledIcon).exists()).toBeTruthy();
66-
expect(alert.classes()).toContain('t-alert--info');
125+
expect(operation.exists()).eq(true);
126+
expect(operation.text()).eq('this is operation');
67127
});
68128

69-
it(':theme:success', () => {
70-
const wrapper = mount(() => <Alert theme="success" message="text" />);
71-
const alert = wrapper.find('.t-alert');
72-
const icon = wrapper.find('.t-alert__icon');
73-
expect(icon.findComponent(CheckCircleFilledIcon).exists()).toBeTruthy();
74-
expect(alert.classes()).toContain('t-alert--success');
129+
it('operation[function]', () => {
130+
const wrapper = mount(() => <Alert operation={() => <Fragment>op-area</Fragment>}>text</Alert>);
131+
const operation = wrapper.find('.t-alert__operation');
132+
expect(operation.exists()).eq(true);
133+
expect(operation.text()).eq('op-area');
75134
});
76135

77-
it(':theme:warning', () => {
78-
const wrapper = mount(() => <Alert theme="warning" message="text" />);
79-
const alert = wrapper.find('.t-alert');
80-
const icon = wrapper.find('.t-alert__icon');
81-
expect(icon.findComponent(ErrorCircleFilledIcon).exists()).toBeTruthy();
82-
expect(alert.classes()).toContain('t-alert--warning');
83-
});
136+
it('theme[string]', () => {
137+
const themes = [
138+
{ theme: 'info', icon: InfoCircleFilledIcon },
139+
{ theme: 'success', icon: CheckCircleFilledIcon },
140+
{ theme: 'warning', icon: ErrorCircleFilledIcon },
141+
{ theme: 'error', icon: ErrorCircleFilledIcon },
142+
] as const;
84143

85-
it(':theme:error', () => {
86-
const wrapper = mount(() => <Alert theme="error" message="text" />);
87-
const alert = wrapper.find('.t-alert');
88-
const icon = wrapper.find('.t-alert__icon');
89-
expect(icon.findComponent(ErrorCircleFilledIcon).exists()).toBeTruthy();
90-
expect(alert.classes()).toContain('t-alert--error');
144+
themes.forEach(({ theme, icon }) => {
145+
const wrapper = mount(() => <Alert theme={theme} message="text" />);
146+
const alert = wrapper.find('.t-alert');
147+
const iconEl = wrapper.find('.t-alert__icon');
148+
expect(iconEl.findComponent(icon).exists()).eq(true);
149+
expect(alert.classes()).toContain(`t-alert--${theme}`);
150+
});
91151
});
92152

93-
it(':maxLine', async () => {
153+
it('maxLine[number]', async () => {
94154
const wrapper = mount(() => (
95155
<Alert title="this is title" maxLine={2}>
96156
<span>这是折叠的第一条消息</span>
@@ -103,17 +163,30 @@ describe('Alert', () => {
103163
));
104164
const description = wrapper.find('.t-alert__description');
105165
const collapse = description.find('.t-alert__collapse');
106-
expect(description.element.children.length).toBe(3);
107-
expect(collapse.exists()).toBeTruthy();
108-
expect(collapse.text()).toBe('展开更多');
166+
expect(description.element.children.length).eq(3);
167+
expect(collapse.exists()).eq(true);
168+
expect(collapse.text()).eq('展开更多');
109169
await collapse.trigger('click');
110-
expect(description.element.children.length).toBe(7);
111-
expect(collapse.text()).toBe('收起');
170+
expect(description.element.children.length).eq(7);
171+
expect(collapse.text()).eq('收起');
172+
});
173+
174+
it('maxLine[number] no collapse when maxLine=0', () => {
175+
const wrapper = mount(() => (
176+
<Alert maxLine={0} title="title">
177+
<span>1</span>
178+
<span>2</span>
179+
<span>3</span>
180+
</Alert>
181+
));
182+
const description = wrapper.find('.t-alert__description');
183+
expect(description.exists()).eq(true);
184+
expect(description.find('.t-alert__collapse').exists()).eq(false);
112185
});
113186
});
114187

115-
describe(':event', () => {
116-
it(':onClose', async () => {
188+
describe('events', () => {
189+
it('onClose', async () => {
117190
const fn = vi.fn();
118191
const wrapper = mount(() => (
119192
<Alert closeBtn onClose={fn}>
@@ -128,4 +201,136 @@ describe('Alert', () => {
128201
expect(alert.classes()).toContain('t-alert--closing');
129202
});
130203
});
204+
205+
describe('others', () => {
206+
it('onClosed not triggered when propertyName is not opacity', async () => {
207+
const onClosed = vi.fn();
208+
const wrapper = mount(() => (
209+
<Alert closeBtn onClosed={onClosed}>
210+
text
211+
</Alert>
212+
));
213+
const alertEl = wrapper.find('.t-alert');
214+
215+
mock.store.handler?.({ propertyName: 'height', target: alertEl.element });
216+
await nextTick();
217+
expect(onClosed).not.toHaveBeenCalled();
218+
expect(alertEl.classes()).not.toContain('t-is-hidden');
219+
});
220+
221+
it('close (deprecated) triggers warnOnce and renders CloseIcon', async () => {
222+
const warnSpy = vi.spyOn(log, 'warnOnce');
223+
const wrapper = mount(() => <Alert close>text</Alert>);
224+
const close = wrapper.find('.t-alert__close');
225+
expect(close.exists()).eq(true);
226+
expect(wrapper.findComponent(CloseIcon).exists()).eq(true);
227+
expect(warnSpy).toHaveBeenCalled();
228+
warnSpy.mockRestore();
229+
});
230+
231+
it('theme invalid value triggers validator branch', () => {
232+
const wrapper = mount(() => <Alert theme={'unknown' as any} message="text" />);
233+
const alert = wrapper.find('.t-alert');
234+
// 即便 validator 警告,类名仍会跟随传入值,覆盖该分支
235+
expect(alert.classes()).toContain('t-alert--unknown');
236+
});
237+
238+
it('renderIcon returns null when icon function returns null', () => {
239+
const wrapper = mount(() => <Alert icon={() => null}>text</Alert>);
240+
// 不应渲染 icon 容器
241+
expect(wrapper.find('.t-alert__icon').exists()).eq(false);
242+
});
243+
244+
it('renderClose uses "close" slot when provided (isUsingClose = true)', async () => {
245+
const slots = {
246+
close: () => <span>关闭插槽</span>,
247+
};
248+
const wrapper = mount(() => <Alert v-slots={slots}>text</Alert>);
249+
const close = wrapper.find('.t-alert__close');
250+
expect(close.exists()).eq(true);
251+
expect(close.text()).eq('关闭插槽');
252+
});
253+
254+
it('renderDescription height short-circuit false branch in collapsed and expanded', async () => {
255+
const wrapper = mount(() => (
256+
<Alert title="desc height test" maxLine={2}>
257+
<span>line1</span>
258+
<span>line2</span>
259+
<span>line3</span>
260+
<span>line4</span>
261+
</Alert>
262+
));
263+
const description = wrapper.find('.t-alert__description');
264+
// 在 jsdom 中 offsetHeight 通常为 0(falsy),应走 height && ... 的 false 分支,不设置 style.height
265+
expect((description.element as HTMLElement).style.height).eq('');
266+
267+
// 展开后也应保持未设置 height(覆盖展开路径的 false 分支)
268+
const collapse = description.find('.t-alert__collapse');
269+
expect(collapse.exists()).eq(true);
270+
await collapse.trigger('click');
271+
expect((description.element as HTMLElement).style.height).eq('');
272+
});
273+
274+
it('props.theme validator falsy branch (val is empty string)', () => {
275+
const wrapper = mount(() => <Alert theme={'' as any} message="text" />);
276+
const alert = wrapper.find('.t-alert');
277+
// 传入空字符串使 validator 走 !val 分支;类名将拼接为空后缀
278+
expect(alert.classes()).toContain('t-alert--');
279+
});
280+
281+
it('renderDescription height truthy branch (collapsed)', async () => {
282+
const wrapper = mount(() => (
283+
<Alert title="height truthy collapsed" maxLine={2}>
284+
<span>line1</span>
285+
<span>line2</span>
286+
<span>line3</span>
287+
<span>line4</span>
288+
</Alert>
289+
));
290+
const description = wrapper.find('.t-alert__description');
291+
// 设置 description 自身和第一个子元素的 offsetHeight,使 height 判断为真
292+
setOffsetHeight(description.element, 8);
293+
const firstChild = description.element.children[0] as HTMLElement;
294+
setOffsetHeight(firstChild, 10);
295+
296+
// 重新触发渲染以走到高度设置逻辑
297+
await wrapper.vm.$forceUpdate();
298+
await new Promise((r) => setTimeout(r, 0));
299+
300+
// 折叠态:style.height 应设置为 descHeight(mounted 时记录的 description.offsetHeight)
301+
expect((description.element as HTMLElement).style.height).eq('0px');
302+
});
303+
304+
it('renderDescription height truthy branch (expanded)', async () => {
305+
const wrapper = mount(() => (
306+
<Alert title="height truthy expanded" maxLine={2}>
307+
<span>line1</span>
308+
<span>line2</span>
309+
<span>line3</span>
310+
<span>line4</span>
311+
</Alert>
312+
));
313+
const description = wrapper.find('.t-alert__description');
314+
// 设置 description 自身和第一个子元素的 offsetHeight,使 height 判断为真
315+
setOffsetHeight(description.element, 8);
316+
const firstChild = description.element.children[0] as HTMLElement;
317+
setOffsetHeight(firstChild, 10);
318+
319+
// 点击展开
320+
const collapse = description.find('.t-alert__collapse');
321+
expect(collapse.exists()).toBeTruthy();
322+
await collapse.trigger('click');
323+
324+
// 展开态:style.height = height * (contentLength - maxLine) + descHeight
325+
// contentLength = 4, maxLine = 2, height = 10, descHeight = 8 => 28px
326+
expect((description.element as HTMLElement).style.height).eq('20px');
327+
});
328+
329+
it('operation via function still renders (sanity)', () => {
330+
const wrapper = mount(() => <Alert operation={() => <Fragment>op</Fragment>}>text</Alert>);
331+
const operation = wrapper.find('.t-alert__operation');
332+
expect(operation.exists()).eq(true);
333+
expect(operation.text()).eq('op');
334+
});
335+
});
131336
});

packages/components/typography/__tests__/typography-text.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { Text } from '@tdesign/components';
33
import type { TdTextProps } from '@tdesign/components';
44
import { nextTick } from 'vue';
55

6+
beforeEach(() => {
7+
vi.spyOn(window, 'prompt').mockImplementation(() => null);
8+
});
9+
610
describe('Typography Text', () => {
711
const longTextString = `TDesign was founded with the principles of open-source collaboration from the beginning. The collaboration scheme discussion, component design, and API design, including source code, are fully open within the company, garnering widespread attention from internal developers and designers. TDesign follows an equal, open, and strict policy, regardless of the participants' roles.`;
812
const shortText = 'TDesign was founded with the principles of open-source collaboration from the beginning.';

0 commit comments

Comments
 (0)