tharaday 0.7.2 → 0.7.3

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.
Files changed (44) hide show
  1. package/.storybook/main.ts +1 -1
  2. package/.storybook/preview.ts +0 -2
  3. package/.storybook/vitest.setup.ts +2 -0
  4. package/dist/ds.css +1 -1
  5. package/dist/ds.js +873 -805
  6. package/dist/ds.umd.cjs +1 -1
  7. package/dist/src/components/Tree/Tree.d.ts +1 -1
  8. package/dist/src/components/Tree/Tree.stories.d.ts +1 -1
  9. package/dist/src/components/Tree/TreeItem.d.ts +1 -1
  10. package/dist/src/components/Tree/TreeItem.types.d.ts +6 -0
  11. package/package.json +8 -1
  12. package/src/components/Accordion/Accordion.test.tsx +82 -0
  13. package/src/components/Avatar/Avatar.test.tsx +36 -0
  14. package/src/components/Badge/Badge.test.tsx +15 -0
  15. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +96 -0
  16. package/src/components/Checkbox/Checkbox.module.css +7 -7
  17. package/src/components/Checkbox/Checkbox.test.tsx +68 -0
  18. package/src/components/Dropdown/Dropdown.test.tsx +104 -0
  19. package/src/components/Input/Input.test.tsx +61 -0
  20. package/src/components/List/List.module.css +12 -12
  21. package/src/components/List/List.test.tsx +46 -0
  22. package/src/components/Modal/Modal.module.css +5 -5
  23. package/src/components/Modal/Modal.test.tsx +86 -0
  24. package/src/components/NavBar/NavBar.module.css +3 -3
  25. package/src/components/Notification/Notification.module.css +6 -6
  26. package/src/components/Notification/Notification.test.tsx +38 -0
  27. package/src/components/Pagination/Pagination.test.tsx +70 -0
  28. package/src/components/ProgressBar/ProgressBar.test.tsx +58 -0
  29. package/src/components/RadioButton/RadioButton.test.tsx +51 -0
  30. package/src/components/Select/Select.test.tsx +64 -0
  31. package/src/components/Slider/Slider.test.tsx +49 -0
  32. package/src/components/Stepper/Step.module.css +2 -2
  33. package/src/components/Stepper/Stepper.test.tsx +51 -0
  34. package/src/components/Switch/Switch.test.tsx +53 -0
  35. package/src/components/Table/Table.test.tsx +78 -0
  36. package/src/components/Tabs/Tabs.test.tsx +83 -0
  37. package/src/components/Textarea/Textarea.test.tsx +56 -0
  38. package/src/components/Tree/Tree.test.tsx +116 -0
  39. package/src/components/Tree/Tree.tsx +65 -1
  40. package/src/components/Tree/TreeItem.module.css +20 -26
  41. package/src/components/Tree/TreeItem.tsx +144 -79
  42. package/src/components/Tree/TreeItem.types.ts +6 -0
  43. package/src/styles/semantic.css +3 -0
  44. package/src/styles/tokens.css +15 -0
