paris 0.19.0 → 0.20.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 +10 -0
- package/package.json +17 -2
- package/src/stories/accordion/Accordion.test.tsx +140 -0
- package/src/stories/accordionselect/AccordionSelect.test.tsx +252 -0
- package/src/stories/avatar/Avatar.test.tsx +77 -0
- package/src/stories/button/Button.test.tsx +266 -0
- package/src/stories/callout/Callout.test.tsx +79 -0
- package/src/stories/card/Card.test.tsx +81 -0
- package/src/stories/cardbutton/CardButton.test.tsx +174 -0
- package/src/stories/checkbox/Checkbox.test.tsx +531 -0
- package/src/stories/combobox/Combobox.test.tsx +164 -0
- package/src/stories/dialog/Dialog.module.scss +2 -2
- package/src/stories/dialog/Dialog.test.tsx +244 -0
- package/src/stories/drawer/Drawer.module.scss +2 -2
- package/src/stories/drawer/Drawer.test.tsx +259 -0
- package/src/stories/field/Field.test.tsx +146 -0
- package/src/stories/icon/Icon.test.tsx +59 -0
- package/src/stories/informationaltooltip/InformationalTooltip.test.tsx +178 -0
- package/src/stories/input/Input.test.tsx +174 -0
- package/src/stories/markdown/Markdown.test.tsx +228 -0
- package/src/stories/markdowneditor/FixedToolbar.tsx +44 -14
- package/src/stories/markdowneditor/LinkPopover.module.scss +1 -1
- package/src/stories/markdowneditor/MarkdownEditor.stories.tsx +4 -1
- package/src/stories/markdowneditor/MarkdownEditor.test.tsx +115 -0
- package/src/stories/markdowneditor/MarkdownEditor.tsx +11 -1
- package/src/stories/markdowneditor/MarkdownEditorContext.tsx +3 -0
- package/src/stories/markdowneditor/index.ts +1 -0
- package/src/stories/menu/Menu.module.scss +1 -1
- package/src/stories/menu/Menu.test.tsx +211 -0
- package/src/stories/pagination/usePagination.test.ts +259 -0
- package/src/stories/popover/Popover.test.tsx +152 -0
- package/src/stories/select/Select.module.scss +2 -1
- package/src/stories/select/Select.test.tsx +233 -0
- package/src/stories/styledlink/StyledLink.test.tsx +59 -0
- package/src/stories/table/Table.test.tsx +156 -0
- package/src/stories/tabs/Tabs.module.scss +1 -1
- package/src/stories/tabs/Tabs.test.tsx +167 -0
- package/src/stories/tag/Tag.test.tsx +90 -0
- package/src/stories/text/Text.test.tsx +81 -0
- package/src/stories/textarea/TextArea.test.tsx +147 -0
- package/src/stories/theme/themes.ts +16 -0
- package/src/stories/tilt/Tilt.test.tsx +203 -0
- package/src/stories/toast/Toast.test.tsx +86 -0
- package/src/stories/utility/Dropdown.module.scss +1 -1
- package/src/stories/utility/Utility.test.tsx +96 -0
- package/src/test/render.tsx +20 -0
- package/src/test/setup.ts +32 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { render, screen } from '../../test/render';
|
|
3
|
+
import { Checkbox } from './Checkbox';
|
|
4
|
+
|
|
5
|
+
// Helper wrapper for controlled checkbox behavior
|
|
6
|
+
function ControlledCheckbox({
|
|
7
|
+
defaultChecked = false,
|
|
8
|
+
...props
|
|
9
|
+
}: Omit<React.ComponentProps<typeof Checkbox>, 'checked'> & { defaultChecked?: boolean }) {
|
|
10
|
+
const [checked, setChecked] = useState(defaultChecked);
|
|
11
|
+
return <Checkbox {...props} checked={checked} onChange={(val) => setChecked(!!val)} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Checkbox', () => {
|
|
15
|
+
// ─── Rendering ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('rendering', () => {
|
|
18
|
+
it('renders with default kind', () => {
|
|
19
|
+
render(<Checkbox checked={false}>Accept terms</Checkbox>);
|
|
20
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText('Accept terms')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders unchecked by default', () => {
|
|
25
|
+
render(<Checkbox checked={false}>Label</Checkbox>);
|
|
26
|
+
const checkbox = screen.getByRole('checkbox');
|
|
27
|
+
expect(checkbox).not.toBeChecked();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders checked when checked prop is true', () => {
|
|
31
|
+
render(<Checkbox checked={true}>Label</Checkbox>);
|
|
32
|
+
const checkbox = screen.getByRole('checkbox');
|
|
33
|
+
expect(checkbox).toBeChecked();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders label text as children', () => {
|
|
37
|
+
render(<Checkbox checked={false}>My label text</Checkbox>);
|
|
38
|
+
expect(screen.getByText('My label text')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders ReactNode children', () => {
|
|
42
|
+
render(
|
|
43
|
+
<Checkbox checked={false}>
|
|
44
|
+
<span data-testid="custom-label">Custom element</span>
|
|
45
|
+
</Checkbox>,
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getByTestId('custom-label')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText('Custom element')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders without children', () => {
|
|
52
|
+
render(<Checkbox checked={false} />);
|
|
53
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── Interaction ─────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe('interaction', () => {
|
|
60
|
+
it('calls onChange when clicked', async () => {
|
|
61
|
+
const handleChange = vi.fn();
|
|
62
|
+
const { user } = render(
|
|
63
|
+
<Checkbox checked={false} onChange={handleChange}>
|
|
64
|
+
Toggle me
|
|
65
|
+
</Checkbox>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await user.click(screen.getByRole('checkbox'));
|
|
69
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(handleChange).toHaveBeenCalledWith(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('calls onChange with false when unchecking', async () => {
|
|
74
|
+
const handleChange = vi.fn();
|
|
75
|
+
const { user } = render(
|
|
76
|
+
<Checkbox checked={true} onChange={handleChange}>
|
|
77
|
+
Toggle me
|
|
78
|
+
</Checkbox>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await user.click(screen.getByRole('checkbox'));
|
|
82
|
+
expect(handleChange).toHaveBeenCalledWith(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('toggles checked state in a controlled component', async () => {
|
|
86
|
+
const { user } = render(<ControlledCheckbox>Toggle me</ControlledCheckbox>);
|
|
87
|
+
|
|
88
|
+
const checkbox = screen.getByRole('checkbox');
|
|
89
|
+
expect(checkbox).not.toBeChecked();
|
|
90
|
+
|
|
91
|
+
await user.click(checkbox);
|
|
92
|
+
expect(checkbox).toBeChecked();
|
|
93
|
+
|
|
94
|
+
await user.click(checkbox);
|
|
95
|
+
expect(checkbox).not.toBeChecked();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('clicking the label toggles the checkbox', async () => {
|
|
99
|
+
const handleChange = vi.fn();
|
|
100
|
+
const { user } = render(
|
|
101
|
+
<Checkbox checked={false} onChange={handleChange}>
|
|
102
|
+
Click this label
|
|
103
|
+
</Checkbox>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// The label wraps the checkbox, so clicking text should also trigger
|
|
107
|
+
await user.click(screen.getByText('Click this label'));
|
|
108
|
+
expect(handleChange).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ─── Disabled ────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
describe('disabled', () => {
|
|
115
|
+
it('applies disabled styling class', () => {
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<Checkbox checked={false} disabled>
|
|
118
|
+
Disabled
|
|
119
|
+
</Checkbox>,
|
|
120
|
+
);
|
|
121
|
+
const label = container.querySelector('label');
|
|
122
|
+
expect(label).toHaveClass('disabled');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('sets data-disabled on the checkbox root', () => {
|
|
126
|
+
render(
|
|
127
|
+
<Checkbox checked={false} disabled>
|
|
128
|
+
Disabled
|
|
129
|
+
</Checkbox>,
|
|
130
|
+
);
|
|
131
|
+
const checkbox = screen.getByRole('checkbox');
|
|
132
|
+
expect(checkbox).toHaveAttribute('data-disabled', 'true');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('sets data-disabled on the switch when kind is switch', () => {
|
|
136
|
+
render(
|
|
137
|
+
<Checkbox kind="switch" checked={false} disabled>
|
|
138
|
+
Disabled switch
|
|
139
|
+
</Checkbox>,
|
|
140
|
+
);
|
|
141
|
+
const switchEl = screen.getByRole('switch');
|
|
142
|
+
expect(switchEl).toHaveAttribute('data-disabled', 'true');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── Kinds ───────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
describe('kind variants', () => {
|
|
149
|
+
describe('default kind', () => {
|
|
150
|
+
it('renders a Radix checkbox with default class', () => {
|
|
151
|
+
const { container } = render(
|
|
152
|
+
<Checkbox kind="default" checked={false}>
|
|
153
|
+
Default
|
|
154
|
+
</Checkbox>,
|
|
155
|
+
);
|
|
156
|
+
const root = container.querySelector('[class*="root"]');
|
|
157
|
+
expect(root).toHaveClass('default');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('shows label text next to checkbox', () => {
|
|
161
|
+
render(
|
|
162
|
+
<Checkbox kind="default" checked={false}>
|
|
163
|
+
Default label
|
|
164
|
+
</Checkbox>,
|
|
165
|
+
);
|
|
166
|
+
expect(screen.getByText('Default label')).toBeInTheDocument();
|
|
167
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('renders an SVG check icon when checked', () => {
|
|
171
|
+
const { container } = render(
|
|
172
|
+
<Checkbox kind="default" checked={true}>
|
|
173
|
+
Checked
|
|
174
|
+
</Checkbox>,
|
|
175
|
+
);
|
|
176
|
+
const svg = container.querySelector('svg');
|
|
177
|
+
expect(svg).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('surface kind', () => {
|
|
182
|
+
it('renders a Radix checkbox with surface class', () => {
|
|
183
|
+
const { container } = render(
|
|
184
|
+
<Checkbox kind="surface" checked={false}>
|
|
185
|
+
Surface
|
|
186
|
+
</Checkbox>,
|
|
187
|
+
);
|
|
188
|
+
const root = container.querySelector('[class*="root"]');
|
|
189
|
+
expect(root).toHaveClass('surface');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('shows children inside the Radix root (not outside)', () => {
|
|
193
|
+
render(
|
|
194
|
+
<Checkbox kind="surface" checked={false}>
|
|
195
|
+
Surface label
|
|
196
|
+
</Checkbox>,
|
|
197
|
+
);
|
|
198
|
+
expect(screen.getByText('Surface label')).toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not show label outside root when kind is surface', () => {
|
|
202
|
+
const { container } = render(
|
|
203
|
+
<Checkbox kind="surface" checked={false}>
|
|
204
|
+
Surface label
|
|
205
|
+
</Checkbox>,
|
|
206
|
+
);
|
|
207
|
+
// In surface mode, the label text is inside the Radix root, not adjacent
|
|
208
|
+
const root = container.querySelector('[class*="root"]');
|
|
209
|
+
expect(root).toContainElement(screen.getByText('Surface label'));
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('panel kind', () => {
|
|
214
|
+
it('renders a Radix checkbox with panel class', () => {
|
|
215
|
+
const { container } = render(
|
|
216
|
+
<Checkbox kind="panel" checked={false}>
|
|
217
|
+
Panel
|
|
218
|
+
</Checkbox>,
|
|
219
|
+
);
|
|
220
|
+
const root = container.querySelector('[class*="root"]');
|
|
221
|
+
expect(root).toHaveClass('panel');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('shows children inside the Radix root', () => {
|
|
225
|
+
const { container } = render(
|
|
226
|
+
<Checkbox kind="panel" checked={false}>
|
|
227
|
+
Panel label
|
|
228
|
+
</Checkbox>,
|
|
229
|
+
);
|
|
230
|
+
const root = container.querySelector('[class*="root"]');
|
|
231
|
+
expect(root).toContainElement(screen.getByText('Panel label'));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('renders a box element when unchecked', () => {
|
|
235
|
+
const { container } = render(
|
|
236
|
+
<Checkbox kind="panel" checked={false}>
|
|
237
|
+
Panel
|
|
238
|
+
</Checkbox>,
|
|
239
|
+
);
|
|
240
|
+
const box = container.querySelector('.box');
|
|
241
|
+
expect(box).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('switch kind', () => {
|
|
246
|
+
it('renders a switch element instead of a Radix checkbox', () => {
|
|
247
|
+
render(
|
|
248
|
+
<Checkbox kind="switch" checked={false}>
|
|
249
|
+
Switch
|
|
250
|
+
</Checkbox>,
|
|
251
|
+
);
|
|
252
|
+
// HeadlessUI Switch renders a button with role="switch"
|
|
253
|
+
expect(screen.getByRole('switch')).toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('does not render a Radix checkbox role', () => {
|
|
257
|
+
render(
|
|
258
|
+
<Checkbox kind="switch" checked={false}>
|
|
259
|
+
Switch
|
|
260
|
+
</Checkbox>,
|
|
261
|
+
);
|
|
262
|
+
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('shows label text', () => {
|
|
266
|
+
render(
|
|
267
|
+
<Checkbox kind="switch" checked={false}>
|
|
268
|
+
Switch label
|
|
269
|
+
</Checkbox>,
|
|
270
|
+
);
|
|
271
|
+
expect(screen.getByText('Switch label')).toBeInTheDocument();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('toggles switch state on click', async () => {
|
|
275
|
+
const handleChange = vi.fn();
|
|
276
|
+
const { user } = render(
|
|
277
|
+
<Checkbox kind="switch" checked={false} onChange={handleChange}>
|
|
278
|
+
Switch
|
|
279
|
+
</Checkbox>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await user.click(screen.getByRole('switch'));
|
|
283
|
+
expect(handleChange).toHaveBeenCalledWith(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('renders knob element', () => {
|
|
287
|
+
const { container } = render(
|
|
288
|
+
<Checkbox kind="switch" checked={false}>
|
|
289
|
+
Switch
|
|
290
|
+
</Checkbox>,
|
|
291
|
+
);
|
|
292
|
+
const knob = container.querySelector('[class*="knob"]');
|
|
293
|
+
expect(knob).toBeInTheDocument();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('applies knobChecked class when checked', () => {
|
|
297
|
+
const { container } = render(
|
|
298
|
+
<Checkbox kind="switch" checked={true}>
|
|
299
|
+
Switch
|
|
300
|
+
</Checkbox>,
|
|
301
|
+
);
|
|
302
|
+
const knob = container.querySelector('[class*="knob"]');
|
|
303
|
+
expect(knob).toHaveClass('knobChecked');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('does not apply knobChecked class when unchecked', () => {
|
|
307
|
+
const { container } = render(
|
|
308
|
+
<Checkbox kind="switch" checked={false}>
|
|
309
|
+
Switch
|
|
310
|
+
</Checkbox>,
|
|
311
|
+
);
|
|
312
|
+
const knob = container.querySelector('[class*="knob"]');
|
|
313
|
+
expect(knob).not.toHaveClass('knobChecked');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ─── Hide Label ──────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
describe('hideLabel', () => {
|
|
321
|
+
it('visually hides the label when hideLabel is true', () => {
|
|
322
|
+
const { container } = render(
|
|
323
|
+
<Checkbox kind="default" checked={false} hideLabel>
|
|
324
|
+
Hidden label
|
|
325
|
+
</Checkbox>,
|
|
326
|
+
);
|
|
327
|
+
// VisuallyHidden uses Ariakit's VisuallyHidden which renders content off-screen
|
|
328
|
+
// The text should still be in the DOM for accessibility
|
|
329
|
+
expect(screen.getByText('Hidden label')).toBeInTheDocument();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('shows label when hideLabel is false', () => {
|
|
333
|
+
render(
|
|
334
|
+
<Checkbox kind="default" checked={false} hideLabel={false}>
|
|
335
|
+
Visible label
|
|
336
|
+
</Checkbox>,
|
|
337
|
+
);
|
|
338
|
+
expect(screen.getByText('Visible label')).toBeInTheDocument();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('hides the label for switch kind', () => {
|
|
342
|
+
render(
|
|
343
|
+
<Checkbox kind="switch" checked={false} hideLabel>
|
|
344
|
+
Hidden switch label
|
|
345
|
+
</Checkbox>,
|
|
346
|
+
);
|
|
347
|
+
// Still accessible in DOM
|
|
348
|
+
expect(screen.getByText('Hidden switch label')).toBeInTheDocument();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ─── className forwarding ────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
describe('className forwarding', () => {
|
|
355
|
+
it('forwards className to the label container', () => {
|
|
356
|
+
const { container } = render(
|
|
357
|
+
<Checkbox checked={false} className="custom-class">
|
|
358
|
+
Label
|
|
359
|
+
</Checkbox>,
|
|
360
|
+
);
|
|
361
|
+
const label = container.querySelector('label');
|
|
362
|
+
expect(label).toHaveClass('custom-class');
|
|
363
|
+
expect(label).toHaveClass('container');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('preserves container class when custom className is added', () => {
|
|
367
|
+
const { container } = render(
|
|
368
|
+
<Checkbox checked={false} className="my-class">
|
|
369
|
+
Label
|
|
370
|
+
</Checkbox>,
|
|
371
|
+
);
|
|
372
|
+
const label = container.querySelector('label');
|
|
373
|
+
expect(label).toHaveClass('container');
|
|
374
|
+
expect(label).toHaveClass('my-class');
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// ─── Checked state styling ───────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
describe('checked state styling', () => {
|
|
381
|
+
it('applies checked class to container when checked', () => {
|
|
382
|
+
const { container } = render(<Checkbox checked={true}>Checked</Checkbox>);
|
|
383
|
+
const label = container.querySelector('label');
|
|
384
|
+
expect(label).toHaveClass('checked');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('does not apply checked class to container when unchecked', () => {
|
|
388
|
+
const { container } = render(<Checkbox checked={false}>Unchecked</Checkbox>);
|
|
389
|
+
const label = container.querySelector('label');
|
|
390
|
+
expect(label).not.toHaveClass('checked');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ─── Accessibility ───────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
describe('accessibility', () => {
|
|
397
|
+
it('has proper role="checkbox" for default kind', () => {
|
|
398
|
+
render(<Checkbox checked={false}>Accessible</Checkbox>);
|
|
399
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('has proper role="switch" for switch kind', () => {
|
|
403
|
+
render(
|
|
404
|
+
<Checkbox kind="switch" checked={false}>
|
|
405
|
+
Switch
|
|
406
|
+
</Checkbox>,
|
|
407
|
+
);
|
|
408
|
+
expect(screen.getByRole('switch')).toBeInTheDocument();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('has aria-checked false when unchecked', () => {
|
|
412
|
+
render(<Checkbox checked={false}>Label</Checkbox>);
|
|
413
|
+
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-checked', 'false');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('has aria-checked true when checked', () => {
|
|
417
|
+
render(<Checkbox checked={true}>Label</Checkbox>);
|
|
418
|
+
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-checked', 'true');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('sets aria-details when children is a string', () => {
|
|
422
|
+
render(<Checkbox checked={false}>String label</Checkbox>);
|
|
423
|
+
const checkbox = screen.getByRole('checkbox');
|
|
424
|
+
expect(checkbox).toHaveAttribute('aria-details', 'String label');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('does not set aria-details when children is not a string', () => {
|
|
428
|
+
render(
|
|
429
|
+
<Checkbox checked={false}>
|
|
430
|
+
<span>Non-string label</span>
|
|
431
|
+
</Checkbox>,
|
|
432
|
+
);
|
|
433
|
+
const checkbox = screen.getByRole('checkbox');
|
|
434
|
+
expect(checkbox).not.toHaveAttribute('aria-details');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('associates label with checkbox via htmlFor and id', () => {
|
|
438
|
+
const { container } = render(<Checkbox checked={false}>Label</Checkbox>);
|
|
439
|
+
const label = container.querySelector('label');
|
|
440
|
+
const checkbox = screen.getByRole('checkbox');
|
|
441
|
+
expect(label).toHaveAttribute('for');
|
|
442
|
+
expect(checkbox).toHaveAttribute('id', label?.getAttribute('for'));
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('associates label with switch via htmlFor and id', () => {
|
|
446
|
+
const { container } = render(
|
|
447
|
+
<Checkbox kind="switch" checked={false}>
|
|
448
|
+
Switch label
|
|
449
|
+
</Checkbox>,
|
|
450
|
+
);
|
|
451
|
+
const label = container.querySelector('label');
|
|
452
|
+
const switchEl = screen.getByRole('switch');
|
|
453
|
+
expect(label).toHaveAttribute('for');
|
|
454
|
+
expect(switchEl).toHaveAttribute('id', label?.getAttribute('for'));
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// ─── Spread props ────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
describe('spread props', () => {
|
|
461
|
+
it('forwards additional HTML attributes to the label', () => {
|
|
462
|
+
const { container } = render(
|
|
463
|
+
<Checkbox checked={false} data-testid="checkbox-label" title="My checkbox">
|
|
464
|
+
Label
|
|
465
|
+
</Checkbox>,
|
|
466
|
+
);
|
|
467
|
+
const label = container.querySelector('label');
|
|
468
|
+
expect(label).toHaveAttribute('data-testid', 'checkbox-label');
|
|
469
|
+
expect(label).toHaveAttribute('title', 'My checkbox');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('forwards style prop to the label', () => {
|
|
473
|
+
const { container } = render(
|
|
474
|
+
<Checkbox checked={false} style={{ marginTop: '10px' }}>
|
|
475
|
+
Label
|
|
476
|
+
</Checkbox>,
|
|
477
|
+
);
|
|
478
|
+
const label = container.querySelector('label');
|
|
479
|
+
expect(label).toHaveStyle({ marginTop: '10px' });
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ─── Edge cases ──────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
describe('edge cases', () => {
|
|
486
|
+
it('handles multiple rapid toggles', async () => {
|
|
487
|
+
const handleChange = vi.fn();
|
|
488
|
+
const { user } = render(<ControlledCheckbox onChange={handleChange}>Rapid</ControlledCheckbox>);
|
|
489
|
+
|
|
490
|
+
const checkbox = screen.getByRole('checkbox');
|
|
491
|
+
await user.click(checkbox);
|
|
492
|
+
await user.click(checkbox);
|
|
493
|
+
await user.click(checkbox);
|
|
494
|
+
|
|
495
|
+
expect(checkbox).toBeChecked();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('renders with no onChange handler without crashing', () => {
|
|
499
|
+
expect(() => {
|
|
500
|
+
render(<Checkbox checked={false}>No handler</Checkbox>);
|
|
501
|
+
}).not.toThrow();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('renders with checked=true and no onChange without crashing', () => {
|
|
505
|
+
expect(() => {
|
|
506
|
+
render(<Checkbox checked={true}>Static checked</Checkbox>);
|
|
507
|
+
}).not.toThrow();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('handles empty string children', () => {
|
|
511
|
+
render(<Checkbox checked={false}>{''}</Checkbox>);
|
|
512
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('handles number children', () => {
|
|
516
|
+
render(<Checkbox checked={false}>{42}</Checkbox>);
|
|
517
|
+
expect(screen.getByText('42')).toBeInTheDocument();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('applies both disabled and checked classes simultaneously', () => {
|
|
521
|
+
const { container } = render(
|
|
522
|
+
<Checkbox checked={true} disabled>
|
|
523
|
+
Both
|
|
524
|
+
</Checkbox>,
|
|
525
|
+
);
|
|
526
|
+
const label = container.querySelector('label');
|
|
527
|
+
expect(label).toHaveClass('disabled');
|
|
528
|
+
expect(label).toHaveClass('checked');
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|