-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathnotificationToast.test.tsx
More file actions
136 lines (117 loc) · 4.97 KB
/
Copy pathnotificationToast.test.tsx
File metadata and controls
136 lines (117 loc) · 4.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* The console's `toast` surface — the one display type `NotificationProvider`
* delegates instead of rendering itself (#3014). These pin the mapping onto
* sonner; which notifications ever get here is covered by
* `console/__tests__/ConsoleShell.notifications.test.tsx`.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { isValidElement } from 'react';
import type { NotificationItem } from '@object-ui/react';
vi.mock('sonner', () => ({
toast: Object.assign(vi.fn(), {
info: vi.fn(), success: vi.fn(), warning: vi.fn(), error: vi.fn(),
}),
}));
import { toast } from 'sonner';
import { presentNotificationToast } from './notificationToast';
// Imported at module scope, not inside a test: `@object-ui/components` is a
// heavy barrel and resolving it mid-assertion would race the RTL timeouts
// (AGENTS.md § 测试纪律).
import '@object-ui/components';
function item(overrides: Partial<NotificationItem> = {}): NotificationItem {
return {
id: 'n1',
title: 'Saved',
severity: 'success',
createdAt: new Date(0),
...overrides,
};
}
describe('presentNotificationToast', () => {
beforeEach(() => { vi.clearAllMocks(); });
it('maps each severity to its sonner variant', () => {
presentNotificationToast(item({ severity: 'info' }));
presentNotificationToast(item({ severity: 'success' }));
presentNotificationToast(item({ severity: 'warning' }));
presentNotificationToast(item({ severity: 'error' }));
expect(toast.info).toHaveBeenCalledTimes(1);
expect(toast.success).toHaveBeenCalledTimes(1);
expect(toast.warning).toHaveBeenCalledTimes(1);
expect(toast.error).toHaveBeenCalledTimes(1);
});
it('passes the message through as the toast description', () => {
presentNotificationToast(item({ message: 'Record updated' }));
expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
description: 'Record updated',
}));
});
it('turns duration 0 into a persistent toast', () => {
// `0 = persistent` is the notification contract; sonner spells it Infinity.
// Passing the 0 through would make the toast vanish on the next tick.
presentNotificationToast(item({ duration: 0 }));
expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
duration: Infinity,
}));
});
it('leaves an unset duration to the ConsoleToaster default', () => {
presentNotificationToast(item());
expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
duration: undefined,
}));
});
it('forwards an explicit duration unchanged', () => {
presentNotificationToast(item({ duration: 12_000 }));
expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
duration: 12_000,
}));
});
it('maps the first action to the toast action button', () => {
const onClick = vi.fn();
presentNotificationToast(item({
actions: [
{ label: 'Undo', onClick },
{ label: 'Ignored — a toast has one action slot', onClick: vi.fn() },
],
}));
const options = vi.mocked(toast.success).mock.calls[0][1] as {
action?: { label: string; onClick: () => void };
};
expect(options.action?.label).toBe('Undo');
options.action?.onClick();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('omits the action slot entirely when none is declared', () => {
presentNotificationToast(item());
const options = vi.mocked(toast.success).mock.calls[0][1] as Record<string, unknown>;
expect(options).not.toHaveProperty('action');
});
it('forwards dismissible', () => {
presentNotificationToast(item({ dismissible: false }));
expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
dismissible: false,
}));
});
it('passes the spec icon override to sonner', () => {
presentNotificationToast(item({ icon: 'rocket' }));
const { icon } = vi.mocked(toast.success).mock.calls[0][1] as { icon?: unknown };
expect(isValidElement(icon)).toBe(true);
expect((icon as { props: { name?: string } }).props.name).toBe('rocket');
});
it('accepts a PascalCase icon name', () => {
presentNotificationToast(item({ icon: 'CircleCheck' }));
const { icon } = vi.mocked(toast.success).mock.calls[0][1] as { icon?: unknown };
expect(isValidElement(icon)).toBe(true);
});
it('omits the icon key for a name Lucide does not have', () => {
// Passing it through would render LazyIcon's `Database` fallback — a
// meaningless glyph where ConsoleToaster's severity icon belongs.
presentNotificationToast(item({ icon: 'not-a-real-icon' }));
const options = vi.mocked(toast.success).mock.calls[0][1] as Record<string, unknown>;
expect(options).not.toHaveProperty('icon');
});
it('omits the icon key when none is declared', () => {
presentNotificationToast(item());
const options = vi.mocked(toast.success).mock.calls[0][1] as Record<string, unknown>;
expect(options).not.toHaveProperty('icon');
});
});