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,146 @@
1
+ import { render, screen } from '../../test/render';
2
+ import { Field } from './Field';
3
+
4
+ describe('Field', () => {
5
+ it('renders children', () => {
6
+ render(
7
+ <Field label="Name">
8
+ <input data-testid="child" />
9
+ </Field>,
10
+ );
11
+ expect(screen.getByTestId('child')).toBeInTheDocument();
12
+ });
13
+
14
+ it('renders a string label as a <label> element', () => {
15
+ render(
16
+ <Field label="Email" htmlFor="email-input">
17
+ <input id="email-input" />
18
+ </Field>,
19
+ );
20
+ const label = screen.getByText('Email');
21
+ expect(label.tagName).toBe('LABEL');
22
+ expect(label).toHaveAttribute('for', 'email-input');
23
+ });
24
+
25
+ it('renders a ReactNode label wrapped in a plain <label>', () => {
26
+ render(
27
+ <Field label={<span data-testid="custom-label">Custom</span>} htmlFor="input-id">
28
+ <input id="input-id" />
29
+ </Field>,
30
+ );
31
+ const customLabel = screen.getByTestId('custom-label');
32
+ expect(customLabel).toBeInTheDocument();
33
+ // The wrapping <label> should have the htmlFor
34
+ expect(customLabel.closest('label')).toHaveAttribute('for', 'input-id');
35
+ });
36
+
37
+ it('hides the label visually when hideLabel is true', () => {
38
+ render(
39
+ <Field label="Hidden Label" hideLabel>
40
+ <input />
41
+ </Field>,
42
+ );
43
+ const label = screen.getByText('Hidden Label');
44
+ expect(label).toHaveClass('hidden');
45
+ });
46
+
47
+ it('renders a string description', () => {
48
+ render(
49
+ <Field label="Name" description="Enter your full name" htmlFor="name">
50
+ <input id="name" />
51
+ </Field>,
52
+ );
53
+ expect(screen.getByText('Enter your full name')).toBeInTheDocument();
54
+ });
55
+
56
+ it('hides the description when hideDescription is true', () => {
57
+ render(
58
+ <Field label="Name" description="Helper text" hideDescription htmlFor="name">
59
+ <input id="name" />
60
+ </Field>,
61
+ );
62
+ const desc = screen.getByText('Helper text');
63
+ expect(desc).toHaveClass('hidden');
64
+ });
65
+
66
+ it('hides the description element when no description is provided', () => {
67
+ render(
68
+ <Field label="Name" htmlFor="test-id">
69
+ <input id="test-id" />
70
+ </Field>,
71
+ );
72
+ const descEl = document.getElementById('test-id-description');
73
+ expect(descEl).toBeInTheDocument();
74
+ expect(descEl).toHaveClass('hidden');
75
+ });
76
+
77
+ it('renders description at the bottom by default', () => {
78
+ const { container } = render(
79
+ <Field label="Name" description="Below input" htmlFor="name">
80
+ <input id="name" />
81
+ </Field>,
82
+ );
83
+ const rootDiv = container.firstElementChild!;
84
+ const children = Array.from(rootDiv.children);
85
+ // Order: label, children (input), description
86
+ const labelIdx = children.findIndex((el) => el.textContent === 'Name');
87
+ const descIdx = children.findIndex((el) => el.textContent === 'Below input');
88
+ expect(descIdx).toBeGreaterThan(labelIdx);
89
+ });
90
+
91
+ it('renders description above children when descriptionPosition is top', () => {
92
+ const { container } = render(
93
+ <Field label="Name" description="Above input" descriptionPosition="top" htmlFor="name">
94
+ <input id="name" data-testid="input" />
95
+ </Field>,
96
+ );
97
+ const rootDiv = container.firstElementChild!;
98
+ const children = Array.from(rootDiv.children);
99
+ // When top: label and description are in a labelContainer div, then children
100
+ const labelContainer = children[0];
101
+ expect(labelContainer).toHaveClass('labelContainer');
102
+ expect(labelContainer.textContent).toContain('Name');
103
+ expect(labelContainer.textContent).toContain('Above input');
104
+ });
105
+
106
+ it('focuses the input when the container is clicked', async () => {
107
+ const { user } = render(
108
+ <Field label="Click me" htmlFor="focusable">
109
+ <input id="focusable" data-testid="focusable" />
110
+ </Field>,
111
+ );
112
+ const label = screen.getByText('Click me');
113
+ await user.click(label);
114
+ expect(screen.getByTestId('focusable')).toHaveFocus();
115
+ });
116
+
117
+ it('does not focus the input when disabled', async () => {
118
+ const { user, container } = render(
119
+ <Field label="Disabled" htmlFor="disabled-input" disabled>
120
+ <input id="disabled-input" data-testid="disabled-input" />
121
+ </Field>,
122
+ );
123
+ await user.click(container.firstElementChild!);
124
+ expect(screen.getByTestId('disabled-input')).not.toHaveFocus();
125
+ });
126
+
127
+ it('applies overrides.container props', () => {
128
+ render(
129
+ <Field label="Test" overrides={{ container: { 'data-testid': 'custom-container' } }}>
130
+ <input />
131
+ </Field>,
132
+ );
133
+ expect(screen.getByTestId('custom-container')).toBeInTheDocument();
134
+ });
135
+
136
+ it('renders ReactNode description in a div', () => {
137
+ render(
138
+ <Field label="Name" description={<em data-testid="em-desc">Emphasized</em>} htmlFor="name">
139
+ <input id="name" />
140
+ </Field>,
141
+ );
142
+ const emDesc = screen.getByTestId('em-desc');
143
+ expect(emDesc).toBeInTheDocument();
144
+ expect(emDesc.closest('div')).toHaveAttribute('id', 'name-description');
145
+ });
146
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { render, screen } from '../../test/render';
3
+ import { Close } from './Close';
4
+ import { Icon } from './Icon';
5
+
6
+ describe('Icon', () => {
7
+ it('renders with the icon prop', () => {
8
+ render(<Icon icon={Close} size={20} data-testid="icon" />);
9
+ const el = screen.getByTestId('icon');
10
+ expect(el).toBeInTheDocument();
11
+ });
12
+
13
+ it('renders as a span by default', () => {
14
+ render(<Icon icon={Close} size={20} data-testid="icon-span" />);
15
+ const el = screen.getByTestId('icon-span');
16
+ expect(el.tagName).toBe('SPAN');
17
+ });
18
+
19
+ it('renders as a different element via the as prop', () => {
20
+ render(<Icon icon={Close} size={20} as="div" data-testid="icon-div" />);
21
+ const el = screen.getByTestId('icon-div');
22
+ expect(el.tagName).toBe('DIV');
23
+ });
24
+
25
+ it('renders the SVG icon inside', () => {
26
+ render(<Icon icon={Close} size={20} data-testid="icon-svg" />);
27
+ const el = screen.getByTestId('icon-svg');
28
+ const svg = el.querySelector('svg');
29
+ expect(svg).toBeInTheDocument();
30
+ });
31
+
32
+ it('passes size to the icon definition', () => {
33
+ render(<Icon icon={Close} size={24} data-testid="icon-sized" />);
34
+ const el = screen.getByTestId('icon-sized');
35
+ const svg = el.querySelector('svg');
36
+ expect(svg?.getAttribute('width')).toBe('24');
37
+ expect(svg?.getAttribute('height')).toBe('24');
38
+ });
39
+
40
+ it('forwards className prop', () => {
41
+ render(<Icon icon={Close} size={20} className="custom-icon" data-testid="icon-class" />);
42
+ const el = screen.getByTestId('icon-class');
43
+ expect(el.className).toContain('custom-icon');
44
+ });
45
+
46
+ it('forwards additional HTML props', () => {
47
+ render(<Icon icon={Close} size={20} id="my-icon" data-testid="icon-props" />);
48
+ const el = screen.getByTestId('icon-props');
49
+ expect(el.id).toBe('my-icon');
50
+ });
51
+
52
+ it('renders correctly with overrides prop', () => {
53
+ render(<Icon icon={Close} size={16} overrides={{ icon: {} }} data-testid="icon-overrides" />);
54
+ const el = screen.getByTestId('icon-overrides');
55
+ const svg = el.querySelector('svg');
56
+ expect(svg).toBeInTheDocument();
57
+ expect(svg?.getAttribute('width')).toBe('16');
58
+ });
59
+ });
@@ -0,0 +1,178 @@
1
+ import { render, screen, waitFor } from '../../test/render';
2
+ import { InformationalTooltip } from './InformationalTooltip';
3
+
4
+ // Mock pget to return a valid CSS time value for animation duration
5
+ vi.mock('../theme', () => ({
6
+ pvar: (path: string) => `var(--pte-${path.replace(/\./g, '-')})`,
7
+ pget: () => '100ms',
8
+ }));
9
+
10
+ /**
11
+ * Radix Tooltip duplicates content inside a visually-hidden `role="tooltip"` span
12
+ * for screen readers. This helper queries the *visible* tooltip content element,
13
+ * excluding the hidden aria-describedby clone.
14
+ */
15
+ function getVisibleTooltipContent(): HTMLElement | null {
16
+ const wrapper = document.querySelector('[data-radix-popper-content-wrapper]');
17
+ if (!wrapper) return null;
18
+ // The direct child with data-side is the visible tooltip content
19
+ return wrapper.querySelector(':scope > [data-side]');
20
+ }
21
+
22
+ describe('InformationalTooltip', () => {
23
+ it('renders the default trigger icon', () => {
24
+ render(<InformationalTooltip>Tooltip content</InformationalTooltip>);
25
+ const trigger = screen.getByRole('button');
26
+ expect(trigger).toBeInTheDocument();
27
+ });
28
+
29
+ it('renders a custom trigger', () => {
30
+ render(
31
+ <InformationalTooltip trigger={<span data-testid="custom-trigger">?</span>}>
32
+ Tooltip body
33
+ </InformationalTooltip>,
34
+ );
35
+ expect(screen.getByTestId('custom-trigger')).toBeInTheDocument();
36
+ });
37
+
38
+ it('shows tooltip content on hover', async () => {
39
+ const { user } = render(<InformationalTooltip>Hover content</InformationalTooltip>);
40
+
41
+ const trigger = screen.getByRole('button');
42
+ await user.hover(trigger);
43
+
44
+ await waitFor(() => {
45
+ const tooltip = getVisibleTooltipContent();
46
+ expect(tooltip).toBeInTheDocument();
47
+ expect(tooltip).toHaveTextContent('Hover content');
48
+ });
49
+ });
50
+
51
+ it('does not show tooltip when closed', () => {
52
+ render(<InformationalTooltip>Hidden content</InformationalTooltip>);
53
+ expect(getVisibleTooltipContent()).not.toBeInTheDocument();
54
+ });
55
+
56
+ it('renders with a heading when provided', async () => {
57
+ const { user } = render(<InformationalTooltip heading="Info Title">Body text</InformationalTooltip>);
58
+
59
+ const trigger = screen.getByRole('button');
60
+ await user.hover(trigger);
61
+
62
+ await waitFor(() => {
63
+ const tooltip = getVisibleTooltipContent();
64
+ expect(tooltip).toHaveTextContent('Info Title');
65
+ expect(tooltip).toHaveTextContent('Body text');
66
+ });
67
+ });
68
+
69
+ it('does not render heading when heading is null', async () => {
70
+ const { user } = render(<InformationalTooltip heading={null}>Only body</InformationalTooltip>);
71
+
72
+ const trigger = screen.getByRole('button');
73
+ await user.hover(trigger);
74
+
75
+ await waitFor(() => {
76
+ const tooltip = getVisibleTooltipContent();
77
+ expect(tooltip).toHaveTextContent('Only body');
78
+ });
79
+
80
+ const headingEl = getVisibleTooltipContent()?.querySelector('[class*="heading"]');
81
+ expect(headingEl).toBeNull();
82
+ });
83
+
84
+ it('opens on click by default', async () => {
85
+ const { user } = render(<InformationalTooltip>Click content</InformationalTooltip>);
86
+
87
+ const trigger = screen.getByRole('button');
88
+ await user.click(trigger);
89
+
90
+ await waitFor(() => {
91
+ const tooltip = getVisibleTooltipContent();
92
+ expect(tooltip).toBeInTheDocument();
93
+ expect(tooltip).toHaveTextContent('Click content');
94
+ });
95
+ });
96
+
97
+ it('does not open on click when disableClick is true', async () => {
98
+ const { user } = render(<InformationalTooltip disableClick>No click</InformationalTooltip>);
99
+
100
+ const trigger = screen.getByRole('button');
101
+ await user.click(trigger);
102
+
103
+ expect(getVisibleTooltipContent()).not.toBeInTheDocument();
104
+ });
105
+
106
+ it('renders open by default when defaultOpen is true', async () => {
107
+ render(<InformationalTooltip defaultOpen>Default open content</InformationalTooltip>);
108
+
109
+ await waitFor(() => {
110
+ const tooltip = getVisibleTooltipContent();
111
+ expect(tooltip).toBeInTheDocument();
112
+ expect(tooltip).toHaveTextContent('Default open content');
113
+ });
114
+ });
115
+
116
+ it('applies the medium size class', async () => {
117
+ const { user } = render(<InformationalTooltip size="medium">Medium tip</InformationalTooltip>);
118
+
119
+ const trigger = screen.getByRole('button');
120
+ await user.hover(trigger);
121
+
122
+ await waitFor(() => {
123
+ const tooltip = getVisibleTooltipContent();
124
+ expect(tooltip).toHaveClass('medium');
125
+ });
126
+ });
127
+
128
+ it('applies the large size class by default', async () => {
129
+ const { user } = render(<InformationalTooltip>Large tip</InformationalTooltip>);
130
+
131
+ const trigger = screen.getByRole('button');
132
+ await user.hover(trigger);
133
+
134
+ await waitFor(() => {
135
+ const tooltip = getVisibleTooltipContent();
136
+ expect(tooltip).toHaveClass('large');
137
+ });
138
+ });
139
+
140
+ it('accepts override props for the tooltip element', async () => {
141
+ render(
142
+ <InformationalTooltip
143
+ defaultOpen
144
+ overrides={{
145
+ tooltip: { 'data-testid': 'overridden-tooltip' } as any,
146
+ }}
147
+ >
148
+ Overridden
149
+ </InformationalTooltip>,
150
+ );
151
+
152
+ await waitFor(() => {
153
+ const tooltip = getVisibleTooltipContent();
154
+ expect(tooltip).toHaveAttribute('data-testid', 'overridden-tooltip');
155
+ });
156
+ });
157
+
158
+ it('accepts override props for the heading element', async () => {
159
+ render(
160
+ <InformationalTooltip
161
+ defaultOpen
162
+ heading="Title"
163
+ overrides={{
164
+ heading: { 'data-testid': 'overridden-heading' },
165
+ }}
166
+ >
167
+ Body
168
+ </InformationalTooltip>,
169
+ );
170
+
171
+ await waitFor(() => {
172
+ const tooltip = getVisibleTooltipContent();
173
+ // Query only direct children to avoid the duplicated aria-describedby copy
174
+ const heading = tooltip?.querySelector(':scope > [data-testid="overridden-heading"]');
175
+ expect(heading).toBeInTheDocument();
176
+ });
177
+ });
178
+ });
@@ -0,0 +1,174 @@
1
+ import { createRef } from 'react';
2
+ import { render, screen } from '../../test/render';
3
+ import { Input } from './Input';
4
+
5
+ describe('Input', () => {
6
+ it('renders with a label', () => {
7
+ render(<Input label="Username" />);
8
+ expect(screen.getByLabelText('Username')).toBeInTheDocument();
9
+ });
10
+
11
+ it('renders an input element with type text by default', () => {
12
+ render(<Input label="Name" />);
13
+ const input = screen.getByLabelText('Name');
14
+ expect(input.tagName).toBe('INPUT');
15
+ expect(input).toHaveAttribute('type', 'text');
16
+ });
17
+
18
+ it('allows typing into the input', async () => {
19
+ const { user } = render(<Input label="Email" />);
20
+ const input = screen.getByLabelText('Email');
21
+ await user.type(input, 'test@example.com');
22
+ expect(input).toHaveValue('test@example.com');
23
+ });
24
+
25
+ it('displays placeholder text', () => {
26
+ render(<Input label="Search" placeholder="Type to search..." />);
27
+ expect(screen.getByPlaceholderText('Type to search...')).toBeInTheDocument();
28
+ });
29
+
30
+ it('sets data-status to default when no status is provided', () => {
31
+ render(<Input label="Default" />);
32
+ const input = screen.getByLabelText('Default');
33
+ expect(input).toHaveAttribute('data-status', 'default');
34
+ });
35
+
36
+ it('sets data-status to error', () => {
37
+ render(<Input label="Error field" status="error" />);
38
+ const input = screen.getByLabelText('Error field');
39
+ expect(input).toHaveAttribute('data-status', 'error');
40
+ });
41
+
42
+ it('sets data-status to success', () => {
43
+ render(<Input label="Success field" status="success" />);
44
+ const input = screen.getByLabelText('Success field');
45
+ expect(input).toHaveAttribute('data-status', 'success');
46
+ });
47
+
48
+ it('sets data-status to disabled when disabled', () => {
49
+ render(<Input label="Disabled" disabled />);
50
+ const input = screen.getByLabelText('Disabled');
51
+ expect(input).toHaveAttribute('data-status', 'disabled');
52
+ expect(input).toHaveAttribute('aria-disabled', 'true');
53
+ expect(input).toHaveAttribute('readonly');
54
+ });
55
+
56
+ it('overrides status with disabled when both are provided', () => {
57
+ render(<Input label="Both" status="error" disabled />);
58
+ const input = screen.getByLabelText('Both');
59
+ expect(input).toHaveAttribute('data-status', 'disabled');
60
+ });
61
+
62
+ it('forwards ref to the input element', () => {
63
+ const ref = createRef<HTMLInputElement>();
64
+ render(<Input label="Ref test" ref={ref} />);
65
+ expect(ref.current).toBeInstanceOf(HTMLInputElement);
66
+ expect(ref.current).toBe(screen.getByLabelText('Ref test'));
67
+ });
68
+
69
+ it('renders with different input types', () => {
70
+ render(<Input label="Password" type="password" />);
71
+ expect(screen.getByLabelText('Password')).toHaveAttribute('type', 'password');
72
+ });
73
+
74
+ it('renders type email', () => {
75
+ render(<Input label="Email" type="email" />);
76
+ expect(screen.getByLabelText('Email')).toHaveAttribute('type', 'email');
77
+ });
78
+
79
+ it('renders type number', () => {
80
+ render(<Input label="Age" type="number" />);
81
+ expect(screen.getByLabelText('Age')).toHaveAttribute('type', 'number');
82
+ });
83
+
84
+ it('renders a start enhancer', () => {
85
+ render(<Input label="With start" startEnhancer={<span data-testid="start-icon">$</span>} />);
86
+ expect(screen.getByTestId('start-icon')).toBeInTheDocument();
87
+ });
88
+
89
+ it('renders an end enhancer', () => {
90
+ render(<Input label="With end" endEnhancer={<span data-testid="end-icon">%</span>} />);
91
+ expect(screen.getByTestId('end-icon')).toBeInTheDocument();
92
+ });
93
+
94
+ it('renders both start and end enhancers', () => {
95
+ render(
96
+ <Input
97
+ label="Both enhancers"
98
+ startEnhancer={<span data-testid="start">S</span>}
99
+ endEnhancer={<span data-testid="end">E</span>}
100
+ />,
101
+ );
102
+ expect(screen.getByTestId('start')).toBeInTheDocument();
103
+ expect(screen.getByTestId('end')).toBeInTheDocument();
104
+ });
105
+
106
+ it('renders a function enhancer', () => {
107
+ render(
108
+ <Input label="Fn enhancer" startEnhancer={({ size }) => <span data-testid="fn-enhancer">{size}</span>} />,
109
+ );
110
+ expect(screen.getByTestId('fn-enhancer')).toBeInTheDocument();
111
+ });
112
+
113
+ it('forwards className to the input element', () => {
114
+ render(<Input label="Classy" className="my-custom-class" />);
115
+ const input = screen.getByLabelText('Classy');
116
+ expect(input).toHaveClass('my-custom-class');
117
+ });
118
+
119
+ it('sets aria-describedby linking to the description', () => {
120
+ render(<Input label="Described" description="Some helper text" />);
121
+ const input = screen.getByLabelText('Described');
122
+ const describedBy = input.getAttribute('aria-describedby');
123
+ expect(describedBy).toBeTruthy();
124
+ const descriptionEl = document.getElementById(describedBy!);
125
+ expect(descriptionEl).toBeInTheDocument();
126
+ expect(descriptionEl).toHaveTextContent('Some helper text');
127
+ });
128
+
129
+ it('renders description text', () => {
130
+ render(<Input label="With desc" description="Helpful description" />);
131
+ expect(screen.getByText('Helpful description')).toBeInTheDocument();
132
+ });
133
+
134
+ it('hides label when hideLabel is true', () => {
135
+ render(<Input label="Hidden" hideLabel />);
136
+ const label = screen.getByText('Hidden');
137
+ expect(label).toHaveClass('hidden');
138
+ });
139
+
140
+ it('calls onChange handler', async () => {
141
+ const handleChange = vi.fn();
142
+ const { user } = render(<Input label="Change test" onChange={handleChange} />);
143
+ await user.type(screen.getByLabelText('Change test'), 'a');
144
+ expect(handleChange).toHaveBeenCalled();
145
+ });
146
+
147
+ it('calls onFocus and onBlur handlers', async () => {
148
+ const handleFocus = vi.fn();
149
+ const handleBlur = vi.fn();
150
+ const { user } = render(<Input label="Focus test" onFocus={handleFocus} onBlur={handleBlur} />);
151
+ const input = screen.getByLabelText('Focus test');
152
+ await user.click(input);
153
+ expect(handleFocus).toHaveBeenCalled();
154
+ await user.tab();
155
+ expect(handleBlur).toHaveBeenCalled();
156
+ });
157
+
158
+ it('uses aria-label prop when label is not a string', () => {
159
+ render(<Input label={<span>Complex label</span>} aria-label="accessible label" />);
160
+ expect(screen.getByRole('textbox', { name: 'accessible label' })).toBeInTheDocument();
161
+ });
162
+
163
+ it('applies container override className', () => {
164
+ render(<Input label="Override test" overrides={{ container: { 'data-testid': 'wrapper' } }} />);
165
+ expect(screen.getByTestId('wrapper')).toBeInTheDocument();
166
+ });
167
+
168
+ it('spreads additional HTML input props', () => {
169
+ render(<Input label="Extra" maxLength={10} autoComplete="off" />);
170
+ const input = screen.getByLabelText('Extra');
171
+ expect(input).toHaveAttribute('maxlength', '10');
172
+ expect(input).toHaveAttribute('autocomplete', 'off');
173
+ });
174
+ });