@@ -0,0 +1,58 @@
1
+ import { render, screen } from '@testing-library/react';
2
+
3
+ import { ProgressBar } from './ProgressBar.tsx';
4
+
5
+ describe('ProgressBar', () => {
6
+ it('renders a progressbar', () => {
7
+ render(<ProgressBar value={50} />);
8
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
9
+ });
10
+
11
+ it('sets aria-valuenow to the current value', () => {
12
+ render(<ProgressBar value={40} />);
13
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '40');
14
+ });
15
+
16
+ it('sets aria-valuemin to 0', () => {
17
+ render(<ProgressBar value={50} />);
18
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemin', '0');
19
+ });
20
+
21
+ it('sets aria-valuemax to the max prop', () => {
22
+ render(<ProgressBar value={5} max={10} />);
23
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemax', '10');
24
+ });
25
+
26
+ it('clamps value to max', () => {
27
+ render(<ProgressBar value={150} max={100} />);
28
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '100');
29
+ });
30
+
31
+ it('clamps value to 0', () => {
32
+ render(<ProgressBar value={-10} />);
33
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
34
+ });
35
+
36
+ it('renders label text', () => {
37
+ render(<ProgressBar value={50} label="Uploading" />);
38
+ expect(screen.getByText('Uploading')).toBeInTheDocument();
39
+ });
40
+
41
+ it('renders percentage text when showLabel is true', () => {
42
+ render(<ProgressBar value={75} showLabel />);
43
+ expect(screen.getByText('75%')).toBeInTheDocument();
44
+ });
45
+
46
+ it('uses aria-label="Progress" when no label is provided', () => {
47
+ render(<ProgressBar value={50} />);
48
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-label', 'Progress');
49
+ });
50
+
51
+ it('uses aria-labelledby pointing to the label when label is provided', () => {
52
+ render(<ProgressBar value={50} label="Loading" />);
53
+ const pb = screen.getByRole('progressbar');
54
+ const labelId = pb.getAttribute('aria-labelledby');
55
+ expect(labelId).toBeTruthy();
56
+ expect(document.getElementById(labelId!)).toHaveTextContent('Loading');
57
+ });
58
+ });
@@ -0,0 +1,51 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { RadioButton } from './RadioButton.tsx';
5
+
6
+ describe('RadioButton', () => {
7
+ it('renders a radio input', () => {
8
+ render(<RadioButton />);
9
+ expect(screen.getByRole('radio')).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders label and links it to the input', () => {
13
+ render(<RadioButton label="Option A" />);
14
+ expect(screen.getByLabelText('Option A')).toBeInTheDocument();
15
+ });
16
+
17
+ it('renders helper text', () => {
18
+ render(<RadioButton helperText="Recommended" />);
19
+ expect(screen.getByText('Recommended')).toBeInTheDocument();
20
+ });
21
+
22
+ it('links helper text via aria-describedby', () => {
23
+ render(<RadioButton helperText="Hint" />);
24
+ const input = screen.getByRole('radio');
25
+ const helperId = input.getAttribute('aria-describedby');
26
+ expect(helperId).toBeTruthy();
27
+ expect(document.getElementById(helperId!)).toHaveTextContent('Hint');
28
+ });
29
+
30
+ it('is disabled when disabled prop is set', () => {
31
+ render(<RadioButton disabled />);
32
+ expect(screen.getByRole('radio')).toBeDisabled();
33
+ });
34
+
35
+ it('is checked when defaultChecked', () => {
36
+ render(<RadioButton defaultChecked />);
37
+ expect(screen.getByRole('radio')).toBeChecked();
38
+ });
39
+
40
+ it('calls onChange when selected', async () => {
41
+ const onChange = vi.fn();
42
+ render(<RadioButton onChange={onChange} />);
43
+ await userEvent.click(screen.getByRole('radio'));
44
+ expect(onChange).toHaveBeenCalledTimes(1);
45
+ });
46
+
47
+ it('sets aria-invalid when error is true', () => {
48
+ render(<RadioButton error />);
49
+ expect(screen.getByRole('radio')).toHaveAttribute('aria-invalid');
50
+ });
51
+ });
@@ -0,0 +1,64 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { Select } from './Select.tsx';
5
+
6
+ const options = [
7
+ { value: 'a', label: 'Option A' },
8
+ { value: 'b', label: 'Option B' },
9
+ { value: 'c', label: 'Option C', disabled: true },
10
+ ];
11
+
12
+ describe('Select', () => {
13
+ it('renders a combobox', () => {
14
+ render(<Select options={options} />);
15
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders label and links it to the select', () => {
19
+ render(<Select options={options} label="Country" />);
20
+ expect(screen.getByLabelText('Country')).toBeInTheDocument();
21
+ });
22
+
23
+ it('renders options from the options prop', () => {
24
+ render(<Select options={options} />);
25
+ expect(screen.getByRole('option', { name: 'Option A' })).toBeInTheDocument();
26
+ expect(screen.getByRole('option', { name: 'Option B' })).toBeInTheDocument();
27
+ });
28
+
29
+ it('renders children when options prop is not provided', () => {
30
+ render(
31
+ <Select>
32
+ <option value="x">Choice X</option>
33
+ </Select>
34
+ );
35
+ expect(screen.getByRole('option', { name: 'Choice X' })).toBeInTheDocument();
36
+ });
37
+
38
+ it('renders disabled options', () => {
39
+ render(<Select options={options} />);
40
+ expect(screen.getByRole('option', { name: 'Option C' })).toBeDisabled();
41
+ });
42
+
43
+ it('renders helper text', () => {
44
+ render(<Select options={options} helperText="Pick one" />);
45
+ expect(screen.getByText('Pick one')).toBeInTheDocument();
46
+ });
47
+
48
+ it('is disabled when disabled prop is set', () => {
49
+ render(<Select options={options} disabled />);
50
+ expect(screen.getByRole('combobox')).toBeDisabled();
51
+ });
52
+
53
+ it('calls onChange when selection changes', async () => {
54
+ const onChange = vi.fn();
55
+ render(<Select options={options} onChange={onChange} />);
56
+ await userEvent.selectOptions(screen.getByRole('combobox'), 'Option B');
57
+ expect(onChange).toHaveBeenCalled();
58
+ });
59
+
60
+ it('sets aria-invalid when error is true', () => {
61
+ render(<Select options={options} error />);
62
+ expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid');
63
+ });
64
+ });
@@ -0,0 +1,49 @@
1
+ import { render, screen } from '@testing-library/react';
2
+
3
+ import { Slider } from './Slider.tsx';
4
+
5
+ describe('Slider', () => {
6
+ it('renders a range input', () => {
7
+ render(<Slider />);
8
+ expect(screen.getByRole('slider')).toBeInTheDocument();
9
+ });
10
+
11
+ it('renders label and links it to the slider', () => {
12
+ render(<Slider label="Volume" />);
13
+ expect(screen.getByLabelText('Volume')).toBeInTheDocument();
14
+ });
15
+
16
+ it('uses default min=0 and max=100', () => {
17
+ render(<Slider />);
18
+ const slider = screen.getByRole('slider');
19
+ expect(slider).toHaveAttribute('min', '0');
20
+ expect(slider).toHaveAttribute('max', '100');
21
+ });
22
+
23
+ it('forwards min and max props', () => {
24
+ render(<Slider min={10} max={50} />);
25
+ const slider = screen.getByRole('slider');
26
+ expect(slider).toHaveAttribute('min', '10');
27
+ expect(slider).toHaveAttribute('max', '50');
28
+ });
29
+
30
+ it('renders helper text', () => {
31
+ render(<Slider helperText="Adjust the level" />);
32
+ expect(screen.getByText('Adjust the level')).toBeInTheDocument();
33
+ });
34
+
35
+ it('shows current value when showValue is true', () => {
36
+ render(<Slider defaultValue={30} showValue />);
37
+ expect(screen.getByText('30')).toBeInTheDocument();
38
+ });
39
+
40
+ it('is disabled when disabled prop is set', () => {
41
+ render(<Slider disabled />);
42
+ expect(screen.getByRole('slider')).toBeDisabled();
43
+ });
44
+
45
+ it('renders two sliders for a range (dual) value', () => {
46
+ render(<Slider defaultValue={[20, 80]} />);
47
+ expect(screen.getAllByRole('slider')).toHaveLength(2);
48
+ });
49
+ });
@@ -95,13 +95,13 @@
95
95
  }
