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.
Files changed (47) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/package.json +17 -2
  3. package/src/stories/accordion/Accordion.test.tsx +140 -0
  4. package/src/stories/accordionselect/AccordionSelect.test.tsx +252 -0
  5. package/src/stories/avatar/Avatar.test.tsx +77 -0
  6. package/src/stories/button/Button.test.tsx +266 -0
  7. package/src/stories/callout/Callout.test.tsx +79 -0
  8. package/src/stories/card/Card.test.tsx +81 -0
  9. package/src/stories/cardbutton/CardButton.test.tsx +174 -0
  10. package/src/stories/checkbox/Checkbox.test.tsx +531 -0
  11. package/src/stories/combobox/Combobox.test.tsx +164 -0
  12. package/src/stories/dialog/Dialog.module.scss +2 -2
  13. package/src/stories/dialog/Dialog.test.tsx +244 -0
  14. package/src/stories/drawer/Drawer.module.scss +2 -2
  15. package/src/stories/drawer/Drawer.test.tsx +259 -0
  16. package/src/stories/field/Field.test.tsx +146 -0
  17. package/src/stories/icon/Icon.test.tsx +59 -0
  18. package/src/stories/informationaltooltip/InformationalTooltip.test.tsx +178 -0
  19. package/src/stories/input/Input.test.tsx +174 -0
  20. package/src/stories/markdown/Markdown.test.tsx +228 -0
  21. package/src/stories/markdowneditor/FixedToolbar.tsx +44 -14
  22. package/src/stories/markdowneditor/LinkPopover.module.scss +1 -1
  23. package/src/stories/markdowneditor/MarkdownEditor.stories.tsx +4 -1
  24. package/src/stories/markdowneditor/MarkdownEditor.test.tsx +115 -0
  25. package/src/stories/markdowneditor/MarkdownEditor.tsx +11 -1
  26. package/src/stories/markdowneditor/MarkdownEditorContext.tsx +3 -0
  27. package/src/stories/markdowneditor/index.ts +1 -0
  28. package/src/stories/menu/Menu.module.scss +1 -1
  29. package/src/stories/menu/Menu.test.tsx +211 -0
  30. package/src/stories/pagination/usePagination.test.ts +259 -0
  31. package/src/stories/popover/Popover.test.tsx +152 -0
  32. package/src/stories/select/Select.module.scss +2 -1
  33. package/src/stories/select/Select.test.tsx +233 -0
  34. package/src/stories/styledlink/StyledLink.test.tsx +59 -0
  35. package/src/stories/table/Table.test.tsx +156 -0
  36. package/src/stories/tabs/Tabs.module.scss +1 -1
  37. package/src/stories/tabs/Tabs.test.tsx +167 -0
  38. package/src/stories/tag/Tag.test.tsx +90 -0
  39. package/src/stories/text/Text.test.tsx +81 -0
  40. package/src/stories/textarea/TextArea.test.tsx +147 -0
  41. package/src/stories/theme/themes.ts +16 -0
  42. package/src/stories/tilt/Tilt.test.tsx +203 -0
  43. package/src/stories/toast/Toast.test.tsx +86 -0
  44. package/src/stories/utility/Dropdown.module.scss +1 -1
  45. package/src/stories/utility/Utility.test.tsx +96 -0
  46. package/src/test/render.tsx +20 -0
  47. package/src/test/setup.ts +32 -0
