tharaday 0.7.2 → 0.7.4
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/.storybook/main.ts +1 -1
- package/.storybook/preview.ts +0 -2
- package/.storybook/vitest.setup.ts +2 -0
- package/dist/ds.css +1 -1
- package/dist/ds.js +883 -815
- package/dist/ds.umd.cjs +1 -1
- package/dist/src/components/Tree/Tree.d.ts +1 -1
- package/dist/src/components/Tree/Tree.stories.d.ts +1 -1
- package/dist/src/components/Tree/TreeItem.d.ts +1 -1
- package/dist/src/components/Tree/TreeItem.types.d.ts +6 -0
- package/package.json +8 -1
- package/src/components/Accordion/Accordion.test.tsx +82 -0
- package/src/components/Avatar/Avatar.test.tsx +36 -0
- package/src/components/Badge/Badge.test.tsx +15 -0
- package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +96 -0
- package/src/components/Checkbox/Checkbox.module.css +8 -7
- package/src/components/Checkbox/Checkbox.test.tsx +68 -0
- package/src/components/Dropdown/Dropdown.test.tsx +104 -0
- package/src/components/Input/Input.test.tsx +61 -0
- package/src/components/List/List.module.css +12 -12
- package/src/components/List/List.test.tsx +46 -0
- package/src/components/Modal/Modal.module.css +5 -5
- package/src/components/Modal/Modal.test.tsx +86 -0
- package/src/components/NavBar/NavBar.module.css +3 -3
- package/src/components/Notification/Notification.module.css +6 -6
- package/src/components/Notification/Notification.test.tsx +38 -0
- package/src/components/Pagination/Pagination.test.tsx +70 -0
- package/src/components/ProgressBar/ProgressBar.test.tsx +58 -0
- package/src/components/RadioButton/RadioButton.test.tsx +51 -0
- package/src/components/Select/Select.test.tsx +64 -0
- package/src/components/Slider/Slider.test.tsx +49 -0
- package/src/components/Stepper/Step.module.css +2 -2
- package/src/components/Stepper/Stepper.test.tsx +51 -0
- package/src/components/Switch/Switch.test.tsx +53 -0
- package/src/components/Table/Table.test.tsx +78 -0
- package/src/components/Tabs/Tabs.test.tsx +83 -0
- package/src/components/Textarea/Textarea.test.tsx +56 -0
- package/src/components/Tooltip/Tooltip.module.css +6 -6
- package/src/components/Tree/Tree.test.tsx +116 -0
- package/src/components/Tree/Tree.tsx +65 -1
- package/src/components/Tree/TreeItem.module.css +20 -26
- package/src/components/Tree/TreeItem.tsx +144 -79
- package/src/components/Tree/TreeItem.types.ts +6 -0
- package/src/styles/ds.css +14 -9
- package/src/styles/palette.css +71 -0
- package/src/styles/themes/dark.css +35 -35
- package/src/styles/themes/light.css +35 -35
- package/src/styles/themes/retro-dark.css +35 -35
- package/src/styles/themes/retro-light.css +35 -35
- package/src/styles/themes/retro-palette.css +85 -0
- package/src/styles/themes/sanzo-152-dark.css +35 -35
- package/src/styles/themes/sanzo-152-light.css +35 -35
- package/src/styles/themes/sanzo-152-palette.css +66 -0
- package/src/styles/tokens.css +14 -224
- package/src/styles/semantic.css +0 -56
|
@@ -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:
|
|
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:
|
|
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
|
+
});
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
.dark {
|
|
23
|
-
background-color: var(--neutral-900);
|
|
24
|
-
color: var(--neutral-0);
|
|
23
|
+
background-color: var(--ds-p-neutral-900);
|
|
24
|
+
color: var(--ds-p-neutral-0);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
.light {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
.top.dark::after {
|
|
78
|
-
border-color: var(--neutral-900) transparent transparent transparent;
|
|
78
|
+
border-color: var(--ds-p-neutral-900) transparent transparent transparent;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
.top.light::after {
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
.bottom.dark::after {
|
|
92
|
-
border-color: transparent transparent var(--neutral-900) transparent;
|
|
92
|
+
border-color: transparent transparent var(--ds-p-neutral-900) transparent;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
.bottom.light::after {
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
.left.dark::after {
|
|
106
|
-
border-color: transparent transparent transparent var(--neutral-900);
|
|
106
|
+
border-color: transparent transparent transparent var(--ds-p-neutral-900);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
.left.light::after {
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
.right.dark::after {
|
|
120
|
-
border-color: transparent var(--neutral-900) transparent transparent;
|
|
120
|
+
border-color: transparent var(--ds-p-neutral-900) transparent transparent;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
.right.light::after {
|