Skip to content

Commit 9e0f81c

Browse files
committed
fix(ComboBox): add tests and stories
1 parent b5c655b commit 9e0f81c

File tree

5 files changed

+565
-66
lines changed

5 files changed

+565
-66
lines changed

.cursor/rules/storybook.mdc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ alwaysApply: false
77
### Stories Files (.stories.tsx)
88
- Import types: `import type { Meta, StoryObj } from '@storybook/react-vite';`
99
- Import `StoryFn` for custom template functions
10-
- For interactive tests: `import { within, userEvent } from '@testing-library/react';` or `import { userEvent, within } from 'storybook/test';`
10+
- For interactive tests: `import { userEvent, within } from 'storybook/test';` (NOT from `@testing-library/react`)
1111

1212
### Documentation Files (.docs.mdx)
1313
- `import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks';`
@@ -103,6 +103,8 @@ export const Interactive: StoryObj = {
103103
};
104104
```
105105

106+
**Important:** Always import `userEvent` and `within` from `'storybook/test'` in story files. This ensures they respect Storybook's configuration (e.g., `testIdAttribute: 'data-qa'` set in `.storybook/preview.jsx`). Do NOT use `@testing-library/react` imports in stories.
107+
106108
## MDX Documentation Structure
107109

108110
```mdx