@@ -0,0 +1,233 @@
1
+ import { useState } from 'react';
2
+ import { render, screen, waitFor } from '../../test/render';
3
+ import type { Option } from './Select';
4
+ import { Select } from './Select';
5
+
6
+ const options: Option[] = [
7
+ { id: '1', node: 'Single' },
8
+ { id: '2', node: 'EP' },
9
+ { id: '3', node: 'Album (LP)' },
10
+ { id: '4', node: 'Compilation' },
11
+ ];
12
+
13
+ function ControlledSelect(props: Partial<React.ComponentProps<typeof Select>>) {
14
+ const [value, setValue] = useState<string | null>((props.value as string | null) ?? null);
15
+ return (
16
+ <Select
17
+ options={options}
18
+ value={value}
19
+ onChange={(v) => {
20
+ setValue(v);
21
+ (props.onChange as (v: string | null) => void)?.(v);
22
+ }}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ function ControlledMultiSelect(props: Partial<React.ComponentProps<typeof Select>> & { initialValue?: string[] }) {
29
+ const [value, setValue] = useState<string[]>(props.initialValue ?? []);
30
+ return (
31
+ <Select
32
+ options={options}
33
+ value={value}
34
+ onChange={(v) => {
35
+ setValue(v as string[]);
36
+ (props.onChange as (v: string[] | null) => void)?.(v as string[] | null);
37
+ }}
38
+ multiple
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ describe('Select', () => {
45
+ describe('listbox kind (default)', () => {
46
+ it('renders with placeholder text', () => {
47
+ render(<Select options={options} placeholder="Pick one" />);
48
+ expect(screen.getByText('Pick one')).toBeInTheDocument();
49
+ });
50
+
51
+ it('renders default placeholder when none provided', () => {
52
+ render(<Select options={options} />);
53
+ expect(screen.getByText('Select an option')).toBeInTheDocument();
54
+ });
55
+
56
+ it('renders label and description via Field wrapper', () => {
57
+ render(<Select options={options} label="Release type" description="Choose a release type." />);
58
+ expect(screen.getByText('Release type')).toBeInTheDocument();
59
+ expect(screen.getByText('Choose a release type.')).toBeInTheDocument();
60
+ });
61
+
62
+ it('shows selected value text in the button', () => {
63
+ render(<Select options={options} value="2" />);
64
+ expect(screen.getByText('EP')).toBeInTheDocument();
65
+ });
66
+
67
+ it('opens the dropdown and displays options on click', async () => {
68
+ const { user } = render(<ControlledSelect />);
69
+ await user.click(screen.getByText('Select an option'));
70
+
71
+ await waitFor(() => {
72
+ expect(screen.getByText('Single')).toBeInTheDocument();
73
+ expect(screen.getByText('EP')).toBeInTheDocument();
74
+ expect(screen.getByText('Album (LP)')).toBeInTheDocument();
75
+ expect(screen.getByText('Compilation')).toBeInTheDocument();
76
+ });
77
+ });
78
+
79
+ it('selects an option and calls onChange', async () => {
80
+ const handleChange = vi.fn();
81
+ const { user } = render(<ControlledSelect onChange={handleChange} />);
82
+
83
+ await user.click(screen.getByText('Select an option'));
84
+ await waitFor(() => {
85
+ expect(screen.getByText('EP')).toBeInTheDocument();
86
+ });
87
+
88
+ await user.click(screen.getByText('EP'));
89
+ expect(handleChange).toHaveBeenCalledWith('2');
90
+ });
91
+
92
+ it('displays selected option text after selection', async () => {
93
+ const { user } = render(<ControlledSelect />);
94
+
95
+ await user.click(screen.getByText('Select an option'));
96
+ await waitFor(() => {
97
+ expect(screen.getByText('Album (LP)')).toBeInTheDocument();
98
+ });
99
+
100
+ await user.click(screen.getByText('Album (LP)'));
101
+
102
+ await waitFor(() => {
103
+ const button = screen.getByRole('button', { expanded: false });
104
+ expect(button).toHaveTextContent('Album (LP)');
105
+ });
106
+ });
107
+
108
+ it('sets aria-disabled and status data attribute when disabled', () => {
109
+ render(<Select options={options} disabled placeholder="Pick one" />);
110
+ const button = screen.getByText('Pick one').closest('button');
111
+ expect(button).toHaveAttribute('aria-disabled', 'true');
112
+ expect(button).toHaveAttribute('data-status', 'disabled');
113
+ });
114
+
115
+ it('applies error status data attribute', () => {
116
+ render(<Select options={options} status="error" placeholder="Pick one" />);
117
+ const button = screen.getByText('Pick one').closest('button');
118
+ expect(button).toHaveAttribute('data-status', 'error');
119
+ });
120
+
121
+ it('applies success status data attribute', () => {
122
+ render(<Select options={options} status="success" placeholder="Pick one" />);
123
+ const button = screen.getByText('Pick one').closest('button');
124
+ expect(button).toHaveAttribute('data-status', 'success');
125
+ });
126
+ });
127
+
128
+ describe('multi-select', () => {
129
+ it('shows placeholder when no items selected', () => {
130
+ render(<Select options={options} multiple value={[]} placeholder="Select items" />);
131
+ expect(screen.getByText('Select items')).toBeInTheDocument();
132
+ });
133
+
134
+ it('shows single item text when one item selected', () => {
135
+ render(<Select options={options} multiple value={['1']} />);
136
+ expect(screen.getByText('Single')).toBeInTheDocument();
137
+ });
138
+
139
+ it('shows count when multiple items selected', () => {
140
+ render(<Select options={options} multiple value={['1', '2']} />);
141
+ expect(screen.getByText('2 items')).toBeInTheDocument();
142
+ });
143
+
144
+ it('shows "All" text when all items selected', () => {
145
+ render(<Select options={options} multiple value={['1', '2', '3', '4']} />);
146
+ expect(screen.getByText('All items')).toBeInTheDocument();
147
+ });
148
+
149
+ it('uses custom multipleItemsName', () => {
150
+ render(<Select options={options} multiple value={['1', '2']} multipleItemsName="releases" />);
151
+ expect(screen.getByText('2 releases')).toBeInTheDocument();
152
+ });
153
+
154
+ it('shows "All <custom>" when all selected with custom name', () => {
155
+ render(<Select options={options} multiple value={['1', '2', '3', '4']} multipleItemsName="releases" />);
156
+ expect(screen.getByText('All releases')).toBeInTheDocument();
157
+ });
158
+
159
+ it('calls onChange with updated array on selection', async () => {
160
+ const handleChange = vi.fn();
161
+ const { user } = render(<ControlledMultiSelect onChange={handleChange} />);
162
+
163
+ await user.click(screen.getByText('Select an option'));
164
+ await waitFor(() => {
165
+ expect(screen.getByText('Single')).toBeInTheDocument();
166
+ });
167
+
168
+ await user.click(screen.getByText('Single'));
169
+ expect(handleChange).toHaveBeenCalled();
170
+ });
171
+ });
172
+
173
+ describe('radio kind', () => {
174
+ it('renders radio options', () => {
175
+ render(<Select options={options} kind="radio" />);
176
+ expect(screen.getByText('Single')).toBeInTheDocument();
177
+ expect(screen.getByText('EP')).toBeInTheDocument();
178
+ expect(screen.getByText('Album (LP)')).toBeInTheDocument();
179
+ expect(screen.getByText('Compilation')).toBeInTheDocument();
180
+ });
181
+
182
+ it('renders with a radiogroup role', () => {
183
+ render(<Select options={options} kind="radio" />);
184
+ expect(screen.getByRole('radiogroup')).toBeInTheDocument();
185
+ });
186
+
187
+ it('renders radio items with radio role', () => {
188
+ render(<Select options={options} kind="radio" />);
189
+ const radios = screen.getAllByRole('radio');
190
+ expect(radios).toHaveLength(4);
191
+ });
192
+
193
+ it('calls onChange when a radio option is clicked', async () => {
194
+ const handleChange = vi.fn();
195
+ const { user } = render(<ControlledSelect kind="radio" onChange={handleChange} />);
196
+
197
+ await user.click(screen.getByText('EP'));
198
+ expect(handleChange).toHaveBeenCalledWith('2');
199
+ });
200
+ });
201
+
202
+ describe('card kind', () => {
203
+ it('renders card options', () => {
204
+ render(<Select options={options} kind="card" />);
205
+ expect(screen.getByText('Single')).toBeInTheDocument();
206
+ expect(screen.getByText('EP')).toBeInTheDocument();
207
+ });
208
+
209
+ it('calls onChange when a card option is clicked', async () => {
210
+ const handleChange = vi.fn();
211
+ const { user } = render(<ControlledSelect kind="card" onChange={handleChange} />);
212
+
213
+ await user.click(screen.getByText('EP'));
214
+ expect(handleChange).toHaveBeenCalledWith('2');
215
+ });
216
+ });
217
+
218
+ describe('segmented kind', () => {
219
+ it('renders segmented options', () => {
220
+ render(<Select options={options} kind="segmented" />);
221
+ expect(screen.getByText('Single')).toBeInTheDocument();
222
+ expect(screen.getByText('EP')).toBeInTheDocument();
223
+ });
224
+
225
+ it('calls onChange when a segmented option is clicked', async () => {
226
+ const handleChange = vi.fn();
227
+ const { user } = render(<ControlledSelect kind="segmented" onChange={handleChange} />);
228
+
229
+ await user.click(screen.getByText('EP'));
230
+ expect(handleChange).toHaveBeenCalledWith('2');
231
+ });
232
+ });
233
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { render, screen } from '../../test/render';
3
+ import { StyledLink } from './StyledLink';
4
+
5
+ describe('StyledLink', () => {
6
+ it('renders children text', () => {
7
+ render(<StyledLink>Click me</StyledLink>);
8
+ expect(screen.getByText('Click me')).toBeInTheDocument();
9
+ });
10
+
11
+ it('renders as an anchor element by default', () => {
12
+ render(<StyledLink>Link</StyledLink>);
13
+ const el = screen.getByText('Link');
14
+ expect(el.tagName).toBe('A');
15
+ });
16
+
17
+ it('renders with href attribute', () => {
18
+ render(<StyledLink href="https://example.com">Example</StyledLink>);
19
+ const el = screen.getByText('Example');
20
+ expect(el.getAttribute('href')).toBe('https://example.com');
21
+ });
22
+
23
+ it('renders as a different element via the as prop', () => {
24
+ render(<StyledLink as="button">Button link</StyledLink>);
25
+ const el = screen.getByText('Button link');
26
+ expect(el.tagName).toBe('BUTTON');
27
+ });
28
+
29
+ it('applies the link style class', () => {
30
+ render(<StyledLink>Styled</StyledLink>);
31
+ const el = screen.getByText('Styled');
32
+ expect(el.className).toContain('link');
33
+ });
34
+
35
+ it('forwards className prop and merges with default class', () => {
36
+ render(<StyledLink className="custom-link">Custom</StyledLink>);
37
+ const el = screen.getByText('Custom');
38
+ expect(el.className).toContain('custom-link');
39
+ expect(el.className).toContain('link');
40
+ });
41
+
42
+ it('forwards additional HTML props', () => {
43
+ render(
44
+ <StyledLink target="_blank" rel="noopener noreferrer" data-testid="styled-link">
45
+ External
46
+ </StyledLink>,
47
+ );
48
+ const el = screen.getByTestId('styled-link');
49
+ expect(el.getAttribute('target')).toBe('_blank');
50
+ expect(el.getAttribute('rel')).toBe('noopener noreferrer');
51
+ });
52
+
53
+ it('renders without children', () => {
54
+ const { container } = render(<StyledLink href="/empty" />);
55
+ const anchor = container.querySelector('a');
56
+ expect(anchor).toBeInTheDocument();
57
+ expect(anchor?.getAttribute('href')).toBe('/empty');
58
+ });
59
+ });
@@ -0,0 +1,156 @@
1
+ import { render, screen } from '../../test/render';
2
+ import { Table } from './Table';
3
+
4
+ const columns = [{ title: 'Name' }, { title: 'Age' }, { title: 'Email' }];
5
+
6
+ const rows = [
7
+ { name: 'Alice', age: 30, email: 'alice@example.com' },
8
+ { name: 'Bob', age: 25, email: 'bob@example.com' },
9
+ { name: 'Charlie', age: 35, email: 'charlie@example.com' },
10
+ ];
11
+
12
+ describe('Table', () => {
13
+ it('renders column headers', () => {
14
+ render(<Table columns={columns} rows={rows} />);
15
+ expect(screen.getByText('Name')).toBeInTheDocument();
16
+ expect(screen.getByText('Age')).toBeInTheDocument();
17
+ expect(screen.getByText('Email')).toBeInTheDocument();
18
+ });
19
+
20
+ it('renders row data using Object.values by default', () => {
21
+ render(<Table columns={columns} rows={rows} />);
22
+ expect(screen.getByText('Alice')).toBeInTheDocument();
23
+ expect(screen.getByText('30')).toBeInTheDocument();
24
+ expect(screen.getByText('alice@example.com')).toBeInTheDocument();
25
+ });
26
+
27
+ it('renders all rows', () => {
28
+ render(<Table columns={columns} rows={rows} />);
29
+ expect(screen.getByText('Alice')).toBeInTheDocument();
30
+ expect(screen.getByText('Bob')).toBeInTheDocument();
31
+ expect(screen.getByText('Charlie')).toBeInTheDocument();
32
+ });
33
+
34
+ it('uses custom rowRenderFn', () => {
35
+ render(
36
+ <Table
37
+ columns={[{ title: 'Full Info' }]}
38
+ rows={rows}
39
+ rowRenderFn={(row) => ({
40
+ key: row.name,
41
+ cells: [`${row.name} (${row.age})`],
42
+ })}
43
+ />,
44
+ );
45
+ expect(screen.getByText('Alice (30)')).toBeInTheDocument();
46
+ expect(screen.getByText('Bob (25)')).toBeInTheDocument();
47
+ });
48
+
49
+ it('renders the table element', () => {
50
+ const { container } = render(<Table columns={columns} rows={rows} />);
51
+ expect(container.querySelector('table')).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders thead and tbody', () => {
55
+ const { container } = render(<Table columns={columns} rows={rows} />);
56
+ expect(container.querySelector('thead')).toBeInTheDocument();
57
+ expect(container.querySelector('tbody')).toBeInTheDocument();
58
+ });
59
+
60
+ it('calls onRowClick when a row is clicked', async () => {
61
+ const onRowClick = vi.fn();
62
+ const { user } = render(<Table columns={columns} rows={rows} onRowClick={onRowClick} />);
63
+ const aliceCell = screen.getByText('Alice');
64
+ await user.click(aliceCell.closest('tr')!);
65
+ expect(onRowClick).toHaveBeenCalledWith(rows[0]);
66
+ });
67
+
68
+ it('does not call onRowClick when clickableRows is false', async () => {
69
+ const onRowClick = vi.fn();
70
+ const { user } = render(<Table columns={columns} rows={rows} onRowClick={onRowClick} clickableRows={false} />);
71
+ const aliceCell = screen.getByText('Alice');
72
+ await user.click(aliceCell.closest('tr')!);
73
+ expect(onRowClick).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it('displays empty state when rows are empty', () => {
77
+ render(<Table columns={columns} rows={[]} emptyState="No data available" />);
78
+ expect(screen.getByText('No data available')).toBeInTheDocument();
79
+ });
80
+
81
+ it('does not display empty state when there are rows', () => {
82
+ render(<Table columns={columns} rows={rows} emptyState="No data available" />);
83
+ expect(screen.queryByText('No data available')).not.toBeInTheDocument();
84
+ });
85
+
86
+ it('applies table className via overrides', () => {
87
+ const { container } = render(
88
+ <Table columns={columns} rows={rows} overrides={{ table: { className: 'custom-table' } }} />,
89
+ );
90
+ expect(container.querySelector('table')).toHaveClass('custom-table');
91
+ });
92
+
93
+ it('applies thead className via overrides', () => {
94
+ const { container } = render(
95
+ <Table columns={columns} rows={rows} overrides={{ thead: { className: 'custom-thead' } }} />,
96
+ );
97
+ expect(container.querySelector('thead')).toHaveClass('custom-thead');
98
+ });
99
+
100
+ it('applies tbody className via overrides', () => {
101
+ const { container } = render(
102
+ <Table columns={columns} rows={rows} overrides={{ tbody: { className: 'custom-tbody' } }} />,
103
+ );
104
+ expect(container.querySelector('tbody')).toHaveClass('custom-tbody');
105
+ });
106
+
107
+ it('applies clickable class to rows when clickableRows is true', () => {
108
+ render(<Table columns={columns} rows={rows} clickableRows={true} />);
109
+ const aliceRow = screen.getByText('Alice').closest('tr')!;
110
+ expect(aliceRow).toHaveClass('clickable');
111
+ });
112
+
113
+ it('does not apply clickable class when clickableRows is false', () => {
114
+ render(<Table columns={columns} rows={rows} clickableRows={false} />);
115
+ const aliceRow = screen.getByText('Alice').closest('tr')!;
116
+ expect(aliceRow).not.toHaveClass('clickable');
117
+ });
118
+
119
+ it('supports keyboard navigation on clickable rows', async () => {
120
+ const onRowClick = vi.fn();
121
+ const { user } = render(<Table columns={columns} rows={rows} onRowClick={onRowClick} />);
122
+ const aliceRow = screen.getByText('Alice').closest('tr')!;
123
+ aliceRow.focus();
124
+ await user.keyboard('{Enter}');
125
+ expect(onRowClick).toHaveBeenCalledWith(rows[0]);
126
+ });
127
+
128
+ it('handles hideBelow column property', () => {
129
+ const columnsWithHide = [{ title: 'Name' }, { title: 'Age', hideBelow: 'md' as const }, { title: 'Email' }];
130
+ const { container } = render(<Table columns={columnsWithHide} rows={rows} />);
131
+ const ageHeader = screen.getByText('Age').closest('th');
132
+ expect(ageHeader).toHaveClass('md');
133
+ });
134
+
135
+ it('renders empty state with colSpan matching column count', () => {
136
+ const { container } = render(<Table columns={columns} rows={[]} emptyState="Empty" />);
137
+ const td = container.querySelector('td[colspan]');
138
+ expect(td).toHaveAttribute('colspan', String(columns.length));
139
+ });
140
+
141
+ it('renders ReactNode content in cells', () => {
142
+ const richRows = [{ name: 'Alice', age: 30, email: 'alice@example.com' }];
143
+ render(
144
+ <Table
145
+ columns={columns}
146
+ rows={richRows}
147
+ rowRenderFn={(row) => ({
148
+ key: row.name,
149
+ cells: [<strong key="name">{row.name}</strong>, String(row.age), row.email],
150
+ })}
151
+ />,
152
+ );
153
+ const strong = screen.getByText('Alice');
154
+ expect(strong.tagName).toBe('STRONG');
155
+ });
156
+ });
@@ -35,7 +35,7 @@
35
35
  position: sticky;
36
36
  top: 0;
37
37
  width: 100%;
38
- z-index: 1;
38
+ z-index: var(--pte-new-layers-sticky);
39
39
  background: var(--pte-new-materials-primaryThin-background);
40
40
  }
41
41
  }
@@ -0,0 +1,167 @@
1
+ import { render, screen, waitFor } from '../../test/render';
2
+ import { Tabs } from './Tabs';
3
+
4
+ // Mock framer-motion to avoid layout animation issues in tests
5
+ vi.mock('framer-motion', () => ({
6
+ motion: {
7
+ div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
8
+ },
9
+ AnimatePresence: ({ children }: any) => <>{children}</>,
10
+ cubicBezier: (..._args: number[]) => [0, 0, 1, 1],
11
+ }));
12
+
13
+ const defaultTabs = [
14
+ { title: 'Transactions', content: 'Transactions content' },
15
+ { title: 'Cards', content: 'Cards content' },
16
+ { title: 'Documents', content: 'Documents content' },
17
+ ];
18
+
19
+ describe('Tabs', () => {
20
+ it('renders all tab titles', () => {
21
+ render(<Tabs tabs={defaultTabs} />);
22
+
23
+ expect(screen.getByText('Transactions')).toBeInTheDocument();
24
+ expect(screen.getByText('Cards')).toBeInTheDocument();
25
+ expect(screen.getByText('Documents')).toBeInTheDocument();
26
+ });
27
+
28
+ it('renders the first tab panel content by default', () => {
29
+ render(<Tabs tabs={defaultTabs} />);
30
+
31
+ expect(screen.getByText('Transactions content')).toBeInTheDocument();
32
+ });
33
+
34
+ it('renders tab elements with correct roles', () => {
35
+ render(<Tabs tabs={defaultTabs} />);
36
+
37
+ const tabs = screen.getAllByRole('tab');
38
+ expect(tabs).toHaveLength(3);
39
+ expect(screen.getByRole('tablist')).toBeInTheDocument();
40
+ expect(screen.getByRole('tabpanel')).toBeInTheDocument();
41
+ });
42
+
43
+ it('marks the first tab as selected by default', () => {
44
+ render(<Tabs tabs={defaultTabs} />);
45
+
46
+ const tabs = screen.getAllByRole('tab');
47
+ expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
48
+ expect(tabs[1]).toHaveAttribute('aria-selected', 'false');
49
+ expect(tabs[2]).toHaveAttribute('aria-selected', 'false');
50
+ });
51
+
52
+ it('switches active panel when clicking a different tab', async () => {
53
+ const { user } = render(<Tabs tabs={defaultTabs} />);
54
+
55
+ const tabs = screen.getAllByRole('tab');
56
+ await user.click(tabs[1]);
57
+
58
+ await waitFor(() => {
59
+ expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
60
+ });
61
+
62
+ expect(screen.getByText('Cards content')).toBeInTheDocument();
63
+ });
64
+
65
+ it('calls onTabChange when a tab is clicked', async () => {
66
+ const onTabChange = vi.fn();
67
+ const { user } = render(<Tabs tabs={defaultTabs} onTabChange={onTabChange} />);
68
+
69
+ const tabs = screen.getAllByRole('tab');
70
+ await user.click(tabs[2]);
71
+
72
+ await waitFor(() => {
73
+ expect(onTabChange).toHaveBeenCalledWith(2);
74
+ });
75
+ });
76
+
77
+ it('respects defaultIndex prop', () => {
78
+ render(<Tabs tabs={defaultTabs} defaultIndex={1} />);
79
+
80
+ const tabs = screen.getAllByRole('tab');
81
+ expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
82
+ expect(screen.getByText('Cards content')).toBeInTheDocument();
83
+ });
84
+
85
+ it('supports controlled index', () => {
86
+ const { rerender } = render(<Tabs tabs={defaultTabs} index={0} />);
87
+
88
+ const tabs = screen.getAllByRole('tab');
89
+ expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
90
+
91
+ rerender(<Tabs tabs={defaultTabs} index={2} />);
92
+
93
+ expect(tabs[2]).toHaveAttribute('aria-selected', 'true');
94
+ expect(screen.getByText('Documents content')).toBeInTheDocument();
95
+ });
96
+
97
+ it('navigates tabs with keyboard arrow keys', async () => {
98
+ const { user } = render(<Tabs tabs={defaultTabs} />);
99
+
100
+ const tabs = screen.getAllByRole('tab');
101
+ await user.click(tabs[0]);
102
+ await user.keyboard('{ArrowRight}');
103
+
104
+ await waitFor(() => {
105
+ expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
106
+ });
107
+ });
108
+
109
+ it('renders with compact kind', () => {
110
+ render(<Tabs tabs={defaultTabs} kind="compact" />);
111
+
112
+ const tabs = screen.getAllByRole('tab');
113
+ expect(tabs).toHaveLength(3);
114
+ });
115
+
116
+ it('renders with full kind', () => {
117
+ render(<Tabs tabs={defaultTabs} kind="full" />);
118
+
119
+ const tabs = screen.getAllByRole('tab');
120
+ expect(tabs).toHaveLength(3);
121
+ });
122
+
123
+ it('renders with thin barStyle', () => {
124
+ render(<Tabs tabs={defaultTabs} barStyle="thin" />);
125
+
126
+ const tabs = screen.getAllByRole('tab');
127
+ expect(tabs).toHaveLength(3);
128
+ });
129
+
130
+ it('renders with glass backgroundStyle', () => {
131
+ render(<Tabs tabs={defaultTabs} backgroundStyle="glass" />);
132
+
133
+ expect(screen.getAllByRole('tab')).toHaveLength(3);
134
+ });
135
+
136
+ it('renders ReactNode content in tab panels', async () => {
137
+ const tabsWithJsx = [
138
+ { title: 'Tab 1', content: <div data-testid="custom-content">Custom JSX</div> },
139
+ { title: 'Tab 2', content: 'Plain text' },
140
+ ];
141
+
142
+ render(<Tabs tabs={tabsWithJsx} />);
143
+
144
+ expect(screen.getByTestId('custom-content')).toBeInTheDocument();
145
+ expect(screen.getByText('Custom JSX')).toBeInTheDocument();
146
+ });
147
+
148
+ it('handles a single tab', () => {
149
+ const singleTab = [{ title: 'Only Tab', content: 'Only content' }];
150
+ render(<Tabs tabs={singleTab} />);
151
+
152
+ expect(screen.getAllByRole('tab')).toHaveLength(1);
153
+ expect(screen.getByText('Only content')).toBeInTheDocument();
154
+ });
155
+
156
+ it('does not call onTabChange when clicking the already-selected tab', async () => {
157
+ const onTabChange = vi.fn();
158
+ const { user } = render(<Tabs tabs={defaultTabs} onTabChange={onTabChange} />);
159
+
160
+ const tabs = screen.getAllByRole('tab');
161
+ await user.click(tabs[0]);
162
+
163
+ // HeadlessUI may or may not call onChange for same-tab clicks.
164
+ // The important thing is it doesn't crash.
165
+ expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
166
+ });
167
+ });