@dashforge/tw 0.11.0-beta → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -12
- package/COVERAGE.md +2 -2
- package/PARITY.md +7 -9
- package/PERFORMANCE.md +3 -4
- package/README.md +74 -15
- package/THEME-AUDIT.md +3 -4
- package/dashforge-tw-1.0.0.tgz +0 -0
- package/dist/index.esm.js +3127 -748
- package/dist/src/components/Alert/Alert.d.ts +65 -0
- package/dist/src/components/Alert/Alert.d.ts.map +1 -0
- package/dist/src/components/Alert/alert.types.d.ts +130 -0
- package/dist/src/components/Alert/alert.types.d.ts.map +1 -0
- package/dist/src/components/Alert/alert.variants.d.ts +85 -0
- package/dist/src/components/Alert/alert.variants.d.ts.map +1 -0
- package/dist/src/components/Avatar/Avatar.d.ts +48 -0
- package/dist/src/components/Avatar/Avatar.d.ts.map +1 -0
- package/dist/src/components/Avatar/avatar.types.d.ts +162 -0
- package/dist/src/components/Avatar/avatar.types.d.ts.map +1 -0
- package/dist/src/components/Avatar/avatar.variants.d.ts +208 -0
- package/dist/src/components/Avatar/avatar.variants.d.ts.map +1 -0
- package/dist/src/components/Badge/Badge.d.ts +63 -0
- package/dist/src/components/Badge/Badge.d.ts.map +1 -0
- package/dist/src/components/Badge/badge.types.d.ts +124 -0
- package/dist/src/components/Badge/badge.types.d.ts.map +1 -0
- package/dist/src/components/Badge/badge.variants.d.ts +262 -0
- package/dist/src/components/Badge/badge.variants.d.ts.map +1 -0
- package/dist/src/components/Box/Box.d.ts.map +1 -1
- package/dist/src/components/Box/box.types.d.ts +53 -0
- package/dist/src/components/Box/box.types.d.ts.map +1 -1
- package/dist/src/components/Button/Button.d.ts.map +1 -1
- package/dist/src/components/Button/button.types.d.ts +35 -0
- package/dist/src/components/Button/button.types.d.ts.map +1 -1
- package/dist/src/components/Card/Card.d.ts +107 -0
- package/dist/src/components/Card/Card.d.ts.map +1 -0
- package/dist/src/components/Card/card.types.d.ts +119 -0
- package/dist/src/components/Card/card.types.d.ts.map +1 -0
- package/dist/src/components/Chip/Chip.d.ts +67 -0
- package/dist/src/components/Chip/Chip.d.ts.map +1 -0
- package/dist/src/components/Chip/chip.types.d.ts +113 -0
- package/dist/src/components/Chip/chip.types.d.ts.map +1 -0
- package/dist/src/components/Chip/chip.variants.d.ts +155 -0
- package/dist/src/components/Chip/chip.variants.d.ts.map +1 -0
- package/dist/src/components/IconButton/IconButton.d.ts +57 -0
- package/dist/src/components/IconButton/IconButton.d.ts.map +1 -0
- package/dist/src/components/IconButton/iconButton.types.d.ts +96 -0
- package/dist/src/components/IconButton/iconButton.types.d.ts.map +1 -0
- package/dist/src/components/IconButton/iconButton.variants.d.ts +26 -0
- package/dist/src/components/IconButton/iconButton.variants.d.ts.map +1 -0
- package/dist/src/components/Menu/Menu.d.ts +118 -0
- package/dist/src/components/Menu/Menu.d.ts.map +1 -0
- package/dist/src/components/Menu/menu.types.d.ts +179 -0
- package/dist/src/components/Menu/menu.types.d.ts.map +1 -0
- package/dist/src/components/Menu/menu.variants.d.ts +129 -0
- package/dist/src/components/Menu/menu.variants.d.ts.map +1 -0
- package/dist/src/components/Snackbar/Snackbar.d.ts.map +1 -1
- package/dist/src/components/Snackbar/snackbar.types.d.ts +40 -3
- package/dist/src/components/Snackbar/snackbar.types.d.ts.map +1 -1
- package/dist/src/components/Snackbar/snackbar.variants.d.ts +12 -57
- package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -1
- package/dist/src/components/Spinner/Spinner.d.ts +44 -0
- package/dist/src/components/Spinner/Spinner.d.ts.map +1 -0
- package/dist/src/components/Spinner/spinner.types.d.ts +89 -0
- package/dist/src/components/Spinner/spinner.types.d.ts.map +1 -0
- package/dist/src/components/Spinner/spinner.variants.d.ts +111 -0
- package/dist/src/components/Spinner/spinner.variants.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderChip.d.ts +15 -72
- package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -1
- package/dist/src/components/_shared/severity/index.d.ts +21 -0
- package/dist/src/components/_shared/severity/index.d.ts.map +1 -0
- package/dist/src/components/_shared/severity/severity.types.d.ts +48 -0
- package/dist/src/components/_shared/severity/severity.types.d.ts.map +1 -0
- package/dist/src/components/_shared/severity/severityIcons.d.ts +45 -0
- package/dist/src/components/_shared/severity/severityIcons.d.ts.map +1 -0
- package/dist/src/components/_shared/severity/severityVariants.d.ts +22 -0
- package/dist/src/components/_shared/severity/severityVariants.d.ts.map +1 -0
- package/dist/src/index.d.ts +27 -2
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/components/Alert/Alert.test.tsx +302 -0
- package/src/components/Alert/Alert.tsx +186 -0
- package/src/components/Alert/alert.types.ts +144 -0
- package/src/components/Alert/alert.variants.ts +71 -0
- package/src/components/Autocomplete/autocomplete.variants.ts +1 -1
- package/src/components/Avatar/Avatar.test.tsx +287 -0
- package/src/components/Avatar/Avatar.tsx +304 -0
- package/src/components/Avatar/avatar.types.ts +205 -0
- package/src/components/Avatar/avatar.variants.ts +194 -0
- package/src/components/Badge/Badge.test.tsx +385 -0
- package/src/components/Badge/Badge.tsx +174 -0
- package/src/components/Badge/badge.types.ts +154 -0
- package/src/components/Badge/badge.variants.ts +161 -0
- package/src/components/Box/Box.test.tsx +21 -0
- package/src/components/Box/Box.tsx +32 -3
- package/src/components/Box/box.types.ts +55 -0
- package/src/components/Button/Button.test.tsx +21 -0
- package/src/components/Button/Button.tsx +28 -27
- package/src/components/Button/button.types.ts +36 -0
- package/src/components/Card/Card.test.tsx +291 -0
- package/src/components/Card/Card.tsx +241 -0
- package/src/components/Card/card.types.ts +132 -0
- package/src/components/Chip/Chip.test.tsx +316 -0
- package/src/components/Chip/Chip.tsx +224 -0
- package/src/components/Chip/chip.types.ts +128 -0
- package/src/components/Chip/chip.variants.ts +173 -0
- package/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx +1 -1
- package/src/components/IconButton/IconButton.test.tsx +367 -0
- package/src/components/IconButton/IconButton.tsx +159 -0
- package/src/components/IconButton/iconButton.types.ts +106 -0
- package/src/components/IconButton/iconButton.variants.ts +31 -0
- package/src/components/LeftNav/leftNav.variants.ts +2 -2
- package/src/components/Menu/Menu.test.tsx +408 -0
- package/src/components/Menu/Menu.tsx +335 -0
- package/src/components/Menu/menu.types.ts +221 -0
- package/src/components/Menu/menu.variants.ts +132 -0
- package/src/components/Pagination/pagination.variants.ts +1 -1
- package/src/components/Snackbar/Snackbar.tsx +46 -28
- package/src/components/Snackbar/snackbar.types.ts +47 -3
- package/src/components/Snackbar/snackbar.variants.ts +29 -22
- package/src/components/Spinner/Spinner.test.tsx +199 -0
- package/src/components/Spinner/Spinner.tsx +158 -0
- package/src/components/Spinner/spinner.types.ts +113 -0
- package/src/components/Spinner/spinner.variants.ts +83 -0
- package/src/components/Table/cells/RenderChip.tsx +24 -87
- package/src/components/Table/cells/RowActionsMenu.tsx +1 -1
- package/src/components/TextField/TextField.test.tsx +5 -1
- package/src/components/_shared/severity/index.ts +33 -0
- package/src/components/_shared/severity/severity.types.ts +50 -0
- package/src/components/_shared/severity/severityIcons.tsx +104 -0
- package/src/components/_shared/severity/severityVariants.test.ts +136 -0
- package/src/components/_shared/severity/severityVariants.ts +115 -0
- package/src/index.ts +163 -1
- package/vite.config.ts +8 -0
- package/vitest.config.mts +8 -1
- package/LICENSE +0 -21
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
4
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
5
|
+
import { RbacProvider } from '@dashforge/rbac';
|
|
6
|
+
import type { RbacPolicy, Subject } from '@dashforge/rbac';
|
|
7
|
+
import {
|
|
8
|
+
Menu,
|
|
9
|
+
MenuContent,
|
|
10
|
+
MenuItem,
|
|
11
|
+
MenuLabel,
|
|
12
|
+
MenuSeparator,
|
|
13
|
+
MenuSkeleton,
|
|
14
|
+
MenuTrigger,
|
|
15
|
+
} from './Menu.js';
|
|
16
|
+
|
|
17
|
+
void React;
|
|
18
|
+
afterEach(() => cleanup());
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Unit tests for the Menu family.
|
|
22
|
+
*
|
|
23
|
+
* Testing strategy: open the menu via `defaultOpen={true}` instead
|
|
24
|
+
* of clicking the trigger. Radix DropdownMenu's open/close flow
|
|
25
|
+
* uses pointer events that don't translate cleanly to jsdom's
|
|
26
|
+
* `fireEvent.click` — and that logic is Radix's responsibility
|
|
27
|
+
* anyway (already covered by their tests). Our tests focus on:
|
|
28
|
+
* - Our `<MenuItem>` props (onClick / disabled / selected / color /
|
|
29
|
+
* icon / endIcon)
|
|
30
|
+
* - Bridge integration on `<MenuItem>` (access / visibleWhen)
|
|
31
|
+
* - Our wrapper components (`<MenuLabel>`, `<MenuSeparator>`,
|
|
32
|
+
* `<MenuSkeleton>`)
|
|
33
|
+
* - `closeOnItemClick` propagation
|
|
34
|
+
*
|
|
35
|
+
* NB: Radix DropdownMenu uses Portal — when `defaultOpen={true}`,
|
|
36
|
+
* the content renders to `document.body`. We assert via
|
|
37
|
+
* `screen.getByText` / `screen.getByRole` which scan the whole
|
|
38
|
+
* document.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const NO_ACCESS_POLICY: RbacPolicy = {
|
|
42
|
+
roles: [{ name: 'guest', permissions: [] }],
|
|
43
|
+
};
|
|
44
|
+
const ADMIN_POLICY: RbacPolicy = {
|
|
45
|
+
roles: [
|
|
46
|
+
{
|
|
47
|
+
name: 'admin',
|
|
48
|
+
permissions: [{ resource: '*', action: '*', effect: 'allow' }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const guestSubject: Subject = { id: 'g', roles: ['guest'] };
|
|
53
|
+
const adminSubject: Subject = { id: 'a', roles: ['admin'] };
|
|
54
|
+
|
|
55
|
+
describe('<Menu>', () => {
|
|
56
|
+
describe('rendering', () => {
|
|
57
|
+
it('renders the trigger', () => {
|
|
58
|
+
render(
|
|
59
|
+
<Menu>
|
|
60
|
+
<MenuTrigger>
|
|
61
|
+
<button>Open</button>
|
|
62
|
+
</MenuTrigger>
|
|
63
|
+
<MenuContent>
|
|
64
|
+
<MenuItem>Item</MenuItem>
|
|
65
|
+
</MenuContent>
|
|
66
|
+
</Menu>
|
|
67
|
+
);
|
|
68
|
+
expect(screen.getByRole('button', { name: /open/i })).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does NOT render content when closed (lazy mount)', () => {
|
|
72
|
+
render(
|
|
73
|
+
<Menu>
|
|
74
|
+
<MenuTrigger>
|
|
75
|
+
<button>Open</button>
|
|
76
|
+
</MenuTrigger>
|
|
77
|
+
<MenuContent>
|
|
78
|
+
<MenuItem>Hidden item</MenuItem>
|
|
79
|
+
</MenuContent>
|
|
80
|
+
</Menu>
|
|
81
|
+
);
|
|
82
|
+
// Without defaultOpen, Radix Portal isn't mounted.
|
|
83
|
+
expect(screen.queryByText('Hidden item')).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders content when defaultOpen=true', () => {
|
|
87
|
+
render(
|
|
88
|
+
<Menu defaultOpen>
|
|
89
|
+
<MenuTrigger>
|
|
90
|
+
<button>Open</button>
|
|
91
|
+
</MenuTrigger>
|
|
92
|
+
<MenuContent>
|
|
93
|
+
<MenuItem>Visible item</MenuItem>
|
|
94
|
+
</MenuContent>
|
|
95
|
+
</Menu>
|
|
96
|
+
);
|
|
97
|
+
expect(screen.getByText('Visible item')).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('MenuItem rendering', () => {
|
|
102
|
+
it('renders icon prop', () => {
|
|
103
|
+
render(
|
|
104
|
+
<Menu defaultOpen>
|
|
105
|
+
<MenuTrigger>
|
|
106
|
+
<button>Open</button>
|
|
107
|
+
</MenuTrigger>
|
|
108
|
+
<MenuContent>
|
|
109
|
+
<MenuItem icon={<span data-testid="icon">i</span>}>
|
|
110
|
+
With icon
|
|
111
|
+
</MenuItem>
|
|
112
|
+
</MenuContent>
|
|
113
|
+
</Menu>
|
|
114
|
+
);
|
|
115
|
+
expect(screen.getByTestId('icon')).toBeTruthy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders endIcon prop', () => {
|
|
119
|
+
render(
|
|
120
|
+
<Menu defaultOpen>
|
|
121
|
+
<MenuTrigger>
|
|
122
|
+
<button>Open</button>
|
|
123
|
+
</MenuTrigger>
|
|
124
|
+
<MenuContent>
|
|
125
|
+
<MenuItem endIcon={<span data-testid="end">⌘K</span>}>
|
|
126
|
+
Command
|
|
127
|
+
</MenuItem>
|
|
128
|
+
</MenuContent>
|
|
129
|
+
</Menu>
|
|
130
|
+
);
|
|
131
|
+
expect(screen.getByTestId('end')).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('renders the children as label', () => {
|
|
135
|
+
render(
|
|
136
|
+
<Menu defaultOpen>
|
|
137
|
+
<MenuTrigger>
|
|
138
|
+
<button>Open</button>
|
|
139
|
+
</MenuTrigger>
|
|
140
|
+
<MenuContent>
|
|
141
|
+
<MenuItem>My label</MenuItem>
|
|
142
|
+
</MenuContent>
|
|
143
|
+
</Menu>
|
|
144
|
+
);
|
|
145
|
+
expect(screen.getByText('My label')).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('MenuItem disabled', () => {
|
|
150
|
+
it('marks the item as disabled', () => {
|
|
151
|
+
render(
|
|
152
|
+
<Menu defaultOpen>
|
|
153
|
+
<MenuTrigger>
|
|
154
|
+
<button>Open</button>
|
|
155
|
+
</MenuTrigger>
|
|
156
|
+
<MenuContent>
|
|
157
|
+
<MenuItem disabled>Save</MenuItem>
|
|
158
|
+
</MenuContent>
|
|
159
|
+
</Menu>
|
|
160
|
+
);
|
|
161
|
+
const item = screen.getByText('Save').closest('[role="menuitem"]');
|
|
162
|
+
expect(item?.getAttribute('data-disabled')).not.toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('MenuItem selected visual', () => {
|
|
167
|
+
it('applies primary bg classes when selected', () => {
|
|
168
|
+
render(
|
|
169
|
+
<Menu defaultOpen>
|
|
170
|
+
<MenuTrigger>
|
|
171
|
+
<button>Open</button>
|
|
172
|
+
</MenuTrigger>
|
|
173
|
+
<MenuContent>
|
|
174
|
+
<MenuItem selected>Theme: light</MenuItem>
|
|
175
|
+
</MenuContent>
|
|
176
|
+
</Menu>
|
|
177
|
+
);
|
|
178
|
+
const item = screen.getByText('Theme: light').closest('[role="menuitem"]');
|
|
179
|
+
expect(item?.className).toContain('bg-primary-50');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('does NOT apply primary bg when not selected', () => {
|
|
183
|
+
render(
|
|
184
|
+
<Menu defaultOpen>
|
|
185
|
+
<MenuTrigger>
|
|
186
|
+
<button>Open</button>
|
|
187
|
+
</MenuTrigger>
|
|
188
|
+
<MenuContent>
|
|
189
|
+
<MenuItem>Theme: light</MenuItem>
|
|
190
|
+
</MenuContent>
|
|
191
|
+
</Menu>
|
|
192
|
+
);
|
|
193
|
+
const item = screen.getByText('Theme: light').closest('[role="menuitem"]');
|
|
194
|
+
expect(item?.className).not.toContain('bg-primary-50');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('MenuItem color="danger"', () => {
|
|
199
|
+
it('applies danger text class', () => {
|
|
200
|
+
render(
|
|
201
|
+
<Menu defaultOpen>
|
|
202
|
+
<MenuTrigger>
|
|
203
|
+
<button>Open</button>
|
|
204
|
+
</MenuTrigger>
|
|
205
|
+
<MenuContent>
|
|
206
|
+
<MenuItem color="danger">Delete</MenuItem>
|
|
207
|
+
</MenuContent>
|
|
208
|
+
</Menu>
|
|
209
|
+
);
|
|
210
|
+
const item = screen.getByText('Delete').closest('[role="menuitem"]');
|
|
211
|
+
expect(item?.className).toContain('text-danger-700');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('default color does NOT apply danger class', () => {
|
|
215
|
+
render(
|
|
216
|
+
<Menu defaultOpen>
|
|
217
|
+
<MenuTrigger>
|
|
218
|
+
<button>Open</button>
|
|
219
|
+
</MenuTrigger>
|
|
220
|
+
<MenuContent>
|
|
221
|
+
<MenuItem>Default</MenuItem>
|
|
222
|
+
</MenuContent>
|
|
223
|
+
</Menu>
|
|
224
|
+
);
|
|
225
|
+
const item = screen.getByText('Default').closest('[role="menuitem"]');
|
|
226
|
+
expect(item?.className).not.toContain('text-danger-700');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('MenuItem access RBAC', () => {
|
|
231
|
+
it('hides the item for unauthorized subjects (onUnauthorized="hide")', () => {
|
|
232
|
+
render(
|
|
233
|
+
<RbacProvider policy={NO_ACCESS_POLICY} subject={guestSubject}>
|
|
234
|
+
<Menu defaultOpen>
|
|
235
|
+
<MenuTrigger>
|
|
236
|
+
<button>Open</button>
|
|
237
|
+
</MenuTrigger>
|
|
238
|
+
<MenuContent>
|
|
239
|
+
<MenuItem>Public</MenuItem>
|
|
240
|
+
<MenuItem
|
|
241
|
+
access={{
|
|
242
|
+
resource: 'admin',
|
|
243
|
+
action: 'manage',
|
|
244
|
+
onUnauthorized: 'hide',
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
Admin-only
|
|
248
|
+
</MenuItem>
|
|
249
|
+
</MenuContent>
|
|
250
|
+
</Menu>
|
|
251
|
+
</RbacProvider>
|
|
252
|
+
);
|
|
253
|
+
expect(screen.getByText('Public')).toBeTruthy();
|
|
254
|
+
expect(screen.queryByText('Admin-only')).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('renders for authorized subjects', () => {
|
|
258
|
+
render(
|
|
259
|
+
<RbacProvider policy={ADMIN_POLICY} subject={adminSubject}>
|
|
260
|
+
<Menu defaultOpen>
|
|
261
|
+
<MenuTrigger>
|
|
262
|
+
<button>Open</button>
|
|
263
|
+
</MenuTrigger>
|
|
264
|
+
<MenuContent>
|
|
265
|
+
<MenuItem
|
|
266
|
+
access={{
|
|
267
|
+
resource: 'admin',
|
|
268
|
+
action: 'manage',
|
|
269
|
+
onUnauthorized: 'hide',
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
Admin-only
|
|
273
|
+
</MenuItem>
|
|
274
|
+
</MenuContent>
|
|
275
|
+
</Menu>
|
|
276
|
+
</RbacProvider>
|
|
277
|
+
);
|
|
278
|
+
expect(screen.getByText('Admin-only')).toBeTruthy();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('MenuItem visibleWhen', () => {
|
|
283
|
+
it('renders when predicate returns true', () => {
|
|
284
|
+
render(
|
|
285
|
+
<Menu defaultOpen>
|
|
286
|
+
<MenuTrigger>
|
|
287
|
+
<button>Open</button>
|
|
288
|
+
</MenuTrigger>
|
|
289
|
+
<MenuContent>
|
|
290
|
+
<MenuItem visibleWhen={() => true}>Shown</MenuItem>
|
|
291
|
+
</MenuContent>
|
|
292
|
+
</Menu>
|
|
293
|
+
);
|
|
294
|
+
expect(screen.getByText('Shown')).toBeTruthy();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('returns null when predicate returns false', () => {
|
|
298
|
+
render(
|
|
299
|
+
<Menu defaultOpen>
|
|
300
|
+
<MenuTrigger>
|
|
301
|
+
<button>Open</button>
|
|
302
|
+
</MenuTrigger>
|
|
303
|
+
<MenuContent>
|
|
304
|
+
<MenuItem>Always shown</MenuItem>
|
|
305
|
+
<MenuItem visibleWhen={() => false}>Hidden</MenuItem>
|
|
306
|
+
</MenuContent>
|
|
307
|
+
</Menu>
|
|
308
|
+
);
|
|
309
|
+
expect(screen.getByText('Always shown')).toBeTruthy();
|
|
310
|
+
expect(screen.queryByText('Hidden')).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('MenuLabel + MenuSeparator', () => {
|
|
315
|
+
it('renders MenuLabel content', () => {
|
|
316
|
+
render(
|
|
317
|
+
<Menu defaultOpen>
|
|
318
|
+
<MenuTrigger>
|
|
319
|
+
<button>Open</button>
|
|
320
|
+
</MenuTrigger>
|
|
321
|
+
<MenuContent>
|
|
322
|
+
<MenuLabel>Section heading</MenuLabel>
|
|
323
|
+
<MenuItem>Item</MenuItem>
|
|
324
|
+
</MenuContent>
|
|
325
|
+
</Menu>
|
|
326
|
+
);
|
|
327
|
+
expect(screen.getByText('Section heading')).toBeTruthy();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('renders MenuSeparator with role="separator"', () => {
|
|
331
|
+
const { baseElement } = render(
|
|
332
|
+
<Menu defaultOpen>
|
|
333
|
+
<MenuTrigger>
|
|
334
|
+
<button>Open</button>
|
|
335
|
+
</MenuTrigger>
|
|
336
|
+
<MenuContent>
|
|
337
|
+
<MenuItem>Above</MenuItem>
|
|
338
|
+
<MenuSeparator />
|
|
339
|
+
<MenuItem>Below</MenuItem>
|
|
340
|
+
</MenuContent>
|
|
341
|
+
</Menu>
|
|
342
|
+
);
|
|
343
|
+
const sep = baseElement.querySelector('[role="separator"]');
|
|
344
|
+
expect(sep).toBeTruthy();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('overrides — sx + className', () => {
|
|
349
|
+
it('applies sx to a MenuItem', () => {
|
|
350
|
+
render(
|
|
351
|
+
<Menu defaultOpen>
|
|
352
|
+
<MenuTrigger>
|
|
353
|
+
<button>Open</button>
|
|
354
|
+
</MenuTrigger>
|
|
355
|
+
<MenuContent>
|
|
356
|
+
<MenuItem sx="ring-2 ring-purple-500">x</MenuItem>
|
|
357
|
+
</MenuContent>
|
|
358
|
+
</Menu>
|
|
359
|
+
);
|
|
360
|
+
const item = screen.getByText('x').closest('[role="menuitem"]');
|
|
361
|
+
expect(item?.className).toContain('ring-purple-500');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ─── MenuSkeleton — standalone tests (no Menu wrapper) ───────────
|
|
367
|
+
|
|
368
|
+
describe('<MenuSkeleton>', () => {
|
|
369
|
+
it('renders the default 3 placeholder rows', () => {
|
|
370
|
+
const { container } = render(<MenuSkeleton />);
|
|
371
|
+
// Each row is a div with the inline pulse classes; we match on
|
|
372
|
+
// the unique "h-3 flex-1 rounded-sm bg-neutral-200" inline bone.
|
|
373
|
+
const rows = container.querySelectorAll('.h-3.flex-1.rounded-sm');
|
|
374
|
+
expect(rows.length).toBe(3);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('renders custom count', () => {
|
|
378
|
+
const { container } = render(<MenuSkeleton count={7} />);
|
|
379
|
+
const rows = container.querySelectorAll('.h-3.flex-1.rounded-sm');
|
|
380
|
+
expect(rows.length).toBe(7);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('renders a skeleton heading when withHeading=true', () => {
|
|
384
|
+
const { container } = render(<MenuSkeleton withHeading />);
|
|
385
|
+
// Heading is the .uppercase wrapper (matches the MenuLabel style).
|
|
386
|
+
expect(container.querySelector('.uppercase')).toBeTruthy();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('omits the heading by default', () => {
|
|
390
|
+
const { container } = render(<MenuSkeleton />);
|
|
391
|
+
expect(container.querySelector('.uppercase')).toBeNull();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('has aria-hidden="true" (decorative)', () => {
|
|
395
|
+
const { container } = render(<MenuSkeleton />);
|
|
396
|
+
expect(container.firstElementChild?.getAttribute('aria-hidden')).toBe(
|
|
397
|
+
'true'
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('uses animate-pulse class (motion-reduce-safe)', () => {
|
|
402
|
+
const { container } = render(<MenuSkeleton />);
|
|
403
|
+
expect(container.firstElementChild?.className).toContain('animate-pulse');
|
|
404
|
+
expect(container.firstElementChild?.className).toContain(
|
|
405
|
+
'motion-reduce:animate-none'
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
});
|