.cursor/rules/tests.mdc

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
globs: *.test.tsx
3+
alwaysApply: false
4+
---
5+
6+
# Testing Rules for UI Kit
7+
8+
## Setup
9+
10+
- Place test files next to components with `.test.tsx` extension
11+
- Mock internal warnings: `jest.mock('../../../_internal/hooks/use-warn')`
12+
- Define test data at the top of `describe` blocks
13+
14+
## Render Functions
15+
16+
**Choose based on component requirements:**
17+
- `render()` - Simple components (Button, Text, standalone UI elements)
18+
- `renderWithRoot()` - Components using overlays, popovers, portals, modals, or complex interactions (Select, ComboBox, Menu, Dialog, etc.)
19+
- `renderWithForm()` - Form-integrated components (returns `{ formInstance, ...renderResult }`)
20+
21+
Note: Root provides ModalProvider, PortalProvider, EventBusProvider, NotificationsProvider, and styled-components context. Most interactive components need it, but simple presentational components don't.
22+
23+
## User Interactions
24+
25+
**Always use `userEvent` for interactions:**
26+
- `await userEvent.click(element)`
27+
- `await userEvent.type(input, 'text')`
28+
- `await userEvent.clear(input)`
29+
- `await userEvent.keyboard('{Enter}')` / `'{Escape}'` / `'{ArrowDown}'` / `'{Home}'` / etc.
30+
- `await userEvent.tab()`
31+
32+
**Focus management:**
33+
```tsx
34+
await act(async () => {
35+
element.focus();
36+
element.blur();
37+
});
38+
```
39+
40+
## Async Testing
41+
42+
Use `waitFor()` for async state changes:
43+
```tsx
44+
await userEvent.click(button);
45+
46+
await waitFor(() => {
47+
expect(element).toBeInTheDocument();
48+
});
49+
```
50+
51+
Wait for removal: `await waitForElementToBeRemoved(() => queryByRole('dialog'))`
52+
53+
## Common Patterns
54+
55+
### Basic Rendering & User Interactions
56+
```tsx
57+
it('should handle button press', async () => {
58+
const onPress = jest.fn();
59+
const { getByRole } = render(<Button onPress={onPress}>Label</Button>);
60+
61+
await userEvent.click(getByRole('button'));
62+
63+
expect(onPress).toHaveBeenCalled();
64+
});
65+
```
66+
67+
### Popover/Overlay State
68+
```tsx
69+
it('should open and close popover', async () => {
70+
const { getByRole, queryByRole } = renderWithRoot(<Select label="test">...</Select>);
71+
72+
expect(queryByRole('listbox')).not.toBeInTheDocument();
73+
74+
await userEvent.click(getByRole('button'));
75+
await waitFor(() => expect(queryByRole('listbox')).toBeInTheDocument());
76+
77+
await userEvent.keyboard('{Escape}');
78+
await waitFor(() => expect(queryByRole('listbox')).not.toBeInTheDocument());
79+
});
80+
```
81+
82+
### Form Integration
83+
```tsx
84+
// Modern Form integration
85+
it('should integrate with Form', async () => {
86+
const { getByRole, formInstance } = renderWithForm(
87+
<TextInput name="test" label="test" />
88+
);
89+
90+
await userEvent.type(getByRole('textbox'), 'Hello');
91+
expect(formInstance.getFieldValue('test')).toBe('Hello');
92+
});
93+
94+
// Legacy Field wrapper
95+
it('should interop with legacy <Field />', async () => {
96+
const { getByRole, formInstance } = renderWithForm(
97+
<Field name="test"><Switch aria-label="test" /></Field>
98+
);
99+
100+
await userEvent.click(getByRole('switch'));
101+
expect(formInstance.getFieldValue('test')).toBe(true);
102+
});
103+
```
104+
105+
### Props & State Changes (Rerender)
106+
```tsx
107+
it('should respect state props', async () => {
108+
const { getByRole, rerender } = renderWithRoot(<Component isDisabled />);
109+
110+
expect(getByRole('button')).toBeDisabled();
111+
112+
rerender(<Component isReadOnly />);
113+
expect(getByRole('button')).toHaveAttribute('readonly');
114+
});
115+
```
116+
117+
### Keyboard Navigation
118+
```tsx
119+
it('should handle keyboard navigation', async () => {
120+
const { getByRole } = renderWithRoot(<ComboBox label="test">...</ComboBox>);
121+
const input = getByRole('combobox');
122+
123+
await act(async () => {
124+
input.focus();
125+
await userEvent.keyboard('{ArrowDown}');
126+
});
127+
128+
await waitFor(() => expect(input).toHaveAttribute('aria-expanded', 'true'));
129+
});
130+
```
131+
132+
### Filtering/Search
133+
```tsx
134+
it('should filter options', async () => {
135+
const { getByRole, getAllByRole } = renderWithRoot(<ComboBox label="test">{items}</ComboBox>);
136+
137+
await userEvent.type(getByRole('combobox'), 'red');
138+
139+
await waitFor(() => {
140+
expect(getAllByRole('option')).toHaveLength(2);
141+
});
142+
});
143+
```
144+
145+
### Validation
146+
```tsx
147+
it('should display validation errors', async () => {
148+
const { getByRole, getByText } = renderWithForm(
149+
<TextInput name="test" label="test" rules={[{ required: true, message: 'Required' }]} />
150+
);
151+
152+
await userEvent.type(getByRole('textbox'), 'a');
153+
await userEvent.clear(getByRole('textbox'));
154+
await userEvent.tab();
155+
156+
await waitFor(() => expect(getByText('Required')).toBeInTheDocument());
157+
});
158+
```
159+
160+
### Parameterized Tests
161+
```tsx
162+
it.each([
163+
['aria-label', { 'aria-label': 'test' }],
164+
['label', { label: 'test' }],
165+
])('should not warn if %s is provided', (_, props) => {
166+
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
167+
render(<Button {...props} />);
168+
expect(spy).not.toHaveBeenCalled();
169+
spy.mockRestore();
170+
});
171+
```
172+
173+
## Query Selectors Priority
174+
175+
1. `getByRole()` - most accessible
176+
2. `getByLabelText()` - form fields
177+
3. `getByTestId()` - QA attributes (`qa` prop)
178+
4. `getByText()` - text content
179+
180+
Use `query*` when checking non-existence, `getAll*` for multiple elements.
181+
182+
## Common Matchers
183+
184+
- `toBeInTheDocument()`, `toHaveTextContent()`, `toHaveAttribute()`, `toHaveValue()`
185+
- `toBeDisabled()`, `toBeChecked()`
186+
- `toHaveBeenCalled()`, `toHaveBeenCalledWith()`, `toHaveBeenCalledTimes()`
187+
188+
## Best Practices
189+
190+
### Do
191+
- ✅ Test user-facing behavior, not implementation
192+
- ✅ Use semantic queries (`getByRole`)
193+
- ✅ Wait for async changes with `waitFor()`
194+
- ✅ Test accessibility (ARIA, keyboard navigation)
195+
- ✅ Test form integration when supported
196+
- ✅ Use descriptive test names
197+
198+
### Don't
199+
- ❌ Don't test implementation details
200+
- ❌ Don't overuse `act()` (userEvent handles it)
201+
- ❌ Don't query by class names when semantic queries work
202+
- ❌ Don't test `styles` or simple layout props (e.g., `icon` that just renders to slot)
203+
- ❌ Don't forget to mock warnings for clean output
204+
205+
## Debugging
206+
207+
- `screen.debug()` - print DOM
208+
- `waitFor(() => {...}, { timeout: 5000 })` - custom timeout

0 commit comments

Comments
 (0)