@mohantn/gate-keeper 2.2.2 → 2.2.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/README.md +84 -113
- package/dist/cli-entry.d.ts +2 -0
- package/dist/cli-entry.d.ts.map +1 -1
- package/dist/cli-entry.js +201 -7
- package/dist/cli-entry.js.map +1 -1
- package/package.json +6 -6
- package/scripts/setup.sh +112 -34
- package/.github/instructions/dotnet-api-integration.instructions.md +0 -416
- package/.github/instructions/dotnet-development.instructions.md +0 -353
- package/.github/instructions/dotnet-testing.instructions.md +0 -406
- package/.github/instructions/react-development.instructions.md +0 -315
- package/.github/instructions/react-testing-optimization.instructions.md +0 -373
- package/.github/instructions/uiux.instructions.md +0 -261
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
# React 17 Development Instructions
|
|
2
|
-
|
|
3
|
-
**Scope**: All `.tsx`/`.ts` files | **Version**: 3.0 | **Tags**: `react`, `typescript`, `hooks`, `emotion`, `react-query`
|
|
4
|
-
|
|
5
|
-
**Related Files**: See [react-testing-optimization.instructions.md](./react-testing-optimization.instructions.md) for testing & optimization
|
|
6
|
-
|
|
7
|
-
## 🔴 MANDATORY: Import Organization
|
|
8
|
-
Group: **React** → **Third-party** → **dh-component-library/store-components** → **Local Components** → **Hooks** → **Utils/API** → **Types**
|
|
9
|
-
|
|
10
|
-
```typescript
|
|
11
|
-
import { useState, useCallback } from 'react';
|
|
12
|
-
import { useQuery } from 'react-query';
|
|
13
|
-
import styled from '@emotion/styled';
|
|
14
|
-
import { Button, Flex } from 'dh-component-library';
|
|
15
|
-
import { PageCard } from 'store-components';
|
|
16
|
-
import { FormElementText } from 'src/Components/Forms/FormElements';
|
|
17
|
-
import { useGetActivationPrice } from 'src/Hooks/query/pricingData';
|
|
18
|
-
import { activationPrice } from 'src/api';
|
|
19
|
-
import { t } from 'src/I18n';
|
|
20
|
-
import { Activation, ThemeType } from 'src/types';
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Core Patterns
|
|
24
|
-
|
|
25
|
-
| Pattern | Usage |
|
|
26
|
-
|---------|-------|
|
|
27
|
-
| **Functional Components** | `export function Component({ prop }: Props)` OR `const Component: React.FC<Props>` |
|
|
28
|
-
| **Emotion Styling** | `styled(Component)<Props>(({ theme }) => ({ ... }))` with ThemeType |
|
|
29
|
-
| **React Query v3** | `useQuery(['key', id, adAccountId], fn, { onError: onErrorPopToast })` |
|
|
30
|
-
| **Context Providers** | Cascade: Theme → User → Configuration → PostHog → Domain contexts |
|
|
31
|
-
| **Custom Hooks** | Prefix `use*`, return object: `{ data, isLoading, error }` |
|
|
32
|
-
| **Test IDs** | Hierarchical: `DATA_TEST_ID.section.field.name` |
|
|
33
|
-
| **i18n** | All text: `t('translationKey')` from `src/I18n` |
|
|
34
|
-
|
|
35
|
-
## Functional Components
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
interface ProductProps {
|
|
39
|
-
productId: string;
|
|
40
|
-
onSelect?: (id: string) => void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const Product: React.FC<ProductProps> = ({ productId, onSelect }) => {
|
|
44
|
-
const [isLoading, setIsLoading] = React.useState(false);
|
|
45
|
-
|
|
46
|
-
const handleClick = React.useCallback(() => {
|
|
47
|
-
setIsLoading(true);
|
|
48
|
-
onSelect?.(productId);
|
|
49
|
-
}, [productId, onSelect]);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div onClick={handleClick} className={isLoading ? 'loading' : ''}>
|
|
53
|
-
{productId}
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export default Product;
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Custom Hooks
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
interface UseProductReturn {
|
|
65
|
-
product: Product | null;
|
|
66
|
-
loading: boolean;
|
|
67
|
-
error: Error | null;
|
|
68
|
-
refetch: () => Promise<void>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export const useProduct = (productId: string): UseProductReturn => {
|
|
72
|
-
const [product, setProduct] = React.useState<Product | null>(null);
|
|
73
|
-
const [loading, setLoading] = React.useState(false);
|
|
74
|
-
const [error, setError] = React.useState<Error | null>(null);
|
|
75
|
-
|
|
76
|
-
const fetchProduct = React.useCallback(async () => {
|
|
77
|
-
setLoading(true);
|
|
78
|
-
try {
|
|
79
|
-
const response = await fetch(`/api/products/${productId}`);
|
|
80
|
-
if (!response.ok) throw new Error('Failed to fetch');
|
|
81
|
-
const data = await response.json();
|
|
82
|
-
setProduct(data);
|
|
83
|
-
setError(null);
|
|
84
|
-
} catch (err) {
|
|
85
|
-
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
86
|
-
} finally {
|
|
87
|
-
setLoading(false);
|
|
88
|
-
}
|
|
89
|
-
}, [productId]);
|
|
90
|
-
|
|
91
|
-
React.useEffect(() => {
|
|
92
|
-
fetchProduct();
|
|
93
|
-
}, [fetchProduct]);
|
|
94
|
-
|
|
95
|
-
return { product, loading, error, refetch: fetchProduct };
|
|
96
|
-
};
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Context API
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
interface ProductContextType {
|
|
103
|
-
selectedProduct: Product | null;
|
|
104
|
-
selectProduct: (product: Product) => void;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const ProductContext = React.createContext<ProductContextType | undefined>(undefined);
|
|
108
|
-
|
|
109
|
-
export const ProductProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
110
|
-
const [selectedProduct, setSelectedProduct] = React.useState<Product | null>(null);
|
|
111
|
-
|
|
112
|
-
const value: ProductContextType = {
|
|
113
|
-
selectedProduct,
|
|
114
|
-
selectProduct: (product) => setSelectedProduct(product),
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
return <ProductContext.Provider value={value}>{children}</ProductContext.Provider>;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export const useProductContext = () => {
|
|
121
|
-
const context = React.useContext(ProductContext);
|
|
122
|
-
if (!context) throw new Error('useProductContext must be used within ProductProvider');
|
|
123
|
-
return context;
|
|
124
|
-
};
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## React Query v3 (Project Standard)
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
import { useQuery, UseQueryOptions } from 'react-query';
|
|
131
|
-
import { AxiosError } from 'axios';
|
|
132
|
-
import { onErrorPopToast } from './helpers/onErrorPopToast';
|
|
133
|
-
import { t } from 'src/I18n';
|
|
134
|
-
|
|
135
|
-
// Query hook pattern
|
|
136
|
-
export function useGetActivationPrice(options?: UseQueryOptions<Pricing, AxiosError>) {
|
|
137
|
-
const { activationMerged } = useStoreMediaContext();
|
|
138
|
-
const { adAccountId } = useUserContext();
|
|
139
|
-
|
|
140
|
-
const { data, isLoading, error, isError } = useQuery<Pricing, AxiosError>(
|
|
141
|
-
['activationPrice', activationMerged.activationId, adAccountId],
|
|
142
|
-
() => activationPrice(activationMerged.activationId),
|
|
143
|
-
{
|
|
144
|
-
onError: (err) => onErrorPopToast(err, t('getPricingError')),
|
|
145
|
-
enabled: !!activationMerged.activationId,
|
|
146
|
-
...options,
|
|
147
|
-
}
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
return { dataActivationPrice: data, isLoadingActivationPrice: isLoading, isErrorActivationPrice: isError };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// CRITICAL: Always include adAccountId in query keys for multi-tenancy
|
|
154
|
-
// Use descriptive return names: data{EntityName}, isLoading{EntityName}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Emotion Styling (Project Standard)
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import styled from '@emotion/styled';
|
|
161
|
-
import { Flex } from 'dh-component-library';
|
|
162
|
-
import { ThemeType } from 'src/types';
|
|
163
|
-
|
|
164
|
-
// Styled component with theme
|
|
165
|
-
const FilterActions = styled(Flex)<{ theme?: ThemeType }>(({ theme }) => ({
|
|
166
|
-
gap: '16px',
|
|
167
|
-
justifyContent: 'flex-end',
|
|
168
|
-
marginTop: '24px',
|
|
169
|
-
paddingTop: '16px',
|
|
170
|
-
borderTop: `1px solid ${theme?.palette?.greyscale?.[200] || '#e5e7eb'}`,
|
|
171
|
-
|
|
172
|
-
'& button': {
|
|
173
|
-
minWidth: '44px',
|
|
174
|
-
minHeight: '44px',
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
'@media (max-width: 768px)': {
|
|
178
|
-
flexDirection: 'column',
|
|
179
|
-
gap: '12px',
|
|
180
|
-
},
|
|
181
|
-
}));
|
|
182
|
-
|
|
183
|
-
// Always use optional chaining for theme properties
|
|
184
|
-
// Include responsive breakpoints: 768px (tablet), 1024px (desktop)
|
|
185
|
-
// Ensure touch targets ≥ 44px for accessibility
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Type Safety
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
// ✅ Good: Proper typing with interfaces and generics
|
|
192
|
-
interface Product {
|
|
193
|
-
id: string;
|
|
194
|
-
name: string;
|
|
195
|
-
price: number;
|
|
196
|
-
retailerId: string;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
interface UseAsyncState<T> {
|
|
200
|
-
data: T | null;
|
|
201
|
-
loading: boolean;
|
|
202
|
-
error: Error | null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const useAsync = <T,>(fn: () => Promise<T>): UseAsyncState<T> => {
|
|
206
|
-
// implementation
|
|
207
|
-
return { data: null, loading: false, error: null };
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
// ❌ Bad: No typing, unclear props/returns
|
|
211
|
-
const useAsync = (fn) => {
|
|
212
|
-
return { data: null, loading: false, error: null };
|
|
213
|
-
};
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Project Dependencies
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
# Core (React 17 - DO NOT upgrade to 18)
|
|
220
|
-
yarn add react@^17.0.2 react-dom@^17.0.2
|
|
221
|
-
|
|
222
|
-
# State & Data Fetching
|
|
223
|
-
yarn add react-query@^3.39.3 axios@^1.12.0
|
|
224
|
-
|
|
225
|
-
# Styling
|
|
226
|
-
yarn add @emotion/react @emotion/styled
|
|
227
|
-
yarn add dh-component-library@5.37.47 store-components@^0.0.47
|
|
228
|
-
|
|
229
|
-
# Routing (Dual setup required for portal integration)
|
|
230
|
-
yarn add react-router-dom@5.1.2
|
|
231
|
-
yarn add react-router-dom6@npm:react-router-dom@^6.2.1
|
|
232
|
-
|
|
233
|
-
# Testing
|
|
234
|
-
yarn add -D @testing-library/react @testing-library/user-event
|
|
235
|
-
yarn add -D msw@^1.1.0 cypress dh-cypress-support
|
|
236
|
-
|
|
237
|
-
# Utilities
|
|
238
|
-
yarn add lodash ramda react-toastify file-saver react-error-boundary
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
## Project-Specific Patterns
|
|
242
|
-
|
|
243
|
-
### Context Cascading Order
|
|
244
|
-
```typescript
|
|
245
|
-
App → ThemeProvider → UserContext → ConfigurationProvider → PostHogProvider
|
|
246
|
-
→ StoreMediaProvider → RegionsEstatesProvider → SelectedStoresProvider
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### Multi-Step Navigation
|
|
250
|
-
```typescript
|
|
251
|
-
// Each step: { title, route, component, provider, isValid?, subSteps? }
|
|
252
|
-
const step = {
|
|
253
|
-
title: t('stepTitle'),
|
|
254
|
-
route: 'step-route',
|
|
255
|
-
component: StepComponent,
|
|
256
|
-
provider: StepProvider,
|
|
257
|
-
};
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### API Call Pattern
|
|
261
|
-
```typescript
|
|
262
|
-
import { apiCall } from 'src/api/utils';
|
|
263
|
-
|
|
264
|
-
export const getActivation = (id: string): Promise<Activation> =>
|
|
265
|
-
apiCall({ url: `/api/booking-store/${id}`, method: 'GET' });
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### Test ID Convention
|
|
269
|
-
```typescript
|
|
270
|
-
export const DATA_TEST_ID = {
|
|
271
|
-
stepName: {
|
|
272
|
-
section: 'section-step-name',
|
|
273
|
-
field: { fieldName: 'field-name' },
|
|
274
|
-
button: { submit: 'step-submit-button' },
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
## Quick Checklist ✅
|
|
280
|
-
|
|
281
|
-
- Imports: React → Third-party → Components → Hooks/Utils → Types
|
|
282
|
-
- Props typed with interfaces
|
|
283
|
-
- Components are `React.FC<Props>`
|
|
284
|
-
- All handlers wrapped in `useCallback`
|
|
285
|
-
- `useEffect` dependencies correct
|
|
286
|
-
- Custom hooks follow naming: `use*`
|
|
287
|
-
- State lifted appropriately or use Context
|
|
288
|
-
- React Query for server state
|
|
289
|
-
- Forms use validation
|
|
290
|
-
- Error boundary wraps app/sections
|
|
291
|
-
- Memoization with `React.memo()`, `useMemo()`, `useCallback()`
|
|
292
|
-
- No `any` types
|
|
293
|
-
- No side effects in render
|
|
294
|
-
|
|
295
|
-
## Common Mistakes ❌
|
|
296
|
-
|
|
297
|
-
- Import order not organized
|
|
298
|
-
- Missing TypeScript types
|
|
299
|
-
- useCallback missing dependencies
|
|
300
|
-
- useEffect infinite loops (missing/wrong dependencies)
|
|
301
|
-
- Mixing server & client state management
|
|
302
|
-
- Inline object/array in props (creates new refs each render)
|
|
303
|
-
- Props mutation directly
|
|
304
|
-
- useEffect with async directly (use wrapper function)
|
|
305
|
-
- Missing error boundary
|
|
306
|
-
- Not memoizing expensive computations
|
|
307
|
-
- Rendering without keys in lists
|
|
308
|
-
- `console.log()` left in production code
|
|
309
|
-
|
|
310
|
-
## Resources
|
|
311
|
-
|
|
312
|
-
- [React Query v3](https://react-query-v3.tanstack.com)
|
|
313
|
-
- [Emotion](https://emotion.sh)
|
|
314
|
-
- [dh-component-library](https://dh-component-library-docs)
|
|
315
|
-
- [TypeScript React](https://react-typescript-cheatsheet.netlify.app/)
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
# React 17 Testing & Optimization Instructions
|
|
2
|
-
|
|
3
|
-
**Scope**: Testing, Performance, Error Handling | **Version**: 3.0 | **Tags**: `testing`, `msw`, `performance`, `error-handling`
|
|
4
|
-
|
|
5
|
-
**Related Files**: See [react-development.instructions.md](./react-development.instructions.md) for core patterns, [uiux.instructions.md](./uiux.instructions.md) for UI patterns
|
|
6
|
-
|
|
7
|
-
## Error Handling
|
|
8
|
-
|
|
9
|
-
### Error Boundary Pattern
|
|
10
|
-
```typescript
|
|
11
|
-
import { ErrorBoundary } from 'react-error-boundary';
|
|
12
|
-
import { ErrorPage } from 'dh-component-library';
|
|
13
|
-
import { onErrorPopToast } from 'src/Hooks/query/helpers/onErrorPopToast';
|
|
14
|
-
import { t } from 'src/I18n';
|
|
15
|
-
|
|
16
|
-
// Wrap routes with ErrorBoundary
|
|
17
|
-
<ErrorBoundary FallbackComponent={ErrorPage}>
|
|
18
|
-
<Routes>...</Routes>
|
|
19
|
-
</ErrorBoundary>
|
|
20
|
-
|
|
21
|
-
// API error handling pattern
|
|
22
|
-
const { data, isLoading } = useQuery(
|
|
23
|
-
['key', id, adAccountId],
|
|
24
|
-
() => apiCall(),
|
|
25
|
-
{ onError: (err) => onErrorPopToast(err, t('errorMessageKey')) }
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
// Always use i18n keys for error messages, never hardcoded strings
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Suspense & Lazy Loading
|
|
32
|
-
|
|
33
|
-
```typescript
|
|
34
|
-
const ProductDetails = React.lazy(() => import('./ProductDetails'));
|
|
35
|
-
|
|
36
|
-
const App: React.FC = () => (
|
|
37
|
-
<ErrorBoundary>
|
|
38
|
-
<React.Suspense fallback={<div>Loading...</div>}>
|
|
39
|
-
<ProductDetails productId="123" />
|
|
40
|
-
</React.Suspense>
|
|
41
|
-
</ErrorBoundary>
|
|
42
|
-
);
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Memoization & Optimization
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
interface ProductListItemProps {
|
|
49
|
-
product: Product;
|
|
50
|
-
onSelect: (id: string) => void;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const ProductListItem = React.memo<ProductListItemProps>(({ product, onSelect }) => {
|
|
54
|
-
const handleClick = React.useCallback(() => {
|
|
55
|
-
onSelect(product.id);
|
|
56
|
-
}, [product.id, onSelect]);
|
|
57
|
-
|
|
58
|
-
const displayPrice = React.useMemo(() => {
|
|
59
|
-
return `$${(product.price / 100).toFixed(2)}`;
|
|
60
|
-
}, [product.price]);
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div onClick={handleClick}>
|
|
64
|
-
{product.name} - {displayPrice}
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
export default ProductListItem;
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### When to Memoize
|
|
73
|
-
|
|
74
|
-
**Use `React.memo`**:
|
|
75
|
-
- Component renders often with same props
|
|
76
|
-
- Component is expensive to render
|
|
77
|
-
- Parent re-renders frequently
|
|
78
|
-
|
|
79
|
-
**Use `useMemo`**:
|
|
80
|
-
- Expensive calculations
|
|
81
|
-
- Creating objects/arrays passed as props
|
|
82
|
-
- Filtering/transforming large datasets
|
|
83
|
-
|
|
84
|
-
**Use `useCallback`**:
|
|
85
|
-
- Functions passed as props to memoized components
|
|
86
|
-
- Functions in dependency arrays
|
|
87
|
-
- Event handlers passed to child components
|
|
88
|
-
|
|
89
|
-
**DON'T memoize**:
|
|
90
|
-
- Cheap calculations
|
|
91
|
-
- Primitive values
|
|
92
|
-
- Components that always render with different props
|
|
93
|
-
|
|
94
|
-
## Testing (MSW + React Testing Library)
|
|
95
|
-
|
|
96
|
-
### Test Setup with MSW
|
|
97
|
-
```typescript
|
|
98
|
-
import { render, screen } from 'src/test/app-test-utils';
|
|
99
|
-
import { ComponentTestContainer } from 'src/test/app-test-utils';
|
|
100
|
-
import { server } from 'src/test/msw-api/server/test-server';
|
|
101
|
-
import { rest } from 'msw';
|
|
102
|
-
import { DATA_TEST_ID } from 'src/test/generic-ids';
|
|
103
|
-
|
|
104
|
-
describe('Component', () => {
|
|
105
|
-
it('renders with context', () => {
|
|
106
|
-
const mockActivation = { activationId: '123', name: 'Test' };
|
|
107
|
-
|
|
108
|
-
render(
|
|
109
|
-
<ComponentTestContainer state={{ activationMerged: mockActivation }}>
|
|
110
|
-
<Component />
|
|
111
|
-
</ComponentTestContainer>
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
expect(screen.getByTestId(DATA_TEST_ID.section.field)).toBeInTheDocument();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('overrides MSW response', () => {
|
|
118
|
-
server.use(
|
|
119
|
-
rest.get('/api/booking-store/:id', (req, res, ctx) =>
|
|
120
|
-
res(ctx.json({ data: 'override' }))
|
|
121
|
-
)
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
render(<Component />);
|
|
125
|
-
// Test with overridden response
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Use app-test-utils for pre-configured providers
|
|
130
|
-
// MSW automatically mocked in setupTests.ts
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Testing Hooks
|
|
134
|
-
```typescript
|
|
135
|
-
import { renderHook, waitFor } from '@testing-library/react';
|
|
136
|
-
import { QueryClient, QueryClientProvider } from 'react-query';
|
|
137
|
-
|
|
138
|
-
describe('useProduct', () => {
|
|
139
|
-
it('fetches product data', async () => {
|
|
140
|
-
const queryClient = new QueryClient();
|
|
141
|
-
const wrapper = ({ children }) => (
|
|
142
|
-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const { result } = renderHook(() => useProduct('123'), { wrapper });
|
|
146
|
-
|
|
147
|
-
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
148
|
-
expect(result.current.product).toBeDefined();
|
|
149
|
-
expect(result.current.error).toBeNull();
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Testing User Interactions
|
|
155
|
-
```typescript
|
|
156
|
-
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
157
|
-
import userEvent from '@testing-library/user-event';
|
|
158
|
-
|
|
159
|
-
describe('ProductForm', () => {
|
|
160
|
-
it('submits form with valid data', async () => {
|
|
161
|
-
const mockSubmit = jest.fn();
|
|
162
|
-
render(<ProductForm onSubmit={mockSubmit} />);
|
|
163
|
-
|
|
164
|
-
// Type into inputs
|
|
165
|
-
await userEvent.type(screen.getByLabelText(/name/i), 'Test Product');
|
|
166
|
-
await userEvent.type(screen.getByLabelText(/price/i), '99.99');
|
|
167
|
-
|
|
168
|
-
// Click submit
|
|
169
|
-
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
|
|
170
|
-
|
|
171
|
-
// Assert
|
|
172
|
-
await waitFor(() => {
|
|
173
|
-
expect(mockSubmit).toHaveBeenCalledWith({
|
|
174
|
-
name: 'Test Product',
|
|
175
|
-
price: 99.99,
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('shows validation errors', async () => {
|
|
181
|
-
render(<ProductForm onSubmit={jest.fn()} />);
|
|
182
|
-
|
|
183
|
-
// Submit without filling
|
|
184
|
-
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
|
|
185
|
-
|
|
186
|
-
// Assert errors shown
|
|
187
|
-
expect(await screen.findByText(/name is required/i)).toBeInTheDocument();
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Testing Async Operations
|
|
193
|
-
```typescript
|
|
194
|
-
describe('ProductList', () => {
|
|
195
|
-
it('shows loading state', () => {
|
|
196
|
-
render(<ProductList />);
|
|
197
|
-
expect(screen.getByTestId(DATA_TEST_ID.loadingSpinner)).toBeInTheDocument();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('displays products after loading', async () => {
|
|
201
|
-
render(<ProductList />);
|
|
202
|
-
|
|
203
|
-
await waitFor(() => {
|
|
204
|
-
expect(screen.queryByTestId(DATA_TEST_ID.loadingSpinner)).not.toBeInTheDocument();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
expect(screen.getByText(/Product 1/i)).toBeInTheDocument();
|
|
208
|
-
expect(screen.getByText(/Product 2/i)).toBeInTheDocument();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('shows error message on failure', async () => {
|
|
212
|
-
server.use(
|
|
213
|
-
rest.get('/api/products', (req, res, ctx) =>
|
|
214
|
-
res(ctx.status(500), ctx.json({ error: 'Server error' }))
|
|
215
|
-
)
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
render(<ProductList />);
|
|
219
|
-
|
|
220
|
-
expect(await screen.findByText(/failed to load/i)).toBeInTheDocument();
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Performance Best Practices
|
|
226
|
-
|
|
227
|
-
### Code Splitting
|
|
228
|
-
```typescript
|
|
229
|
-
// Lazy load step components
|
|
230
|
-
const LazyStepComponent = lazy(() => import('./StepComponent'));
|
|
231
|
-
|
|
232
|
-
// Use in router
|
|
233
|
-
<Route
|
|
234
|
-
path="/step"
|
|
235
|
-
element={
|
|
236
|
-
<Suspense fallback={<LoadingSpinner />}>
|
|
237
|
-
<LazyStepComponent />
|
|
238
|
-
</Suspense>
|
|
239
|
-
}
|
|
240
|
-
/>
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Bundle Optimization
|
|
244
|
-
```typescript
|
|
245
|
-
// ✅ Tree-shakeable imports
|
|
246
|
-
import { Button } from 'dh-component-library';
|
|
247
|
-
import { map, filter } from 'lodash';
|
|
248
|
-
|
|
249
|
-
// ❌ Avoid full imports
|
|
250
|
-
import * as DH from 'dh-component-library';
|
|
251
|
-
import _ from 'lodash';
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Virtualization for Long Lists
|
|
255
|
-
```typescript
|
|
256
|
-
import { FixedSizeList } from 'react-window';
|
|
257
|
-
|
|
258
|
-
const VirtualizedList: React.FC<{ items: Product[] }> = ({ items }) => (
|
|
259
|
-
<FixedSizeList
|
|
260
|
-
height={600}
|
|
261
|
-
itemCount={items.length}
|
|
262
|
-
itemSize={50}
|
|
263
|
-
width="100%"
|
|
264
|
-
>
|
|
265
|
-
{({ index, style }) => (
|
|
266
|
-
<div style={style}>{items[index].name}</div>
|
|
267
|
-
)}
|
|
268
|
-
</FixedSizeList>
|
|
269
|
-
);
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Debouncing & Throttling
|
|
273
|
-
```typescript
|
|
274
|
-
import { useMemo } from 'react';
|
|
275
|
-
import debounce from 'lodash/debounce';
|
|
276
|
-
|
|
277
|
-
const SearchInput: React.FC = () => {
|
|
278
|
-
const [searchTerm, setSearchTerm] = useState('');
|
|
279
|
-
|
|
280
|
-
const debouncedSearch = useMemo(
|
|
281
|
-
() => debounce((value: string) => {
|
|
282
|
-
// API call here
|
|
283
|
-
console.log('Searching for:', value);
|
|
284
|
-
}, 300),
|
|
285
|
-
[]
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
289
|
-
setSearchTerm(e.target.value);
|
|
290
|
-
debouncedSearch(e.target.value);
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
return <input value={searchTerm} onChange={handleChange} />;
|
|
294
|
-
};
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
## Common Performance Issues
|
|
298
|
-
|
|
299
|
-
### ❌ Creating Functions in Render
|
|
300
|
-
```typescript
|
|
301
|
-
// ❌ BAD: New function every render
|
|
302
|
-
<Button onClick={() => handleClick(id)} />
|
|
303
|
-
|
|
304
|
-
// ✅ GOOD: Memoized with useCallback
|
|
305
|
-
const handleButtonClick = useCallback(() => handleClick(id), [id]);
|
|
306
|
-
<Button onClick={handleButtonClick} />
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### ❌ Creating Objects/Arrays in Render
|
|
310
|
-
```typescript
|
|
311
|
-
// ❌ BAD: New array every render
|
|
312
|
-
<Component items={products.filter(p => p.active)} />
|
|
313
|
-
|
|
314
|
-
// ✅ GOOD: Memoized with useMemo
|
|
315
|
-
const activeProducts = useMemo(() => products.filter(p => p.active), [products]);
|
|
316
|
-
<Component items={activeProducts} />
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### ❌ Not Memoizing Expensive Calculations
|
|
320
|
-
```typescript
|
|
321
|
-
// ❌ BAD: Recalculates every render
|
|
322
|
-
const total = products.reduce((sum, p) => sum + p.price, 0);
|
|
323
|
-
|
|
324
|
-
// ✅ GOOD: Memoized
|
|
325
|
-
const total = useMemo(() =>
|
|
326
|
-
products.reduce((sum, p) => sum + p.price, 0),
|
|
327
|
-
[products]
|
|
328
|
-
);
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
## Testing Checklist ✅
|
|
332
|
-
|
|
333
|
-
- All components have test coverage
|
|
334
|
-
- User interactions tested with userEvent
|
|
335
|
-
- Async operations tested with waitFor
|
|
336
|
-
- Error states tested
|
|
337
|
-
- Loading states tested
|
|
338
|
-
- MSW handlers for all API calls
|
|
339
|
-
- Test IDs used for querying elements
|
|
340
|
-
- No hardcoded waits (use waitFor)
|
|
341
|
-
- Tests isolated (no shared state)
|
|
342
|
-
- Cleanup after tests
|
|
343
|
-
|
|
344
|
-
## Performance Checklist ✅
|
|
345
|
-
|
|
346
|
-
- Lazy load route components
|
|
347
|
-
- Memoize expensive calculations
|
|
348
|
-
- Use React.memo for frequently re-rendering components
|
|
349
|
-
- useCallback for functions in dependency arrays
|
|
350
|
-
- Code splitting for large features
|
|
351
|
-
- Tree-shakeable imports
|
|
352
|
-
- Virtualization for long lists
|
|
353
|
-
- Debounce user input
|
|
354
|
-
- Optimize images (lazy loading, compression)
|
|
355
|
-
|
|
356
|
-
## Common Testing Mistakes ❌
|
|
357
|
-
|
|
358
|
-
- Using `getBy*` instead of `findBy*` for async elements
|
|
359
|
-
- Not waiting for async operations
|
|
360
|
-
- Testing implementation details instead of behavior
|
|
361
|
-
- Hardcoded waits (`setTimeout`)
|
|
362
|
-
- Not cleaning up after tests
|
|
363
|
-
- Missing act() warnings
|
|
364
|
-
- Not mocking external dependencies
|
|
365
|
-
- Snapshot tests for everything
|
|
366
|
-
|
|
367
|
-
## Resources
|
|
368
|
-
|
|
369
|
-
- [React Testing Library](https://testing-library.com/react)
|
|
370
|
-
- [MSW](https://mswjs.io/docs)
|
|
371
|
-
- [React Query Testing](https://react-query-v3.tanstack.com/guides/testing)
|
|
372
|
-
- [React Performance](https://reactjs.org/docs/optimizing-performance.html)
|
|
373
|
-
- [Web.dev React Performance](https://web.dev/react/)
|