@plazmodium/odin 0.3.2-beta → 0.3.4-beta
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 +82 -11
- package/builtin/ODIN.md +1045 -0
- package/builtin/agent-definitions/README.md +170 -0
- package/builtin/agent-definitions/_shared-context.md +377 -0
- package/builtin/agent-definitions/architect.md +627 -0
- package/builtin/agent-definitions/builder.md +716 -0
- package/builtin/agent-definitions/discovery.md +293 -0
- package/builtin/agent-definitions/documenter.md +238 -0
- package/builtin/agent-definitions/guardian.md +1049 -0
- package/builtin/agent-definitions/integrator.md +363 -0
- package/builtin/agent-definitions/planning.md +236 -0
- package/builtin/agent-definitions/product.md +405 -0
- package/builtin/agent-definitions/release.md +430 -0
- package/builtin/agent-definitions/reviewer.md +447 -0
- package/builtin/agent-definitions/watcher.md +402 -0
- package/builtin/skills/api/graphql/SKILL.md +548 -0
- package/builtin/skills/api/grpc/SKILL.md +554 -0
- package/builtin/skills/api/rest-api/SKILL.md +469 -0
- package/builtin/skills/api/trpc/SKILL.md +503 -0
- package/builtin/skills/architecture/clean-architecture/SKILL.md +141 -0
- package/builtin/skills/architecture/domain-driven-design/SKILL.md +129 -0
- package/builtin/skills/architecture/event-driven/SKILL.md +145 -0
- package/builtin/skills/architecture/microservices/SKILL.md +143 -0
- package/builtin/skills/architecture/tla-precheck/SKILL.md +171 -0
- package/builtin/skills/backend/golang-gin/SKILL.md +141 -0
- package/builtin/skills/backend/nodejs-express/SKILL.md +277 -0
- package/builtin/skills/backend/nodejs-fastify/SKILL.md +152 -0
- package/builtin/skills/backend/python-django/SKILL.md +128 -0
- package/builtin/skills/backend/python-fastapi/SKILL.md +140 -0
- package/builtin/skills/database/mongodb/SKILL.md +132 -0
- package/builtin/skills/database/postgresql/SKILL.md +120 -0
- package/builtin/skills/database/prisma-orm/SKILL.md +366 -0
- package/builtin/skills/database/redis/SKILL.md +140 -0
- package/builtin/skills/database/supabase/SKILL.md +416 -0
- package/builtin/skills/devops/aws/SKILL.md +382 -0
- package/builtin/skills/devops/docker/SKILL.md +359 -0
- package/builtin/skills/devops/github-actions/SKILL.md +435 -0
- package/builtin/skills/devops/kubernetes/SKILL.md +459 -0
- package/builtin/skills/devops/terraform/SKILL.md +453 -0
- package/builtin/skills/frontend/alpine-dev/SKILL.md +27 -0
- package/builtin/skills/frontend/angular-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/astro-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/htmx-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/nextjs-dev/SKILL.md +470 -0
- package/builtin/skills/frontend/react-patterns/SKILL.md +166 -0
- package/builtin/skills/frontend/svelte-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/tailwindcss/SKILL.md +131 -0
- package/builtin/skills/frontend/vuejs-dev/SKILL.md +28 -0
- package/builtin/skills/generic-dev/SKILL.md +307 -0
- package/builtin/skills/testing/cypress/SKILL.md +372 -0
- package/builtin/skills/testing/jest/SKILL.md +176 -0
- package/builtin/skills/testing/playwright/SKILL.md +341 -0
- package/builtin/skills/testing/unit-tests-eval-sdd/SKILL.md +73 -0
- package/builtin/skills/testing/unit-tests-sdd/SKILL.md +83 -0
- package/builtin/skills/testing/vitest/SKILL.md +249 -0
- package/dist/adapters/skills/filesystem.d.ts.map +1 -1
- package/dist/adapters/skills/filesystem.js +2 -18
- package/dist/adapters/skills/filesystem.js.map +1 -1
- package/dist/builtin-assets.d.ts +8 -0
- package/dist/builtin-assets.d.ts.map +1 -0
- package/dist/builtin-assets.js +90 -0
- package/dist/builtin-assets.js.map +1 -0
- package/dist/init.js +69 -11
- package/dist/init.js.map +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/prepare-phase-context.d.ts.map +1 -1
- package/dist/tools/prepare-phase-context.js +5 -0
- package/dist/tools/prepare-phase-context.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cypress
|
|
3
|
+
description: Cypress end-to-end testing expertise for web applications. Covers browser testing, component testing, network stubbing, and CI integration. Use for E2E and component testing with real-time browser preview and time-travel debugging.
|
|
4
|
+
category: testing
|
|
5
|
+
compatible_with:
|
|
6
|
+
- nextjs-dev
|
|
7
|
+
- react-patterns
|
|
8
|
+
- vuejs-dev
|
|
9
|
+
- github-actions
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Cypress End-to-End Testing
|
|
13
|
+
|
|
14
|
+
## Instructions
|
|
15
|
+
|
|
16
|
+
1. **Assess the testing scope**: Determine if it's E2E tests, component tests, or API tests.
|
|
17
|
+
2. **Follow Cypress conventions**:
|
|
18
|
+
- E2E tests: `cypress/e2e/*.cy.ts`
|
|
19
|
+
- Component tests: `*.cy.tsx` alongside components
|
|
20
|
+
- Use `cy` commands for all interactions
|
|
21
|
+
3. **Provide complete examples**: Include selectors, assertions, and custom commands.
|
|
22
|
+
4. **Guide on best practices**: Selectors, waiting, anti-patterns.
|
|
23
|
+
5. **Cover advanced features**: Intercepts, fixtures, custom commands.
|
|
24
|
+
|
|
25
|
+
## Test Structure
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
describe('User Authentication', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
cy.visit('/login');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should login with valid credentials', () => {
|
|
34
|
+
cy.get('[data-testid="email"]').type('user@example.com');
|
|
35
|
+
cy.get('[data-testid="password"]').type('password123');
|
|
36
|
+
cy.get('[data-testid="submit"]').click();
|
|
37
|
+
|
|
38
|
+
cy.url().should('include', '/dashboard');
|
|
39
|
+
cy.get('h1').should('contain', 'Welcome');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should show error for invalid credentials', () => {
|
|
43
|
+
cy.get('[data-testid="email"]').type('invalid@example.com');
|
|
44
|
+
cy.get('[data-testid="password"]').type('wrong');
|
|
45
|
+
cy.get('[data-testid="submit"]').click();
|
|
46
|
+
|
|
47
|
+
cy.contains('Invalid credentials').should('be.visible');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Selectors (Recommended Order)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// 1. Data attributes (most resilient)
|
|
56
|
+
cy.get('[data-testid="submit-button"]');
|
|
57
|
+
cy.get('[data-cy="email-input"]');
|
|
58
|
+
|
|
59
|
+
// 2. Contains text
|
|
60
|
+
cy.contains('Submit');
|
|
61
|
+
cy.contains('button', 'Submit'); // Element + text
|
|
62
|
+
|
|
63
|
+
// 3. Role-based (with cypress-testing-library)
|
|
64
|
+
cy.findByRole('button', { name: 'Submit' });
|
|
65
|
+
cy.findByLabelText('Email');
|
|
66
|
+
cy.findByPlaceholderText('Enter email');
|
|
67
|
+
|
|
68
|
+
// 4. CSS selectors
|
|
69
|
+
cy.get('.submit-btn');
|
|
70
|
+
cy.get('#email-input');
|
|
71
|
+
cy.get('form input[type="email"]');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Common Commands
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Navigation
|
|
78
|
+
cy.visit('/page');
|
|
79
|
+
cy.go('back');
|
|
80
|
+
cy.reload();
|
|
81
|
+
|
|
82
|
+
// Clicking
|
|
83
|
+
cy.get('button').click();
|
|
84
|
+
cy.get('button').dblclick();
|
|
85
|
+
cy.get('button').rightclick();
|
|
86
|
+
cy.get('button').click({ force: true }); // Skip visibility checks
|
|
87
|
+
|
|
88
|
+
// Typing
|
|
89
|
+
cy.get('input').type('Hello');
|
|
90
|
+
cy.get('input').type('{enter}'); // Special keys
|
|
91
|
+
cy.get('input').type('text{enter}'); // Text + enter
|
|
92
|
+
cy.get('input').clear().type('new text');
|
|
93
|
+
|
|
94
|
+
// Selection
|
|
95
|
+
cy.get('select').select('value');
|
|
96
|
+
cy.get('select').select(['opt1', 'opt2']); // Multiple
|
|
97
|
+
|
|
98
|
+
// Checkboxes/Radio
|
|
99
|
+
cy.get('input[type="checkbox"]').check();
|
|
100
|
+
cy.get('input[type="checkbox"]').uncheck();
|
|
101
|
+
cy.get('input[type="radio"]').check('value');
|
|
102
|
+
|
|
103
|
+
// Focus/Blur
|
|
104
|
+
cy.get('input').focus();
|
|
105
|
+
cy.get('input').blur();
|
|
106
|
+
|
|
107
|
+
// Scrolling
|
|
108
|
+
cy.scrollTo('bottom');
|
|
109
|
+
cy.get('.list').scrollTo(0, 500);
|
|
110
|
+
|
|
111
|
+
// File upload
|
|
112
|
+
cy.get('input[type="file"]').selectFile('path/to/file.pdf');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Assertions
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Visibility
|
|
119
|
+
cy.get('button').should('be.visible');
|
|
120
|
+
cy.get('modal').should('not.exist');
|
|
121
|
+
cy.get('loader').should('not.be.visible');
|
|
122
|
+
|
|
123
|
+
// Text
|
|
124
|
+
cy.get('h1').should('have.text', 'Exact Text');
|
|
125
|
+
cy.get('h1').should('contain', 'Partial');
|
|
126
|
+
cy.get('input').should('have.value', 'input value');
|
|
127
|
+
|
|
128
|
+
// Attributes
|
|
129
|
+
cy.get('a').should('have.attr', 'href', '/link');
|
|
130
|
+
cy.get('button').should('have.class', 'active');
|
|
131
|
+
cy.get('button').should('be.enabled');
|
|
132
|
+
cy.get('button').should('be.disabled');
|
|
133
|
+
cy.get('input').should('be.checked');
|
|
134
|
+
|
|
135
|
+
// Count
|
|
136
|
+
cy.get('li').should('have.length', 3);
|
|
137
|
+
cy.get('li').should('have.length.greaterThan', 2);
|
|
138
|
+
|
|
139
|
+
// URL
|
|
140
|
+
cy.url().should('include', '/dashboard');
|
|
141
|
+
cy.url().should('eq', 'http://localhost:3000/dashboard');
|
|
142
|
+
|
|
143
|
+
// Chaining
|
|
144
|
+
cy.get('input')
|
|
145
|
+
.should('be.visible')
|
|
146
|
+
.and('have.attr', 'placeholder', 'Email')
|
|
147
|
+
.and('be.enabled');
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Network Interception
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Stub a response
|
|
154
|
+
cy.intercept('GET', '/api/users', {
|
|
155
|
+
statusCode: 200,
|
|
156
|
+
body: [{ id: 1, name: 'John' }],
|
|
157
|
+
}).as('getUsers');
|
|
158
|
+
|
|
159
|
+
cy.visit('/users');
|
|
160
|
+
cy.wait('@getUsers');
|
|
161
|
+
cy.contains('John').should('be.visible');
|
|
162
|
+
|
|
163
|
+
// Wait for real request
|
|
164
|
+
cy.intercept('POST', '/api/login').as('login');
|
|
165
|
+
cy.get('form').submit();
|
|
166
|
+
cy.wait('@login').its('response.statusCode').should('eq', 200);
|
|
167
|
+
|
|
168
|
+
// Modify response
|
|
169
|
+
cy.intercept('GET', '/api/data', (req) => {
|
|
170
|
+
req.reply((res) => {
|
|
171
|
+
res.body.modified = true;
|
|
172
|
+
return res;
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Delay response
|
|
177
|
+
cy.intercept('GET', '/api/slow', (req) => {
|
|
178
|
+
req.on('response', (res) => {
|
|
179
|
+
res.setDelay(2000);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Simulate error
|
|
184
|
+
cy.intercept('GET', '/api/data', {
|
|
185
|
+
statusCode: 500,
|
|
186
|
+
body: { error: 'Server error' },
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Fixtures
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// cypress/fixtures/users.json
|
|
194
|
+
[
|
|
195
|
+
{ "id": 1, "name": "John", "email": "john@example.com" },
|
|
196
|
+
{ "id": 2, "name": "Jane", "email": "jane@example.com" }
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
// In tests
|
|
200
|
+
cy.fixture('users').then((users) => {
|
|
201
|
+
cy.intercept('GET', '/api/users', users);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Or directly
|
|
205
|
+
cy.intercept('GET', '/api/users', { fixture: 'users.json' });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Custom Commands
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// cypress/support/commands.ts
|
|
212
|
+
Cypress.Commands.add('login', (email: string, password: string) => {
|
|
213
|
+
cy.session([email, password], () => {
|
|
214
|
+
cy.visit('/login');
|
|
215
|
+
cy.get('[data-testid="email"]').type(email);
|
|
216
|
+
cy.get('[data-testid="password"]').type(password);
|
|
217
|
+
cy.get('[data-testid="submit"]').click();
|
|
218
|
+
cy.url().should('include', '/dashboard');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
Cypress.Commands.add('getBySel', (selector: string) => {
|
|
223
|
+
return cy.get(`[data-testid="${selector}"]`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Usage
|
|
227
|
+
cy.login('user@example.com', 'password');
|
|
228
|
+
cy.getBySel('submit-button').click();
|
|
229
|
+
|
|
230
|
+
// TypeScript declarations (cypress/support/index.d.ts)
|
|
231
|
+
declare namespace Cypress {
|
|
232
|
+
interface Chainable {
|
|
233
|
+
login(email: string, password: string): Chainable<void>;
|
|
234
|
+
getBySel(selector: string): Chainable<JQuery<HTMLElement>>;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Component Testing
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Button.cy.tsx
|
|
243
|
+
import Button from './Button';
|
|
244
|
+
|
|
245
|
+
describe('Button', () => {
|
|
246
|
+
it('renders with text', () => {
|
|
247
|
+
cy.mount(<Button>Click me</Button>);
|
|
248
|
+
cy.contains('Click me').should('be.visible');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('calls onClick when clicked', () => {
|
|
252
|
+
const onClick = cy.stub().as('onClick');
|
|
253
|
+
cy.mount(<Button onClick={onClick}>Click</Button>);
|
|
254
|
+
|
|
255
|
+
cy.get('button').click();
|
|
256
|
+
cy.get('@onClick').should('have.been.calledOnce');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('can be disabled', () => {
|
|
260
|
+
cy.mount(<Button disabled>Disabled</Button>);
|
|
261
|
+
cy.get('button').should('be.disabled');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Configuration (cypress.config.ts)
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { defineConfig } from 'cypress';
|
|
270
|
+
|
|
271
|
+
export default defineConfig({
|
|
272
|
+
e2e: {
|
|
273
|
+
baseUrl: 'http://localhost:3000',
|
|
274
|
+
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
|
275
|
+
supportFile: 'cypress/support/e2e.ts',
|
|
276
|
+
viewportWidth: 1280,
|
|
277
|
+
viewportHeight: 720,
|
|
278
|
+
video: false,
|
|
279
|
+
screenshotOnRunFailure: true,
|
|
280
|
+
retries: {
|
|
281
|
+
runMode: 2,
|
|
282
|
+
openMode: 0,
|
|
283
|
+
},
|
|
284
|
+
setupNodeEvents(on, config) {
|
|
285
|
+
// Plugins
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
component: {
|
|
289
|
+
devServer: {
|
|
290
|
+
framework: 'react',
|
|
291
|
+
bundler: 'vite',
|
|
292
|
+
},
|
|
293
|
+
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
|
|
294
|
+
},
|
|
295
|
+
env: {
|
|
296
|
+
apiUrl: 'http://localhost:3001',
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Best Practices
|
|
302
|
+
|
|
303
|
+
- **Use data-testid** or data-cy attributes for selectors
|
|
304
|
+
- **Don't use cy.wait(ms)** - Use intercepts and assertions
|
|
305
|
+
- **Keep tests independent** - Each test should work in isolation
|
|
306
|
+
- **Use cy.session()** for login/auth to speed up tests
|
|
307
|
+
- **Avoid conditional testing** - Tests should be deterministic
|
|
308
|
+
- **Use aliases** for readability (`cy.get(...).as('button')`)
|
|
309
|
+
- **Chain assertions** instead of multiple `cy.get()` calls
|
|
310
|
+
- **Test user flows** not implementation details
|
|
311
|
+
|
|
312
|
+
## Anti-Patterns to Avoid
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// ❌ Don't use arbitrary waits
|
|
316
|
+
cy.wait(5000);
|
|
317
|
+
|
|
318
|
+
// ✅ Wait for specific conditions
|
|
319
|
+
cy.get('button').should('be.visible');
|
|
320
|
+
cy.intercept('/api/*').as('api');
|
|
321
|
+
cy.wait('@api');
|
|
322
|
+
|
|
323
|
+
// ❌ Don't rely on DOM structure
|
|
324
|
+
cy.get('div > div > ul > li:first-child');
|
|
325
|
+
|
|
326
|
+
// ✅ Use data attributes
|
|
327
|
+
cy.get('[data-testid="first-item"]');
|
|
328
|
+
|
|
329
|
+
// ❌ Don't share state between tests
|
|
330
|
+
let userId;
|
|
331
|
+
it('creates user', () => { userId = createUser(); });
|
|
332
|
+
it('uses user', () => { /* uses userId */ });
|
|
333
|
+
|
|
334
|
+
// ✅ Each test is independent
|
|
335
|
+
it('user flow', () => {
|
|
336
|
+
const user = createUser();
|
|
337
|
+
// use user in same test
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Debugging
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# Open Cypress UI
|
|
345
|
+
npx cypress open
|
|
346
|
+
|
|
347
|
+
# Run headless
|
|
348
|
+
npx cypress run
|
|
349
|
+
|
|
350
|
+
# Run specific spec
|
|
351
|
+
npx cypress run --spec "cypress/e2e/login.cy.ts"
|
|
352
|
+
|
|
353
|
+
# With browser
|
|
354
|
+
npx cypress run --browser chrome
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// In tests - pause and inspect
|
|
359
|
+
cy.pause();
|
|
360
|
+
|
|
361
|
+
// Debug in console
|
|
362
|
+
cy.get('button').then(($btn) => {
|
|
363
|
+
debugger;
|
|
364
|
+
console.log($btn.text());
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## References
|
|
369
|
+
|
|
370
|
+
- Cypress Documentation: https://docs.cypress.io/
|
|
371
|
+
- Best Practices: https://docs.cypress.io/guides/references/best-practices
|
|
372
|
+
- API Reference: https://docs.cypress.io/api/table-of-contents
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jest
|
|
3
|
+
description: Jest testing framework expertise for JavaScript/TypeScript unit and integration testing. Covers test structure, mocking, assertions, snapshots, coverage, and CI integration. Use when writing tests for React, Node.js, or any JS/TS project.
|
|
4
|
+
category: testing
|
|
5
|
+
compatible_with:
|
|
6
|
+
- react-patterns
|
|
7
|
+
- nodejs-express
|
|
8
|
+
- github-actions
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Jest Testing Framework
|
|
12
|
+
|
|
13
|
+
## Instructions
|
|
14
|
+
|
|
15
|
+
1. **Assess the testing need**: Determine if it's unit tests, integration tests, or snapshot tests.
|
|
16
|
+
2. **Follow Jest conventions**:
|
|
17
|
+
- Test files: `*.test.ts`, `*.spec.ts`, or in `__tests__/` directory
|
|
18
|
+
- Use `describe` blocks to group related tests
|
|
19
|
+
- Use `it` or `test` for individual test cases
|
|
20
|
+
- Use `beforeEach`/`afterEach` for setup/teardown
|
|
21
|
+
3. **Provide complete examples**: Include imports, mocks, and assertions.
|
|
22
|
+
4. **Guide on mocking**: Explain `jest.mock()`, `jest.fn()`, and `jest.spyOn()`.
|
|
23
|
+
5. **Cover edge cases**: Test error conditions, boundary values, and async behavior.
|
|
24
|
+
|
|
25
|
+
## Test Structure
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
29
|
+
import { myFunction } from './myModule';
|
|
30
|
+
|
|
31
|
+
describe('myFunction', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return expected value for valid input', () => {
|
|
37
|
+
const result = myFunction('valid');
|
|
38
|
+
expect(result).toBe('expected');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw error for invalid input', () => {
|
|
42
|
+
expect(() => myFunction(null)).toThrow('Invalid input');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle async operations', async () => {
|
|
46
|
+
const result = await myFunction('async');
|
|
47
|
+
expect(result).resolves.toBe('done');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Mocking Patterns
|
|
53
|
+
|
|
54
|
+
### Mock a module
|
|
55
|
+
```typescript
|
|
56
|
+
jest.mock('./database', () => ({
|
|
57
|
+
query: jest.fn().mockResolvedValue([{ id: 1 }]),
|
|
58
|
+
}));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Mock a function
|
|
62
|
+
```typescript
|
|
63
|
+
const mockCallback = jest.fn((x) => x + 1);
|
|
64
|
+
expect(mockCallback(1)).toBe(2);
|
|
65
|
+
expect(mockCallback).toHaveBeenCalledWith(1);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Spy on methods
|
|
69
|
+
```typescript
|
|
70
|
+
const spy = jest.spyOn(object, 'method');
|
|
71
|
+
object.method();
|
|
72
|
+
expect(spy).toHaveBeenCalled();
|
|
73
|
+
spy.mockRestore();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Mock timers
|
|
77
|
+
```typescript
|
|
78
|
+
jest.useFakeTimers();
|
|
79
|
+
setTimeout(callback, 1000);
|
|
80
|
+
jest.advanceTimersByTime(1000);
|
|
81
|
+
expect(callback).toHaveBeenCalled();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Common Assertions
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Equality
|
|
88
|
+
expect(value).toBe(exact); // Strict equality
|
|
89
|
+
expect(value).toEqual(object); // Deep equality
|
|
90
|
+
expect(value).toStrictEqual(object); // Deep + type equality
|
|
91
|
+
|
|
92
|
+
// Truthiness
|
|
93
|
+
expect(value).toBeTruthy();
|
|
94
|
+
expect(value).toBeFalsy();
|
|
95
|
+
expect(value).toBeNull();
|
|
96
|
+
expect(value).toBeUndefined();
|
|
97
|
+
expect(value).toBeDefined();
|
|
98
|
+
|
|
99
|
+
// Numbers
|
|
100
|
+
expect(value).toBeGreaterThan(3);
|
|
101
|
+
expect(value).toBeLessThanOrEqual(5);
|
|
102
|
+
expect(value).toBeCloseTo(0.3, 5); // Floating point
|
|
103
|
+
|
|
104
|
+
// Strings
|
|
105
|
+
expect(string).toMatch(/pattern/);
|
|
106
|
+
expect(string).toContain('substring');
|
|
107
|
+
|
|
108
|
+
// Arrays
|
|
109
|
+
expect(array).toContain(item);
|
|
110
|
+
expect(array).toHaveLength(3);
|
|
111
|
+
|
|
112
|
+
// Objects
|
|
113
|
+
expect(object).toHaveProperty('key', 'value');
|
|
114
|
+
expect(object).toMatchObject({ partial: 'match' });
|
|
115
|
+
|
|
116
|
+
// Errors
|
|
117
|
+
expect(() => fn()).toThrow();
|
|
118
|
+
expect(() => fn()).toThrow('message');
|
|
119
|
+
expect(() => fn()).toThrow(ErrorClass);
|
|
120
|
+
|
|
121
|
+
// Async
|
|
122
|
+
await expect(promise).resolves.toBe(value);
|
|
123
|
+
await expect(promise).rejects.toThrow();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration (jest.config.js)
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
module.exports = {
|
|
130
|
+
preset: 'ts-jest',
|
|
131
|
+
testEnvironment: 'node', // or 'jsdom' for browser
|
|
132
|
+
roots: ['<rootDir>/src'],
|
|
133
|
+
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
|
134
|
+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
|
|
135
|
+
coverageThreshold: {
|
|
136
|
+
global: { branches: 80, functions: 80, lines: 80 },
|
|
137
|
+
},
|
|
138
|
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
|
139
|
+
moduleNameMapper: {
|
|
140
|
+
'^@/(.*)$': '<rootDir>/src/$1',
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Best Practices
|
|
146
|
+
|
|
147
|
+
- **One assertion per test** when possible for clear failure messages
|
|
148
|
+
- **Arrange-Act-Assert** pattern for test structure
|
|
149
|
+
- **Mock external dependencies** (APIs, databases, file system)
|
|
150
|
+
- **Don't test implementation details** - test behavior and outcomes
|
|
151
|
+
- **Use meaningful test names** that describe the expected behavior
|
|
152
|
+
- **Keep tests fast** - mock slow operations
|
|
153
|
+
- **Run tests in CI** with coverage reporting
|
|
154
|
+
- **Snapshot tests** for UI components, but review changes carefully
|
|
155
|
+
|
|
156
|
+
## React Testing (with @testing-library/react)
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
160
|
+
import { Button } from './Button';
|
|
161
|
+
|
|
162
|
+
test('calls onClick when clicked', () => {
|
|
163
|
+
const handleClick = jest.fn();
|
|
164
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
165
|
+
|
|
166
|
+
fireEvent.click(screen.getByRole('button'));
|
|
167
|
+
|
|
168
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## References
|
|
173
|
+
|
|
174
|
+
- Jest Documentation: https://jestjs.io/docs/getting-started
|
|
175
|
+
- Testing Library: https://testing-library.com/docs/
|
|
176
|
+
- Jest Cheat Sheet: https://github.com/sapegin/jest-cheat-sheet
|