autoworkflow 3.1.4 → 3.5.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.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Testing Library Skill
|
|
2
|
+
|
|
3
|
+
## Basic Test Structure
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
import { render, screen } from '@testing-library/react';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
7
|
+
import { vi, expect, test, describe, beforeEach } from 'vitest';
|
|
8
|
+
import { LoginForm } from './LoginForm';
|
|
9
|
+
|
|
10
|
+
describe('LoginForm', () => {
|
|
11
|
+
const mockOnSubmit = vi.fn();
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockOnSubmit.mockClear();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('submits form with valid data', async () => {
|
|
18
|
+
const user = userEvent.setup();
|
|
19
|
+
|
|
20
|
+
render(<LoginForm onSubmit={mockOnSubmit} />);
|
|
21
|
+
|
|
22
|
+
// Query by accessible role/label
|
|
23
|
+
await user.type(screen.getByLabelText('Email'), 'test@example.com');
|
|
24
|
+
await user.type(screen.getByLabelText('Password'), 'password123');
|
|
25
|
+
await user.click(screen.getByRole('button', { name: 'Sign in' }));
|
|
26
|
+
|
|
27
|
+
expect(mockOnSubmit).toHaveBeenCalledWith({
|
|
28
|
+
email: 'test@example.com',
|
|
29
|
+
password: 'password123',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('shows validation error for invalid email', async () => {
|
|
34
|
+
const user = userEvent.setup();
|
|
35
|
+
|
|
36
|
+
render(<LoginForm onSubmit={mockOnSubmit} />);
|
|
37
|
+
|
|
38
|
+
await user.type(screen.getByLabelText('Email'), 'invalid-email');
|
|
39
|
+
await user.click(screen.getByRole('button', { name: 'Sign in' }));
|
|
40
|
+
|
|
41
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Invalid email');
|
|
42
|
+
expect(mockOnSubmit).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
## Queries (Priority Order)
|
|
48
|
+
|
|
49
|
+
### 1. Accessible Queries (Preferred)
|
|
50
|
+
\`\`\`typescript
|
|
51
|
+
// By role (best - tests accessibility)
|
|
52
|
+
screen.getByRole('button', { name: 'Submit' });
|
|
53
|
+
screen.getByRole('textbox', { name: 'Email' });
|
|
54
|
+
screen.getByRole('checkbox', { name: 'Remember me' });
|
|
55
|
+
screen.getByRole('link', { name: 'Sign up' });
|
|
56
|
+
screen.getByRole('heading', { name: 'Welcome', level: 1 });
|
|
57
|
+
screen.getByRole('list');
|
|
58
|
+
screen.getByRole('listitem');
|
|
59
|
+
screen.getByRole('dialog');
|
|
60
|
+
screen.getByRole('alert');
|
|
61
|
+
screen.getByRole('navigation');
|
|
62
|
+
screen.getByRole('combobox'); // Select/dropdown
|
|
63
|
+
|
|
64
|
+
// By label (form elements)
|
|
65
|
+
screen.getByLabelText('Email');
|
|
66
|
+
screen.getByLabelText(/email/i); // Regex
|
|
67
|
+
|
|
68
|
+
// By placeholder (when no label)
|
|
69
|
+
screen.getByPlaceholderText('Enter your email');
|
|
70
|
+
|
|
71
|
+
// By text (non-interactive elements)
|
|
72
|
+
screen.getByText('Welcome to our app');
|
|
73
|
+
screen.getByText(/welcome/i); // Case-insensitive
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
### 2. Semantic Queries
|
|
77
|
+
\`\`\`typescript
|
|
78
|
+
// By alt text (images)
|
|
79
|
+
screen.getByAltText('Company logo');
|
|
80
|
+
|
|
81
|
+
// By title attribute
|
|
82
|
+
screen.getByTitle('Close');
|
|
83
|
+
|
|
84
|
+
// By display value (inputs)
|
|
85
|
+
screen.getByDisplayValue('john@example.com');
|
|
86
|
+
\`\`\`
|
|
87
|
+
|
|
88
|
+
### 3. Test IDs (Last Resort)
|
|
89
|
+
\`\`\`typescript
|
|
90
|
+
// When accessibility isn't possible
|
|
91
|
+
screen.getByTestId('custom-element');
|
|
92
|
+
// Requires: data-testid="custom-element" in JSX
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
## Query Variants
|
|
96
|
+
\`\`\`typescript
|
|
97
|
+
// getBy - throws if not found (single element)
|
|
98
|
+
screen.getByRole('button');
|
|
99
|
+
|
|
100
|
+
// queryBy - returns null if not found (for asserting absence)
|
|
101
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
102
|
+
|
|
103
|
+
// findBy - async, waits for element (returns Promise)
|
|
104
|
+
await screen.findByRole('alert');
|
|
105
|
+
|
|
106
|
+
// getAllBy - multiple elements (throws if none)
|
|
107
|
+
screen.getAllByRole('listitem');
|
|
108
|
+
|
|
109
|
+
// queryAllBy - multiple elements (empty array if none)
|
|
110
|
+
screen.queryAllByRole('listitem');
|
|
111
|
+
|
|
112
|
+
// findAllBy - async, multiple elements
|
|
113
|
+
await screen.findAllByRole('listitem');
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## userEvent (Interactions)
|
|
117
|
+
\`\`\`typescript
|
|
118
|
+
import userEvent from '@testing-library/user-event';
|
|
119
|
+
|
|
120
|
+
// Always setup userEvent
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
|
|
123
|
+
// Click
|
|
124
|
+
await user.click(element);
|
|
125
|
+
await user.dblClick(element);
|
|
126
|
+
await user.tripleClick(element); // Select all text
|
|
127
|
+
|
|
128
|
+
// Type
|
|
129
|
+
await user.type(input, 'Hello World');
|
|
130
|
+
await user.type(input, 'Hello{Enter}'); // Special keys
|
|
131
|
+
await user.clear(input);
|
|
132
|
+
|
|
133
|
+
// Special keys: {Enter}, {Escape}, {Backspace}, {Delete}, {Tab}
|
|
134
|
+
// Modifiers: {Shift>}A{/Shift} (shift+A)
|
|
135
|
+
|
|
136
|
+
// Keyboard shortcuts
|
|
137
|
+
await user.keyboard('{Control>}a{/Control}'); // Ctrl+A
|
|
138
|
+
await user.keyboard('{Shift>}{Enter}{/Shift}'); // Shift+Enter
|
|
139
|
+
|
|
140
|
+
// Select options
|
|
141
|
+
await user.selectOptions(select, 'option-value');
|
|
142
|
+
await user.selectOptions(select, ['value1', 'value2']); // Multi-select
|
|
143
|
+
|
|
144
|
+
// Checkbox/Radio
|
|
145
|
+
await user.click(checkbox); // Toggle
|
|
146
|
+
|
|
147
|
+
// Hover
|
|
148
|
+
await user.hover(element);
|
|
149
|
+
await user.unhover(element);
|
|
150
|
+
|
|
151
|
+
// Tab navigation
|
|
152
|
+
await user.tab();
|
|
153
|
+
await user.tab({ shift: true }); // Shift+Tab
|
|
154
|
+
|
|
155
|
+
// Clipboard
|
|
156
|
+
await user.copy();
|
|
157
|
+
await user.cut();
|
|
158
|
+
await user.paste('pasted text');
|
|
159
|
+
|
|
160
|
+
// File upload
|
|
161
|
+
const file = new File(['content'], 'file.png', { type: 'image/png' });
|
|
162
|
+
await user.upload(fileInput, file);
|
|
163
|
+
await user.upload(fileInput, [file1, file2]);
|
|
164
|
+
\`\`\`
|
|
165
|
+
|
|
166
|
+
## Async Utilities
|
|
167
|
+
\`\`\`typescript
|
|
168
|
+
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
|
|
169
|
+
|
|
170
|
+
// findBy - built-in waiting
|
|
171
|
+
const alert = await screen.findByRole('alert');
|
|
172
|
+
|
|
173
|
+
// waitFor - custom condition
|
|
174
|
+
await waitFor(() => {
|
|
175
|
+
expect(screen.getByText('Loaded')).toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// waitFor with options
|
|
179
|
+
await waitFor(
|
|
180
|
+
() => expect(mockFn).toHaveBeenCalled(),
|
|
181
|
+
{ timeout: 5000, interval: 100 }
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Wait for element removal
|
|
185
|
+
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
|
|
186
|
+
|
|
187
|
+
// Or with findBy variant
|
|
188
|
+
await waitForElementToBeRemoved(screen.queryByText('Loading...'));
|
|
189
|
+
\`\`\`
|
|
190
|
+
|
|
191
|
+
## Custom Matchers (jest-dom)
|
|
192
|
+
\`\`\`typescript
|
|
193
|
+
import '@testing-library/jest-dom';
|
|
194
|
+
|
|
195
|
+
// Visibility
|
|
196
|
+
expect(element).toBeVisible();
|
|
197
|
+
expect(element).not.toBeVisible();
|
|
198
|
+
expect(element).toBeInTheDocument();
|
|
199
|
+
|
|
200
|
+
// Form states
|
|
201
|
+
expect(input).toBeEnabled();
|
|
202
|
+
expect(input).toBeDisabled();
|
|
203
|
+
expect(input).toBeRequired();
|
|
204
|
+
expect(input).toBeValid();
|
|
205
|
+
expect(input).toBeInvalid();
|
|
206
|
+
expect(checkbox).toBeChecked();
|
|
207
|
+
expect(input).toHaveFocus();
|
|
208
|
+
|
|
209
|
+
// Content
|
|
210
|
+
expect(element).toHaveTextContent('Hello');
|
|
211
|
+
expect(element).toHaveTextContent(/hello/i);
|
|
212
|
+
expect(input).toHaveValue('test@example.com');
|
|
213
|
+
expect(input).toHaveDisplayValue('test@example.com');
|
|
214
|
+
expect(element).toBeEmptyDOMElement();
|
|
215
|
+
|
|
216
|
+
// Attributes
|
|
217
|
+
expect(element).toHaveAttribute('href', '/home');
|
|
218
|
+
expect(element).toHaveClass('btn-primary');
|
|
219
|
+
expect(element).toHaveStyle({ color: 'red' });
|
|
220
|
+
|
|
221
|
+
// Accessibility
|
|
222
|
+
expect(element).toHaveAccessibleName('Submit');
|
|
223
|
+
expect(element).toHaveAccessibleDescription('Submit the form');
|
|
224
|
+
\`\`\`
|
|
225
|
+
|
|
226
|
+
## Testing with Context/Providers
|
|
227
|
+
\`\`\`typescript
|
|
228
|
+
import { render } from '@testing-library/react';
|
|
229
|
+
import { ThemeProvider } from './ThemeContext';
|
|
230
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
231
|
+
|
|
232
|
+
// Custom render with providers
|
|
233
|
+
function customRender(ui: React.ReactElement, options = {}) {
|
|
234
|
+
const queryClient = new QueryClient({
|
|
235
|
+
defaultOptions: { queries: { retry: false } },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
239
|
+
return (
|
|
240
|
+
<QueryClientProvider client={queryClient}>
|
|
241
|
+
<ThemeProvider>
|
|
242
|
+
{children}
|
|
243
|
+
</ThemeProvider>
|
|
244
|
+
</QueryClientProvider>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return render(ui, { wrapper: Wrapper, ...options });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Usage
|
|
252
|
+
import { customRender as render } from '../test-utils';
|
|
253
|
+
|
|
254
|
+
test('renders with theme', () => {
|
|
255
|
+
render(<MyComponent />);
|
|
256
|
+
// Component has access to all providers
|
|
257
|
+
});
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
## Testing Hooks
|
|
261
|
+
\`\`\`typescript
|
|
262
|
+
import { renderHook, act } from '@testing-library/react';
|
|
263
|
+
import { useCounter } from './useCounter';
|
|
264
|
+
|
|
265
|
+
test('should increment counter', () => {
|
|
266
|
+
const { result } = renderHook(() => useCounter());
|
|
267
|
+
|
|
268
|
+
expect(result.current.count).toBe(0);
|
|
269
|
+
|
|
270
|
+
act(() => {
|
|
271
|
+
result.current.increment();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(result.current.count).toBe(1);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// With initial props
|
|
278
|
+
test('should start with initial value', () => {
|
|
279
|
+
const { result, rerender } = renderHook(
|
|
280
|
+
({ initialCount }) => useCounter(initialCount),
|
|
281
|
+
{ initialProps: { initialCount: 10 } }
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(result.current.count).toBe(10);
|
|
285
|
+
|
|
286
|
+
// Re-render with new props
|
|
287
|
+
rerender({ initialCount: 20 });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Async hooks
|
|
291
|
+
test('should fetch data', async () => {
|
|
292
|
+
const { result } = renderHook(() => useFetchUser('123'));
|
|
293
|
+
|
|
294
|
+
expect(result.current.loading).toBe(true);
|
|
295
|
+
|
|
296
|
+
await waitFor(() => {
|
|
297
|
+
expect(result.current.loading).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(result.current.data).toEqual({ id: '123', name: 'John' });
|
|
301
|
+
});
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
## Debugging
|
|
305
|
+
\`\`\`typescript
|
|
306
|
+
import { screen, prettyDOM } from '@testing-library/react';
|
|
307
|
+
|
|
308
|
+
// Print DOM to console
|
|
309
|
+
screen.debug(); // Entire document
|
|
310
|
+
screen.debug(element); // Specific element
|
|
311
|
+
screen.debug(element, 20000); // Increase max length
|
|
312
|
+
|
|
313
|
+
// Get pretty DOM string
|
|
314
|
+
console.log(prettyDOM(element));
|
|
315
|
+
|
|
316
|
+
// Log testing playground URL
|
|
317
|
+
screen.logTestingPlaygroundURL();
|
|
318
|
+
|
|
319
|
+
// Configure debug output
|
|
320
|
+
import { configure } from '@testing-library/react';
|
|
321
|
+
configure({ defaultHidden: true }); // Show hidden elements
|
|
322
|
+
\`\`\`
|
|
323
|
+
|
|
324
|
+
## Setup File (vitest.setup.ts)
|
|
325
|
+
\`\`\`typescript
|
|
326
|
+
import '@testing-library/jest-dom/vitest';
|
|
327
|
+
import { cleanup } from '@testing-library/react';
|
|
328
|
+
import { afterEach, vi } from 'vitest';
|
|
329
|
+
|
|
330
|
+
// Cleanup after each test
|
|
331
|
+
afterEach(() => {
|
|
332
|
+
cleanup();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Mock window.matchMedia
|
|
336
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
337
|
+
writable: true,
|
|
338
|
+
value: vi.fn().mockImplementation((query) => ({
|
|
339
|
+
matches: false,
|
|
340
|
+
media: query,
|
|
341
|
+
onchange: null,
|
|
342
|
+
addEventListener: vi.fn(),
|
|
343
|
+
removeEventListener: vi.fn(),
|
|
344
|
+
})),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Mock IntersectionObserver
|
|
348
|
+
class MockIntersectionObserver {
|
|
349
|
+
observe = vi.fn();
|
|
350
|
+
disconnect = vi.fn();
|
|
351
|
+
unobserve = vi.fn();
|
|
352
|
+
}
|
|
353
|
+
window.IntersectionObserver = MockIntersectionObserver as any;
|
|
354
|
+
\`\`\`
|
|
355
|
+
|
|
356
|
+
## ❌ DON'T
|
|
357
|
+
- Use \`getByTestId\` when accessible queries work
|
|
358
|
+
- Use \`fireEvent\` when \`userEvent\` works
|
|
359
|
+
- Test implementation details (state, props, methods)
|
|
360
|
+
- Test third-party library internals
|
|
361
|
+
- Use arbitrary waits (\`setTimeout\`)
|
|
362
|
+
- Query by class or ID
|
|
363
|
+
|
|
364
|
+
## ✅ DO
|
|
365
|
+
- Query by role, label, text (accessibility first)
|
|
366
|
+
- Use \`userEvent\` for interactions
|
|
367
|
+
- Test from user's perspective
|
|
368
|
+
- Use \`findBy\` for async elements
|
|
369
|
+
- Use \`queryBy\` to assert absence
|
|
370
|
+
- Create custom render with providers
|
|
371
|
+
- Use \`screen\` for queries (not destructured render)
|