96
96
 
97
97
  [data-orientation='horizontal'] .connector {
98
- height: 2px;
98
+ height: var(--ds-border-width-2);
99
99
  flex: 1;
100
100
  margin-left: var(--ds-space-3);
101
101
  }
102
102
 
103
103
  [data-orientation='vertical'] .connector {
104
- width: 2px;
104
+ width: var(--ds-border-width-2);
105
105
  height: var(--ds-space-8);
106
106
  margin-left: calc(var(--stepper-marker-size) / 2 - 1px);
107
107
  margin-top: var(--ds-space-2);
@@ -0,0 +1,51 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { Stepper } from './Stepper.tsx';
5
+
6
+ const steps = [
7
+ { id: 'account', label: 'Account' },
8
+ { id: 'profile', label: 'Profile' },
9
+ { id: 'review', label: 'Review' },
10
+ ];
11
+
12
+ describe('Stepper', () => {
13
+ it('renders an ordered list', () => {
14
+ render(<Stepper steps={steps} currentStep="account" />);
15
+ expect(screen.getByRole('list')).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders all step labels', () => {
19
+ render(<Stepper steps={steps} currentStep="account" />);
20
+ expect(screen.getByText('Account')).toBeInTheDocument();
21
+ expect(screen.getByText('Profile')).toBeInTheDocument();
22
+ expect(screen.getByText('Review')).toBeInTheDocument();
23
+ });
24
+
25
+ it('uses "Progress" as default aria-label', () => {
26
+ render(<Stepper steps={steps} currentStep="account" />);
27
+ expect(screen.getByRole('list')).toHaveAttribute('aria-label', 'Progress');
28
+ });
29
+
30
+ it('uses custom ariaLabel when provided', () => {
31
+ render(<Stepper steps={steps} currentStep="account" ariaLabel="Checkout steps" />);
32
+ expect(screen.getByRole('list')).toHaveAttribute('aria-label', 'Checkout steps');
33
+ });
34
+
35
+ it('does not render interactive buttons without onStepClick', () => {
36
+ render(<Stepper steps={steps} currentStep="account" />);
37
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
38
+ });
39
+
40
+ it('renders interactive buttons when onStepClick is provided', () => {
41
+ render(<Stepper steps={steps} currentStep="account" onStepClick={vi.fn()} />);
42
+ expect(screen.getAllByRole('button')).toHaveLength(steps.length);
43
+ });
44
+
45
+ it('calls onStepClick with the step when clicked', async () => {
46
+ const onStepClick = vi.fn();
47
+ render(<Stepper steps={steps} currentStep="account" onStepClick={onStepClick} />);
48
+ await userEvent.click(screen.getByRole('button', { name: /Profile/i }));
49
+ expect(onStepClick).toHaveBeenCalledWith(steps[1], 1);
50
+ });
51
+ });
@@ -0,0 +1,53 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { Switch } from './Switch.tsx';
5
+
6
+ describe('Switch', () => {
7
+ it('renders a switch', () => {
8
+ render(<Switch />);
9
+ expect(screen.getByRole('switch')).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders label and links it to the input', () => {
13
+ render(<Switch label="Dark mode" />);
14
+ expect(screen.getByLabelText('Dark mode')).toBeInTheDocument();
15
+ });
16
+
17
+ it('renders helper text', () => {
18
+ render(<Switch helperText="Toggle theme" />);
19
+ expect(screen.getByText('Toggle theme')).toBeInTheDocument();
20
+ });
21
+
22
+ it('links helper text via aria-describedby', () => {
23
+ render(<Switch helperText="Hint" />);
24
+ const input = screen.getByRole('switch');
25
+ const helperId = input.getAttribute('aria-describedby');
26
+ expect(helperId).toBeTruthy();
27
+ expect(document.getElementById(helperId!)).toHaveTextContent('Hint');
28
+ });
29
+
30
+ it('is disabled when disabled prop is set', () => {
31
+ render(<Switch disabled />);
32
+ expect(screen.getByRole('switch')).toBeDisabled();
33
+ });
34
+
35
+ it('is checked when defaultChecked', () => {
36
+ render(<Switch defaultChecked />);
37
+ expect(screen.getByRole('switch')).toBeChecked();
38
+ });
39
+
40
+ it('calls onChange when toggled', async () => {
41
+ const onChange = vi.fn();
42
+ render(<Switch onChange={onChange} />);
43
+ await userEvent.click(screen.getByRole('switch'));
44
+ expect(onChange).toHaveBeenCalledTimes(1);
45
+ });
46
+
47
+ it('does not call onChange when disabled', async () => {
48
+ const onChange = vi.fn();
49
+ render(<Switch disabled onChange={onChange} />);
50
+ await userEvent.click(screen.getByRole('switch'));
51
+ expect(onChange).not.toHaveBeenCalled();
52
+ });
53
+ });
@@ -0,0 +1,78 @@
1
+ import { render, screen } from '@testing-library/react';
2
+
3
+ import {
4
+ Table,
5
+ TableHeader,
6
+ TableBody,
7
+ TableFooter,
8
+ TableRow,
9
+ TableHead,
10
+ TableCell,
11
+ } from './Table.tsx';
12
+
13
+ const BasicTable = () => (
14
+ <Table>
15
+ <TableHeader>
16
+ <TableRow>
17
+ <TableHead>Name</TableHead>
18
+ <TableHead>Age</TableHead>
19
+ </TableRow>
20
+ </TableHeader>
21
+ <TableBody>
22
+ <TableRow>
23
+ <TableCell>Alice</TableCell>
24
+ <TableCell>30</TableCell>
25
+ </TableRow>
26
+ </TableBody>
27
+ </Table>
28
+ );
29
+
30
+ describe('Table', () => {
31
+ it('renders a table', () => {
32
+ render(<BasicTable />);
33
+ expect(screen.getByRole('table')).toBeInTheDocument();
34
+ });
35
+
36
+ it('renders column headers', () => {
37
+ render(<BasicTable />);
38
+ expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
39
+ expect(screen.getByRole('columnheader', { name: 'Age' })).toBeInTheDocument();
40
+ });
41
+
42
+ it('renders cell content', () => {
43
+ render(<BasicTable />);
44
+ expect(screen.getByRole('cell', { name: 'Alice' })).toBeInTheDocument();
45
+ expect(screen.getByRole('cell', { name: '30' })).toBeInTheDocument();
46
+ });
47
+
48
+ it('sets aria-busy when isLoading', () => {
49
+ render(
50
+ <Table isLoading>
51
+ <TableBody>
52
+ <TableRow>
53
+ <TableCell>Data</TableCell>
54
+ </TableRow>
55
+ </TableBody>
56
+ </Table>
57
+ );
58
+ expect(screen.getByRole('table')).toHaveAttribute('aria-busy', 'true');
59
+ });
60
+
61
+ it('does not set aria-busy when not loading', () => {
62
+ render(<BasicTable />);
63
+ expect(screen.getByRole('table')).not.toHaveAttribute('aria-busy');
64
+ });
65
+
66
+ it('renders a footer', () => {
67
+ render(
68
+ <Table>
69
+ <TableFooter>
70
+ <TableRow>
71
+ <TableCell>Total</TableCell>
72
+ </TableRow>
73
+ </TableFooter>
74
+ </Table>
75
+ );
76
+ expect(screen.getByRole('cell', { name: 'Total' })).toBeInTheDocument();
77
+ });
78
+ });
@@ -0,0 +1,83 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { Tabs } from './Tabs.tsx';
5
+
6
+ const items = [
7
+ { id: 'one', label: 'One', content: 'Panel One' },
8
+ { id: 'two', label: 'Two', content: 'Panel Two' },
9
+ { id: 'three', label: 'Three', content: 'Panel Three', disabled: true },
10
+ ];
11
+
12
+ describe('Tabs', () => {
13
+ it('renders a tablist with all tabs', () => {
14
+ render(<Tabs items={items} />);
15
+ expect(screen.getByRole('tablist')).toBeInTheDocument();
16
+ expect(screen.getAllByRole('tab')).toHaveLength(3);
17
+ });
18
+
19
+ it('activates the first tab by default', () => {
20
+ render(<Tabs items={items} />);
21
+ expect(screen.getByRole('tab', { name: 'One' })).toHaveAttribute('aria-selected', 'true');
22
+ });
23
+
24
+ it('shows the panel of the active tab', () => {
25
+ render(<Tabs items={items} />);
26
+ expect(screen.getByRole('tabpanel')).toHaveTextContent('Panel One');
27
+ });
28
+
29
+ it('activates defaultActiveId tab', () => {
30
+ render(<Tabs items={items} defaultActiveId="two" />);
31
+ expect(screen.getByRole('tab', { name: 'Two' })).toHaveAttribute('aria-selected', 'true');
32
+ });
33
+
34
+ it('switches tab on click', async () => {
35
+ render(<Tabs items={items} />);
36
+ await userEvent.click(screen.getByRole('tab', { name: 'Two' }));
37
+ expect(screen.getByRole('tab', { name: 'Two' })).toHaveAttribute('aria-selected', 'true');
38
+ expect(screen.getByRole('tabpanel')).toHaveTextContent('Panel Two');
39
+ });
40
+
41
+ it('does not activate a disabled tab on click', async () => {
42
+ render(<Tabs items={items} />);
43
+ await userEvent.click(screen.getByRole('tab', { name: 'Three' }));
44
+ expect(screen.getByRole('tab', { name: 'Three' })).toHaveAttribute('aria-selected', 'false');
45
+ });
46
+
47
+ it('calls onChange when switching tabs', async () => {
48
+ const onChange = vi.fn();
49
+ render(<Tabs items={items} onChange={onChange} />);
50
+ await userEvent.click(screen.getByRole('tab', { name: 'Two' }));
51
+ expect(onChange).toHaveBeenCalledWith('two');
52
+ });
53
+
54
+ it('navigates with ArrowRight key', async () => {
55
+ render(<Tabs items={items} />);
56
+ screen.getByRole('tab', { name: 'One' }).focus();
57
+ await userEvent.keyboard('{ArrowRight}');
58
+ expect(screen.getByRole('tab', { name: 'Two' })).toHaveAttribute('aria-selected', 'true');
59
+ });
60
+
61
+ it('navigates with ArrowLeft key', async () => {
62
+ render(<Tabs items={items} defaultActiveId="two" />);
63
+ screen.getByRole('tab', { name: 'Two' }).focus();
64
+ await userEvent.keyboard('{ArrowLeft}');
65
+ expect(screen.getByRole('tab', { name: 'One' })).toHaveAttribute('aria-selected', 'true');
66
+ });
67
+
68
+ it('links tab aria-controls to the tabpanel id', () => {
69
+ render(<Tabs items={items} id="t" />);
70
+ const tab = screen.getByRole('tab', { name: 'One' });
71
+ const panelId = tab.getAttribute('aria-controls');
72
+ expect(screen.getByRole('tabpanel').id).toBe(panelId);
73
+ });
74
+
75
+ it('respects controlled activeId', async () => {
76
+ const onChange = vi.fn();
77
+ render(<Tabs items={items} activeId="one" onChange={onChange} />);
78
+ await userEvent.click(screen.getByRole('tab', { name: 'Two' }));
79
+ // controlled — active tab should not change without external state update
80
+ expect(screen.getByRole('tab', { name: 'One' })).toHaveAttribute('aria-selected', 'true');
81
+ expect(onChange).toHaveBeenCalledWith('two');
82
+ });
83
+ });
@@ -0,0 +1,56 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { Textarea } from './Textarea.tsx';
5
+
6
+ describe('Textarea', () => {
7
+ it('renders a textarea', () => {
8
+ render(<Textarea />);
9
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders label and links it to the textarea', () => {
13
+ render(<Textarea label="Message" />);
14
+ expect(screen.getByLabelText('Message')).toBeInTheDocument();
15
+ });
16
+
17
+ it('renders helper text', () => {
18
+ render(<Textarea helperText="Max 500 characters" />);
19
+ expect(screen.getByText('Max 500 characters')).toBeInTheDocument();
20
+ });
21
+
22
+ it('links helper text via aria-describedby', () => {
23
+ render(<Textarea helperText="Hint" />);
24
+ const textarea = screen.getByRole('textbox');
25
+ const helperId = textarea.getAttribute('aria-describedby');
26
+ expect(helperId).toBeTruthy();
27
+ expect(document.getElementById(helperId!)).toHaveTextContent('Hint');
28
+ });
29
+
30
+ it('uses 4 rows by default', () => {
31
+ render(<Textarea />);
32
+ expect(screen.getByRole('textbox')).toHaveAttribute('rows', '4');
33
+ });
34
+
35
+ it('forwards rows prop', () => {
36
+ render(<Textarea rows={8} />);
37
+ expect(screen.getByRole('textbox')).toHaveAttribute('rows', '8');
38
+ });
39
+
40
+ it('is disabled when disabled prop is set', () => {
41
+ render(<Textarea disabled />);
42
+ expect(screen.getByRole('textbox')).toBeDisabled();
43
+ });
44
+
45
+ it('calls onChange when typing', async () => {
46
+ const onChange = vi.fn();
47
+ render(<Textarea onChange={onChange} />);
48
+ await userEvent.type(screen.getByRole('textbox'), 'hello');
49
+ expect(onChange).toHaveBeenCalled();
50
+ });
51
+
52
+ it('sets aria-invalid when error is true', () => {
53
+ render(<Textarea error />);
54
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid');
55
+ });
56
+ });
@@ -0,0 +1,116 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { Tree } from './Tree.tsx';
5
+
6
+ const objectData = { name: 'Alice', age: 30, address: { city: 'Warsaw', zip: '00-001' } };
7
+
8
+ describe('Tree', () => {
9
+ it('renders a tree widget', () => {
10
+ render(<Tree data={objectData} />);
11
+ expect(screen.getByRole('tree')).toBeInTheDocument();
12
+ });
13
+
14
+ it('renders top-level keys as treeitems', () => {
15
+ render(<Tree data={objectData} />);
16
+ const items = screen.getAllByRole('treeitem');
17
+ const labels = items.map((el) => el.textContent);
18
+ expect(labels.some((t) => t?.includes('name'))).toBe(true);
19
+ expect(labels.some((t) => t?.includes('age'))).toBe(true);
20
+ });
21
+
22
+ it('renders leaf values inline', () => {
23
+ render(<Tree data={{ score: 42 }} />);
24
+ expect(screen.getByText('42')).toBeInTheDocument();
25
+ });
26
+
27
+ it('collapses nested objects by default', () => {
28
+ render(<Tree data={objectData} />);
29
+ expect(screen.queryByText('Warsaw')).not.toBeInTheDocument();
30
+ });
31
+
32
+ it('expands a branch on click', async () => {
33
+ render(<Tree data={objectData} />);
34
+ const addressItem = screen
35
+ .getAllByRole('treeitem')
36
+ .find((el) => el.textContent?.includes('address'))!;
37
+ await userEvent.click(addressItem);
38
+ expect(screen.getByText('Warsaw')).toBeInTheDocument();
39
+ });
40
+
41
+ it('collapses an expanded branch on click', async () => {
42
+ render(<Tree data={objectData} defaultExpanded />);
43
+ const addressItem = screen
44
+ .getAllByRole('treeitem')
45
+ .find((el) => el.textContent?.includes('address'))!;
46
+ await userEvent.click(addressItem);
47
+ expect(screen.queryByText('Warsaw')).not.toBeInTheDocument();
48
+ });
49
+
50
+ it('sets aria-expanded on branch items', () => {
51
+ render(<Tree data={objectData} />);
52
+ const addressItem = screen
53
+ .getAllByRole('treeitem')
54
+ .find((el) => el.textContent?.includes('address'))!;
55
+ expect(addressItem).toHaveAttribute('aria-expanded', 'false');
56
+ });
57
+
58
+ it('sets correct aria-level on top-level items', () => {
59
+ render(<Tree data={objectData} />);
60
+ const items = screen.getAllByRole('treeitem');
61
+ items.forEach((item) => expect(item).toHaveAttribute('aria-level', '1'));
62
+ });
63
+
64
+ it('expands all branches when defaultExpanded is true', () => {
65
+ render(<Tree data={objectData} defaultExpanded />);
66
+ expect(screen.getByText('Warsaw')).toBeInTheDocument();
67
+ });
68
+
69
+ it('navigates down with ArrowDown', async () => {
70
+ render(<Tree data={objectData} />);
71
+ const items = screen.getAllByRole('treeitem');
72
+ items[0].focus();
73
+ await userEvent.keyboard('{ArrowDown}');
74
+ expect(document.activeElement).toBe(items[1]);
75
+ });
76
+
77
+ it('navigates up with ArrowUp', async () => {
78
+ render(<Tree data={objectData} />);
79
+ const items = screen.getAllByRole('treeitem');
80
+ items[1].tabIndex = 0;
81
+ items[1].focus();
82
+ await userEvent.keyboard('{ArrowUp}');
83
+ expect(document.activeElement).toBe(items[0]);
84
+ });
85
+
86
+ it('expands a branch with ArrowRight', async () => {
87
+ render(<Tree data={objectData} />);
88
+ const addressItem = screen
89
+ .getAllByRole('treeitem')
90
+ .find((el) => el.textContent?.includes('address'))!;
91
+ addressItem.focus();
92
+ await userEvent.keyboard('{ArrowRight}');
93
+ expect(addressItem).toHaveAttribute('aria-expanded', 'true');
94
+ });
95
+
96
+ it('collapses a branch with ArrowLeft', async () => {
97
+ render(<Tree data={objectData} defaultExpanded />);
98
+ const addressItem = screen
99
+ .getAllByRole('treeitem')
100
+ .find((el) => el.textContent?.startsWith('address'))!;
101
+ addressItem.focus();
102
+ await userEvent.keyboard('{ArrowLeft}');
103
+ expect(addressItem).toHaveAttribute('aria-expanded', 'false');
104
+ });
105
+
106
+ it('renders array data with indexed items', () => {
107
+ render(<Tree data={['x', 'y', 'z']} />);
108
+ expect(screen.getByText('x')).toBeInTheDocument();
109
+ expect(screen.getByText('y')).toBeInTheDocument();
110
+ });
111
+
112
+ it('renders null value', () => {
113
+ render(<Tree data={{ val: null }} />);
114
+ expect(screen.getByText('null')).toBeInTheDocument();
115
+ });
116
+ });