elsabro 2.0.1 → 2.2.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 (46) hide show
  1. package/commands/elsabro/add-phase.md +17 -0
  2. package/commands/elsabro/add-todo.md +111 -53
  3. package/commands/elsabro/audit-milestone.md +19 -0
  4. package/commands/elsabro/check-todos.md +210 -31
  5. package/commands/elsabro/complete-milestone.md +20 -1
  6. package/commands/elsabro/debug.md +19 -0
  7. package/commands/elsabro/discuss-phase.md +18 -1
  8. package/commands/elsabro/execute.md +496 -52
  9. package/commands/elsabro/insert-phase.md +18 -1
  10. package/commands/elsabro/list-phase-assumptions.md +17 -0
  11. package/commands/elsabro/new-milestone.md +19 -0
  12. package/commands/elsabro/new.md +19 -0
  13. package/commands/elsabro/pause-work.md +75 -0
  14. package/commands/elsabro/plan-milestone-gaps.md +20 -1
  15. package/commands/elsabro/plan.md +264 -36
  16. package/commands/elsabro/progress.md +203 -79
  17. package/commands/elsabro/quick.md +19 -0
  18. package/commands/elsabro/remove-phase.md +17 -0
  19. package/commands/elsabro/research-phase.md +18 -1
  20. package/commands/elsabro/resume-work.md +130 -2
  21. package/commands/elsabro/start.md +365 -98
  22. package/commands/elsabro/verify-work.md +271 -12
  23. package/package.json +1 -1
  24. package/references/SYSTEM_INDEX.md +241 -0
  25. package/references/command-flow.md +352 -0
  26. package/references/enforcement-rules.md +331 -0
  27. package/references/error-handling-instructions.md +26 -12
  28. package/references/state-sync.md +381 -0
  29. package/references/task-dispatcher.md +388 -0
  30. package/references/tasks-integration.md +380 -0
  31. package/skills/api-microservice.md +765 -0
  32. package/skills/api-setup.md +76 -3
  33. package/skills/auth-setup.md +46 -6
  34. package/skills/chrome-extension.md +584 -0
  35. package/skills/cicd-setup.md +1206 -0
  36. package/skills/cli-tool.md +884 -0
  37. package/skills/database-setup.md +41 -5
  38. package/skills/desktop-app.md +1351 -0
  39. package/skills/expo-app.md +35 -2
  40. package/skills/full-stack-app.md +543 -0
  41. package/skills/mobile-app.md +813 -0
  42. package/skills/nextjs-app.md +33 -2
  43. package/skills/payments-setup.md +76 -1
  44. package/skills/saas-starter.md +639 -0
  45. package/skills/sentry-setup.md +41 -7
  46. package/skills/testing-setup.md +1218 -0
@@ -0,0 +1,1218 @@
1
+ ---
2
+ name: testing-setup
3
+ description: Configurar testing completo con unit tests (Vitest), E2E (Playwright), mocking (MSW) y coverage
4
+ tags: [testing, vitest, playwright, e2e, unit-tests, tdd, quality, coverage, msw]
5
+ difficulty: intermediate
6
+ estimated_time: 30min
7
+ ---
8
+
9
+ # Skill: Testing Setup
10
+
11
+ <when_to_use>
12
+ Usar cuando el usuario menciona:
13
+ - "agregar tests"
14
+ - "testing"
15
+ - "TDD"
16
+ - "coverage"
17
+ - "E2E"
18
+ - "pruebas automatizadas"
19
+ - "unit tests"
20
+ - "integration tests"
21
+ - "mocking"
22
+ - "test driven development"
23
+ </when_to_use>
24
+
25
+ <pre_requisites>
26
+ ## Pre-requisitos
27
+ - Node.js 20+
28
+ - Proyecto existente con package.json
29
+ - TypeScript configurado (recomendado)
30
+ - Conocimiento basico de Jest/testing
31
+ </pre_requisites>
32
+
33
+ <tech_stack>
34
+ ## Stack Tecnologico
35
+ | Categoria | Tecnologia | Version | Proposito |
36
+ |-----------|------------|---------|-----------|
37
+ | Unit Tests | Vitest | 2.x | Testing rapido con soporte nativo ESM |
38
+ | E2E Tests | Playwright | 1.49.x | Testing cross-browser automatizado |
39
+ | Mocking | MSW | 2.x | Mock de APIs a nivel de red |
40
+ | Coverage | c8 | 10.x | Reporte de cobertura de codigo |
41
+ | React Testing | @testing-library/react | 16.x | Testing de componentes React |
42
+ | DOM Testing | @testing-library/dom | 10.x | Utilidades de testing DOM |
43
+ | User Events | @testing-library/user-event | 14.x | Simulacion de eventos de usuario |
44
+ | Jest DOM | @testing-library/jest-dom | 6.x | Matchers adicionales para DOM |
45
+ </tech_stack>
46
+
47
+ <project_structure>
48
+ ## Estructura de Proyecto
49
+ ```
50
+ project/
51
+ ├── src/
52
+ │ ├── components/
53
+ │ │ └── Button.tsx
54
+ │ ├── lib/
55
+ │ │ └── utils.ts
56
+ │ └── services/
57
+ │ └── api.ts
58
+ ├── tests/
59
+ │ ├── unit/
60
+ │ │ ├── utils.test.ts
61
+ │ │ └── components/
62
+ │ │ └── Button.test.tsx
63
+ │ ├── integration/
64
+ │ │ └── api.test.ts
65
+ │ └── e2e/
66
+ │ ├── auth.spec.ts
67
+ │ └── checkout.spec.ts
68
+ ├── mocks/
69
+ │ ├── handlers.ts
70
+ │ ├── server.ts
71
+ │ └── browser.ts
72
+ ├── vitest.config.ts
73
+ ├── vitest.setup.ts
74
+ ├── playwright.config.ts
75
+ └── package.json
76
+ ```
77
+ </project_structure>
78
+
79
+ <setup_steps>
80
+ ## Paso 1: Instalar Dependencias
81
+
82
+ ### Dependencias de Vitest (Unit/Integration Tests)
83
+ ```bash
84
+ npm install -D vitest @vitest/coverage-v8 @vitest/ui happy-dom
85
+ ```
86
+
87
+ ### Dependencias de Testing Library (React)
88
+ ```bash
89
+ npm install -D @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom
90
+ ```
91
+
92
+ ### Dependencias de Playwright (E2E)
93
+ ```bash
94
+ npm install -D @playwright/test
95
+ npx playwright install
96
+ ```
97
+
98
+ ### Dependencias de MSW (Mocking)
99
+ ```bash
100
+ npm install -D msw
101
+ ```
102
+
103
+ ## Paso 2: Configurar Vitest
104
+
105
+ ### vitest.config.ts
106
+ ```typescript
107
+ import { defineConfig } from 'vitest/config';
108
+ import react from '@vitejs/plugin-react';
109
+ import path from 'path';
110
+
111
+ export default defineConfig({
112
+ plugins: [react()],
113
+ test: {
114
+ // Ambiente de testing
115
+ environment: 'happy-dom',
116
+
117
+ // Archivos de setup
118
+ setupFiles: ['./vitest.setup.ts'],
119
+
120
+ // Incluir archivos de test
121
+ include: [
122
+ 'tests/unit/**/*.{test,spec}.{ts,tsx}',
123
+ 'tests/integration/**/*.{test,spec}.{ts,tsx}',
124
+ 'src/**/*.{test,spec}.{ts,tsx}',
125
+ ],
126
+
127
+ // Excluir
128
+ exclude: [
129
+ 'node_modules',
130
+ 'tests/e2e/**/*',
131
+ '.next',
132
+ 'dist',
133
+ ],
134
+
135
+ // Coverage
136
+ coverage: {
137
+ provider: 'v8',
138
+ reporter: ['text', 'json', 'html', 'lcov'],
139
+ reportsDirectory: './coverage',
140
+ exclude: [
141
+ 'node_modules/',
142
+ 'tests/',
143
+ 'mocks/',
144
+ '**/*.d.ts',
145
+ '**/*.config.*',
146
+ '**/types/**',
147
+ ],
148
+ thresholds: {
149
+ lines: 80,
150
+ functions: 80,
151
+ branches: 80,
152
+ statements: 80,
153
+ },
154
+ },
155
+
156
+ // Globals para jest-dom matchers
157
+ globals: true,
158
+
159
+ // Reporter
160
+ reporter: ['verbose', 'html'],
161
+
162
+ // Timeouts
163
+ testTimeout: 10000,
164
+ hookTimeout: 10000,
165
+
166
+ // Watch mode
167
+ watch: false,
168
+
169
+ // Pool options
170
+ pool: 'forks',
171
+ poolOptions: {
172
+ forks: {
173
+ singleFork: true,
174
+ },
175
+ },
176
+ },
177
+ resolve: {
178
+ alias: {
179
+ '@': path.resolve(__dirname, './src'),
180
+ '@tests': path.resolve(__dirname, './tests'),
181
+ '@mocks': path.resolve(__dirname, './mocks'),
182
+ },
183
+ },
184
+ });
185
+ ```
186
+
187
+ ### vitest.setup.ts
188
+ ```typescript
189
+ import { expect, afterEach, beforeAll, afterAll, vi } from 'vitest';
190
+ import { cleanup } from '@testing-library/react';
191
+ import * as matchers from '@testing-library/jest-dom/matchers';
192
+ import { server } from './mocks/server';
193
+
194
+ // Extender matchers de Vitest con jest-dom
195
+ expect.extend(matchers);
196
+
197
+ // Limpiar despues de cada test
198
+ afterEach(() => {
199
+ cleanup();
200
+ vi.clearAllMocks();
201
+ });
202
+
203
+ // Configurar MSW
204
+ beforeAll(() => {
205
+ server.listen({ onUnhandledRequest: 'error' });
206
+ });
207
+
208
+ afterEach(() => {
209
+ server.resetHandlers();
210
+ });
211
+
212
+ afterAll(() => {
213
+ server.close();
214
+ });
215
+
216
+ // Mock de variables de entorno
217
+ vi.stubEnv('NODE_ENV', 'test');
218
+
219
+ // Mock de window.matchMedia
220
+ Object.defineProperty(window, 'matchMedia', {
221
+ writable: true,
222
+ value: vi.fn().mockImplementation((query: string) => ({
223
+ matches: false,
224
+ media: query,
225
+ onchange: null,
226
+ addListener: vi.fn(),
227
+ removeListener: vi.fn(),
228
+ addEventListener: vi.fn(),
229
+ removeEventListener: vi.fn(),
230
+ dispatchEvent: vi.fn(),
231
+ })),
232
+ });
233
+
234
+ // Mock de ResizeObserver
235
+ global.ResizeObserver = vi.fn().mockImplementation(() => ({
236
+ observe: vi.fn(),
237
+ unobserve: vi.fn(),
238
+ disconnect: vi.fn(),
239
+ }));
240
+
241
+ // Mock de IntersectionObserver
242
+ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
243
+ observe: vi.fn(),
244
+ unobserve: vi.fn(),
245
+ disconnect: vi.fn(),
246
+ }));
247
+ ```
248
+
249
+ ## Paso 3: Configurar Playwright
250
+
251
+ ### playwright.config.ts
252
+ ```typescript
253
+ import { defineConfig, devices } from '@playwright/test';
254
+
255
+ export default defineConfig({
256
+ // Directorio de tests
257
+ testDir: './tests/e2e',
258
+
259
+ // Timeout por test
260
+ timeout: 30000,
261
+
262
+ // Timeout para expect
263
+ expect: {
264
+ timeout: 5000,
265
+ },
266
+
267
+ // Ejecutar tests en paralelo
268
+ fullyParallel: true,
269
+
270
+ // Fallar el build si hay test.only
271
+ forbidOnly: !!process.env.CI,
272
+
273
+ // Reintentos
274
+ retries: process.env.CI ? 2 : 0,
275
+
276
+ // Workers
277
+ workers: process.env.CI ? 1 : undefined,
278
+
279
+ // Reporter
280
+ reporter: [
281
+ ['html', { outputFolder: 'playwright-report' }],
282
+ ['json', { outputFile: 'playwright-report/results.json' }],
283
+ process.env.CI ? ['github'] : ['list'],
284
+ ],
285
+
286
+ // Configuracion global
287
+ use: {
288
+ // URL base
289
+ baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
290
+
291
+ // Trace on failure
292
+ trace: 'on-first-retry',
293
+
294
+ // Screenshot on failure
295
+ screenshot: 'only-on-failure',
296
+
297
+ // Video on failure
298
+ video: 'on-first-retry',
299
+
300
+ // Locale
301
+ locale: 'en-US',
302
+
303
+ // Timezone
304
+ timezoneId: 'America/New_York',
305
+ },
306
+
307
+ // Proyectos (browsers)
308
+ projects: [
309
+ {
310
+ name: 'chromium',
311
+ use: { ...devices['Desktop Chrome'] },
312
+ },
313
+ {
314
+ name: 'firefox',
315
+ use: { ...devices['Desktop Firefox'] },
316
+ },
317
+ {
318
+ name: 'webkit',
319
+ use: { ...devices['Desktop Safari'] },
320
+ },
321
+ // Mobile viewports
322
+ {
323
+ name: 'Mobile Chrome',
324
+ use: { ...devices['Pixel 5'] },
325
+ },
326
+ {
327
+ name: 'Mobile Safari',
328
+ use: { ...devices['iPhone 12'] },
329
+ },
330
+ ],
331
+
332
+ // Servidor de desarrollo
333
+ webServer: {
334
+ command: 'npm run dev',
335
+ url: 'http://localhost:3000',
336
+ reuseExistingServer: !process.env.CI,
337
+ timeout: 120000,
338
+ },
339
+ });
340
+ ```
341
+
342
+ ## Paso 4: Configurar MSW (Mock Service Worker)
343
+
344
+ ### mocks/handlers.ts
345
+ ```typescript
346
+ import { http, HttpResponse, delay } from 'msw';
347
+
348
+ // Tipos de datos
349
+ interface User {
350
+ id: string;
351
+ email: string;
352
+ name: string;
353
+ }
354
+
355
+ interface LoginRequest {
356
+ email: string;
357
+ password: string;
358
+ }
359
+
360
+ interface ApiError {
361
+ message: string;
362
+ code: string;
363
+ }
364
+
365
+ // Base URL
366
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
367
+
368
+ // Datos de prueba
369
+ const mockUsers: User[] = [
370
+ { id: '1', email: 'test@example.com', name: 'Test User' },
371
+ { id: '2', email: 'admin@example.com', name: 'Admin User' },
372
+ ];
373
+
374
+ // Handlers
375
+ export const handlers = [
376
+ // Auth handlers
377
+ http.post<never, LoginRequest>(`${API_BASE}/auth/login`, async ({ request }) => {
378
+ await delay(100); // Simular latencia
379
+
380
+ const body = await request.json();
381
+
382
+ if (body.email === 'test@example.com' && body.password === 'password123') {
383
+ return HttpResponse.json({
384
+ user: mockUsers[0],
385
+ token: 'mock-jwt-token',
386
+ });
387
+ }
388
+
389
+ return HttpResponse.json<ApiError>(
390
+ { message: 'Invalid credentials', code: 'INVALID_CREDENTIALS' },
391
+ { status: 401 }
392
+ );
393
+ }),
394
+
395
+ http.post(`${API_BASE}/auth/logout`, async () => {
396
+ await delay(50);
397
+ return HttpResponse.json({ success: true });
398
+ }),
399
+
400
+ http.get(`${API_BASE}/auth/me`, async ({ request }) => {
401
+ const authHeader = request.headers.get('Authorization');
402
+
403
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
404
+ return HttpResponse.json<ApiError>(
405
+ { message: 'Unauthorized', code: 'UNAUTHORIZED' },
406
+ { status: 401 }
407
+ );
408
+ }
409
+
410
+ return HttpResponse.json({ user: mockUsers[0] });
411
+ }),
412
+
413
+ // User handlers
414
+ http.get(`${API_BASE}/users`, async () => {
415
+ await delay(100);
416
+ return HttpResponse.json({ users: mockUsers });
417
+ }),
418
+
419
+ http.get(`${API_BASE}/users/:id`, async ({ params }) => {
420
+ await delay(50);
421
+ const user = mockUsers.find(u => u.id === params.id);
422
+
423
+ if (!user) {
424
+ return HttpResponse.json<ApiError>(
425
+ { message: 'User not found', code: 'NOT_FOUND' },
426
+ { status: 404 }
427
+ );
428
+ }
429
+
430
+ return HttpResponse.json({ user });
431
+ }),
432
+
433
+ http.post<never, Partial<User>>(`${API_BASE}/users`, async ({ request }) => {
434
+ await delay(100);
435
+ const body = await request.json();
436
+
437
+ const newUser: User = {
438
+ id: String(mockUsers.length + 1),
439
+ email: body.email || '',
440
+ name: body.name || '',
441
+ };
442
+
443
+ return HttpResponse.json({ user: newUser }, { status: 201 });
444
+ }),
445
+
446
+ // Error simulation handler
447
+ http.get(`${API_BASE}/error`, async () => {
448
+ return HttpResponse.json<ApiError>(
449
+ { message: 'Internal server error', code: 'INTERNAL_ERROR' },
450
+ { status: 500 }
451
+ );
452
+ }),
453
+
454
+ // Network delay simulation
455
+ http.get(`${API_BASE}/slow`, async () => {
456
+ await delay(3000);
457
+ return HttpResponse.json({ message: 'Slow response' });
458
+ }),
459
+ ];
460
+ ```
461
+
462
+ ### mocks/server.ts
463
+ ```typescript
464
+ import { setupServer } from 'msw/node';
465
+ import { handlers } from './handlers';
466
+
467
+ // Servidor para Node.js (tests)
468
+ export const server = setupServer(...handlers);
469
+ ```
470
+
471
+ ### mocks/browser.ts
472
+ ```typescript
473
+ import { setupWorker } from 'msw/browser';
474
+ import { handlers } from './handlers';
475
+
476
+ // Worker para browser (desarrollo)
477
+ export const worker = setupWorker(...handlers);
478
+ ```
479
+
480
+ ## Paso 5: Ejemplos de Tests
481
+
482
+ ### tests/unit/utils.test.ts
483
+ ```typescript
484
+ import { describe, it, expect, vi } from 'vitest';
485
+
486
+ // Ejemplo de funciones a testear
487
+ function formatCurrency(amount: number, currency = 'USD'): string {
488
+ return new Intl.NumberFormat('en-US', {
489
+ style: 'currency',
490
+ currency,
491
+ }).format(amount);
492
+ }
493
+
494
+ function validateEmail(email: string): boolean {
495
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
496
+ return emailRegex.test(email);
497
+ }
498
+
499
+ async function fetchWithRetry(
500
+ url: string,
501
+ retries = 3
502
+ ): Promise<Response> {
503
+ for (let i = 0; i < retries; i++) {
504
+ try {
505
+ const response = await fetch(url);
506
+ if (response.ok) return response;
507
+ } catch {
508
+ if (i === retries - 1) throw new Error('Max retries reached');
509
+ }
510
+ }
511
+ throw new Error('Max retries reached');
512
+ }
513
+
514
+ // Tests
515
+ describe('formatCurrency', () => {
516
+ it('should format USD currency correctly', () => {
517
+ expect(formatCurrency(100)).toBe('$100.00');
518
+ expect(formatCurrency(1234.56)).toBe('$1,234.56');
519
+ expect(formatCurrency(0)).toBe('$0.00');
520
+ });
521
+
522
+ it('should format other currencies', () => {
523
+ expect(formatCurrency(100, 'EUR')).toContain('100');
524
+ expect(formatCurrency(100, 'GBP')).toContain('100');
525
+ });
526
+
527
+ it('should handle negative amounts', () => {
528
+ expect(formatCurrency(-50)).toBe('-$50.00');
529
+ });
530
+ });
531
+
532
+ describe('validateEmail', () => {
533
+ it('should return true for valid emails', () => {
534
+ expect(validateEmail('test@example.com')).toBe(true);
535
+ expect(validateEmail('user.name@domain.org')).toBe(true);
536
+ expect(validateEmail('user+tag@example.co.uk')).toBe(true);
537
+ });
538
+
539
+ it('should return false for invalid emails', () => {
540
+ expect(validateEmail('invalid')).toBe(false);
541
+ expect(validateEmail('no@domain')).toBe(false);
542
+ expect(validateEmail('@nodomain.com')).toBe(false);
543
+ expect(validateEmail('spaces in@email.com')).toBe(false);
544
+ });
545
+ });
546
+
547
+ describe('fetchWithRetry', () => {
548
+ it('should return response on success', async () => {
549
+ const mockResponse = new Response(JSON.stringify({ data: 'test' }), {
550
+ status: 200,
551
+ });
552
+
553
+ vi.spyOn(global, 'fetch').mockResolvedValueOnce(mockResponse);
554
+
555
+ const result = await fetchWithRetry('https://api.example.com/data');
556
+
557
+ expect(result).toBe(mockResponse);
558
+ expect(fetch).toHaveBeenCalledTimes(1);
559
+ });
560
+
561
+ it('should retry on failure', async () => {
562
+ const mockFetch = vi.spyOn(global, 'fetch');
563
+
564
+ mockFetch
565
+ .mockRejectedValueOnce(new Error('Network error'))
566
+ .mockRejectedValueOnce(new Error('Network error'))
567
+ .mockResolvedValueOnce(new Response('{}', { status: 200 }));
568
+
569
+ await fetchWithRetry('https://api.example.com/data');
570
+
571
+ expect(fetch).toHaveBeenCalledTimes(3);
572
+ });
573
+
574
+ it('should throw after max retries', async () => {
575
+ vi.spyOn(global, 'fetch').mockRejectedValue(new Error('Network error'));
576
+
577
+ await expect(
578
+ fetchWithRetry('https://api.example.com/data', 3)
579
+ ).rejects.toThrow('Max retries reached');
580
+ });
581
+ });
582
+ ```
583
+
584
+ ### tests/unit/components/Button.test.tsx
585
+ ```typescript
586
+ import { describe, it, expect, vi } from 'vitest';
587
+ import { render, screen } from '@testing-library/react';
588
+ import userEvent from '@testing-library/user-event';
589
+
590
+ // Componente de ejemplo
591
+ interface ButtonProps {
592
+ children: React.ReactNode;
593
+ onClick?: () => void;
594
+ disabled?: boolean;
595
+ variant?: 'primary' | 'secondary' | 'danger';
596
+ loading?: boolean;
597
+ }
598
+
599
+ function Button({
600
+ children,
601
+ onClick,
602
+ disabled = false,
603
+ variant = 'primary',
604
+ loading = false,
605
+ }: ButtonProps) {
606
+ return (
607
+ <button
608
+ onClick={onClick}
609
+ disabled={disabled || loading}
610
+ className={`btn btn-${variant}`}
611
+ aria-busy={loading}
612
+ >
613
+ {loading ? 'Loading...' : children}
614
+ </button>
615
+ );
616
+ }
617
+
618
+ // Tests
619
+ describe('Button Component', () => {
620
+ it('should render children correctly', () => {
621
+ render(<Button>Click me</Button>);
622
+
623
+ expect(screen.getByRole('button')).toHaveTextContent('Click me');
624
+ });
625
+
626
+ it('should call onClick when clicked', async () => {
627
+ const user = userEvent.setup();
628
+ const handleClick = vi.fn();
629
+
630
+ render(<Button onClick={handleClick}>Click me</Button>);
631
+
632
+ await user.click(screen.getByRole('button'));
633
+
634
+ expect(handleClick).toHaveBeenCalledTimes(1);
635
+ });
636
+
637
+ it('should not call onClick when disabled', async () => {
638
+ const user = userEvent.setup();
639
+ const handleClick = vi.fn();
640
+
641
+ render(<Button onClick={handleClick} disabled>Click me</Button>);
642
+
643
+ await user.click(screen.getByRole('button'));
644
+
645
+ expect(handleClick).not.toHaveBeenCalled();
646
+ });
647
+
648
+ it('should show loading state', () => {
649
+ render(<Button loading>Submit</Button>);
650
+
651
+ const button = screen.getByRole('button');
652
+
653
+ expect(button).toHaveTextContent('Loading...');
654
+ expect(button).toBeDisabled();
655
+ expect(button).toHaveAttribute('aria-busy', 'true');
656
+ });
657
+
658
+ it('should apply variant class', () => {
659
+ render(<Button variant="danger">Delete</Button>);
660
+
661
+ expect(screen.getByRole('button')).toHaveClass('btn-danger');
662
+ });
663
+ });
664
+ ```
665
+
666
+ ### tests/integration/api.test.ts
667
+ ```typescript
668
+ import { describe, it, expect, beforeEach } from 'vitest';
669
+ import { server } from '@mocks/server';
670
+ import { http, HttpResponse } from 'msw';
671
+
672
+ const API_BASE = 'http://localhost:3001/api';
673
+
674
+ // Servicio de API a testear
675
+ class ApiService {
676
+ private baseUrl: string;
677
+ private token: string | null = null;
678
+
679
+ constructor(baseUrl: string) {
680
+ this.baseUrl = baseUrl;
681
+ }
682
+
683
+ setToken(token: string) {
684
+ this.token = token;
685
+ }
686
+
687
+ private async request<T>(
688
+ endpoint: string,
689
+ options: RequestInit = {}
690
+ ): Promise<T> {
691
+ const headers: HeadersInit = {
692
+ 'Content-Type': 'application/json',
693
+ ...(this.token && { Authorization: `Bearer ${this.token}` }),
694
+ ...options.headers,
695
+ };
696
+
697
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
698
+ ...options,
699
+ headers,
700
+ });
701
+
702
+ if (!response.ok) {
703
+ const error = await response.json();
704
+ throw new Error(error.message);
705
+ }
706
+
707
+ return response.json();
708
+ }
709
+
710
+ async login(email: string, password: string) {
711
+ return this.request<{ user: any; token: string }>('/auth/login', {
712
+ method: 'POST',
713
+ body: JSON.stringify({ email, password }),
714
+ });
715
+ }
716
+
717
+ async getUsers() {
718
+ return this.request<{ users: any[] }>('/users');
719
+ }
720
+
721
+ async getUser(id: string) {
722
+ return this.request<{ user: any }>(`/users/${id}`);
723
+ }
724
+ }
725
+
726
+ describe('ApiService Integration', () => {
727
+ let api: ApiService;
728
+
729
+ beforeEach(() => {
730
+ api = new ApiService(API_BASE);
731
+ });
732
+
733
+ describe('Authentication', () => {
734
+ it('should login successfully with valid credentials', async () => {
735
+ const result = await api.login('test@example.com', 'password123');
736
+
737
+ expect(result.user).toBeDefined();
738
+ expect(result.user.email).toBe('test@example.com');
739
+ expect(result.token).toBe('mock-jwt-token');
740
+ });
741
+
742
+ it('should fail login with invalid credentials', async () => {
743
+ await expect(
744
+ api.login('wrong@example.com', 'wrongpassword')
745
+ ).rejects.toThrow('Invalid credentials');
746
+ });
747
+ });
748
+
749
+ describe('Users', () => {
750
+ it('should fetch all users', async () => {
751
+ const result = await api.getUsers();
752
+
753
+ expect(result.users).toHaveLength(2);
754
+ expect(result.users[0].email).toBe('test@example.com');
755
+ });
756
+
757
+ it('should fetch a single user by id', async () => {
758
+ const result = await api.getUser('1');
759
+
760
+ expect(result.user.id).toBe('1');
761
+ expect(result.user.email).toBe('test@example.com');
762
+ });
763
+
764
+ it('should handle user not found', async () => {
765
+ await expect(api.getUser('999')).rejects.toThrow('User not found');
766
+ });
767
+ });
768
+
769
+ describe('Error Handling', () => {
770
+ it('should handle server errors', async () => {
771
+ server.use(
772
+ http.get(`${API_BASE}/users`, () => {
773
+ return HttpResponse.json(
774
+ { message: 'Database connection failed' },
775
+ { status: 500 }
776
+ );
777
+ })
778
+ );
779
+
780
+ await expect(api.getUsers()).rejects.toThrow('Database connection failed');
781
+ });
782
+
783
+ it('should handle network errors', async () => {
784
+ server.use(
785
+ http.get(`${API_BASE}/users`, () => {
786
+ return HttpResponse.error();
787
+ })
788
+ );
789
+
790
+ await expect(api.getUsers()).rejects.toThrow();
791
+ });
792
+ });
793
+ });
794
+ ```
795
+
796
+ ### tests/e2e/auth.spec.ts
797
+ ```typescript
798
+ import { test, expect } from '@playwright/test';
799
+
800
+ test.describe('Authentication', () => {
801
+ test.beforeEach(async ({ page }) => {
802
+ await page.goto('/');
803
+ });
804
+
805
+ test('should display login form', async ({ page }) => {
806
+ await page.goto('/login');
807
+
808
+ await expect(page.getByRole('heading', { name: /login/i })).toBeVisible();
809
+ await expect(page.getByLabel(/email/i)).toBeVisible();
810
+ await expect(page.getByLabel(/password/i)).toBeVisible();
811
+ await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible();
812
+ });
813
+
814
+ test('should show validation errors for empty form', async ({ page }) => {
815
+ await page.goto('/login');
816
+
817
+ await page.getByRole('button', { name: /sign in/i }).click();
818
+
819
+ await expect(page.getByText(/email is required/i)).toBeVisible();
820
+ await expect(page.getByText(/password is required/i)).toBeVisible();
821
+ });
822
+
823
+ test('should show error for invalid credentials', async ({ page }) => {
824
+ await page.goto('/login');
825
+
826
+ await page.getByLabel(/email/i).fill('wrong@example.com');
827
+ await page.getByLabel(/password/i).fill('wrongpassword');
828
+ await page.getByRole('button', { name: /sign in/i }).click();
829
+
830
+ await expect(page.getByText(/invalid credentials/i)).toBeVisible();
831
+ });
832
+
833
+ test('should login successfully and redirect', async ({ page }) => {
834
+ await page.goto('/login');
835
+
836
+ await page.getByLabel(/email/i).fill('test@example.com');
837
+ await page.getByLabel(/password/i).fill('password123');
838
+ await page.getByRole('button', { name: /sign in/i }).click();
839
+
840
+ // Esperar redireccion al dashboard
841
+ await expect(page).toHaveURL('/dashboard');
842
+ await expect(page.getByText(/welcome/i)).toBeVisible();
843
+ });
844
+
845
+ test('should logout successfully', async ({ page }) => {
846
+ // Login primero
847
+ await page.goto('/login');
848
+ await page.getByLabel(/email/i).fill('test@example.com');
849
+ await page.getByLabel(/password/i).fill('password123');
850
+ await page.getByRole('button', { name: /sign in/i }).click();
851
+
852
+ await expect(page).toHaveURL('/dashboard');
853
+
854
+ // Logout
855
+ await page.getByRole('button', { name: /logout/i }).click();
856
+
857
+ await expect(page).toHaveURL('/login');
858
+ });
859
+
860
+ test('should persist session after page refresh', async ({ page }) => {
861
+ // Login
862
+ await page.goto('/login');
863
+ await page.getByLabel(/email/i).fill('test@example.com');
864
+ await page.getByLabel(/password/i).fill('password123');
865
+ await page.getByRole('button', { name: /sign in/i }).click();
866
+
867
+ await expect(page).toHaveURL('/dashboard');
868
+
869
+ // Refresh page
870
+ await page.reload();
871
+
872
+ // Should still be on dashboard
873
+ await expect(page).toHaveURL('/dashboard');
874
+ });
875
+ });
876
+
877
+ test.describe('Protected Routes', () => {
878
+ test('should redirect to login for protected routes', async ({ page }) => {
879
+ await page.goto('/dashboard');
880
+
881
+ await expect(page).toHaveURL('/login');
882
+ });
883
+
884
+ test('should redirect back after login', async ({ page }) => {
885
+ // Try to access protected route
886
+ await page.goto('/settings');
887
+
888
+ // Should redirect to login with return URL
889
+ await expect(page).toHaveURL(/\/login\?.*returnUrl/);
890
+
891
+ // Login
892
+ await page.getByLabel(/email/i).fill('test@example.com');
893
+ await page.getByLabel(/password/i).fill('password123');
894
+ await page.getByRole('button', { name: /sign in/i }).click();
895
+
896
+ // Should redirect to original destination
897
+ await expect(page).toHaveURL('/settings');
898
+ });
899
+ });
900
+ ```
901
+
902
+ ### tests/e2e/checkout.spec.ts
903
+ ```typescript
904
+ import { test, expect } from '@playwright/test';
905
+
906
+ test.describe('Checkout Flow', () => {
907
+ test.beforeEach(async ({ page }) => {
908
+ // Login before each test
909
+ await page.goto('/login');
910
+ await page.getByLabel(/email/i).fill('test@example.com');
911
+ await page.getByLabel(/password/i).fill('password123');
912
+ await page.getByRole('button', { name: /sign in/i }).click();
913
+ await expect(page).toHaveURL('/dashboard');
914
+ });
915
+
916
+ test('should complete checkout flow', async ({ page }) => {
917
+ // Add item to cart
918
+ await page.goto('/products');
919
+ await page.getByTestId('product-1').getByRole('button', { name: /add to cart/i }).click();
920
+
921
+ // Verify cart updated
922
+ await expect(page.getByTestId('cart-count')).toHaveText('1');
923
+
924
+ // Go to cart
925
+ await page.getByTestId('cart-icon').click();
926
+ await expect(page).toHaveURL('/cart');
927
+
928
+ // Proceed to checkout
929
+ await page.getByRole('button', { name: /checkout/i }).click();
930
+ await expect(page).toHaveURL('/checkout');
931
+
932
+ // Fill shipping info
933
+ await page.getByLabel(/address/i).fill('123 Main St');
934
+ await page.getByLabel(/city/i).fill('New York');
935
+ await page.getByLabel(/zip/i).fill('10001');
936
+
937
+ // Fill payment info
938
+ await page.getByLabel(/card number/i).fill('4242424242424242');
939
+ await page.getByLabel(/expiry/i).fill('12/25');
940
+ await page.getByLabel(/cvc/i).fill('123');
941
+
942
+ // Place order
943
+ await page.getByRole('button', { name: /place order/i }).click();
944
+
945
+ // Verify success
946
+ await expect(page).toHaveURL(/\/orders\/\d+/);
947
+ await expect(page.getByText(/order confirmed/i)).toBeVisible();
948
+ });
949
+
950
+ test('should validate payment information', async ({ page }) => {
951
+ await page.goto('/checkout');
952
+
953
+ // Try to submit with invalid card
954
+ await page.getByLabel(/card number/i).fill('1234');
955
+ await page.getByRole('button', { name: /place order/i }).click();
956
+
957
+ await expect(page.getByText(/invalid card number/i)).toBeVisible();
958
+ });
959
+
960
+ test('should handle empty cart', async ({ page }) => {
961
+ await page.goto('/checkout');
962
+
963
+ await expect(page.getByText(/your cart is empty/i)).toBeVisible();
964
+ await expect(page.getByRole('link', { name: /continue shopping/i })).toBeVisible();
965
+ });
966
+ });
967
+
968
+ test.describe('Visual Regression', () => {
969
+ test('checkout page matches snapshot', async ({ page }) => {
970
+ await page.goto('/login');
971
+ await page.getByLabel(/email/i).fill('test@example.com');
972
+ await page.getByLabel(/password/i).fill('password123');
973
+ await page.getByRole('button', { name: /sign in/i }).click();
974
+
975
+ await page.goto('/checkout');
976
+
977
+ // Wait for content to load
978
+ await page.waitForLoadState('networkidle');
979
+
980
+ await expect(page).toHaveScreenshot('checkout-page.png', {
981
+ fullPage: true,
982
+ mask: [page.getByTestId('dynamic-content')], // Mask dynamic content
983
+ });
984
+ });
985
+ });
986
+ ```
987
+
988
+ ## Paso 6: Scripts en package.json
989
+
990
+ ```json
991
+ {
992
+ "scripts": {
993
+ "test": "vitest run",
994
+ "test:watch": "vitest",
995
+ "test:ui": "vitest --ui",
996
+ "test:coverage": "vitest run --coverage",
997
+ "test:unit": "vitest run tests/unit",
998
+ "test:integration": "vitest run tests/integration",
999
+ "test:e2e": "playwright test",
1000
+ "test:e2e:ui": "playwright test --ui",
1001
+ "test:e2e:headed": "playwright test --headed",
1002
+ "test:e2e:debug": "playwright test --debug",
1003
+ "test:e2e:codegen": "playwright codegen localhost:3000",
1004
+ "test:e2e:report": "playwright show-report",
1005
+ "test:all": "npm run test && npm run test:e2e",
1006
+ "test:ci": "vitest run --coverage && playwright test"
1007
+ }
1008
+ }
1009
+ ```
1010
+
1011
+ ## Paso 7: Configuracion de CI (GitHub Actions)
1012
+
1013
+ ### .github/workflows/test.yml
1014
+ ```yaml
1015
+ name: Tests
1016
+
1017
+ on:
1018
+ push:
1019
+ branches: [main, develop]
1020
+ pull_request:
1021
+ branches: [main, develop]
1022
+
1023
+ jobs:
1024
+ unit-tests:
1025
+ runs-on: ubuntu-latest
1026
+ steps:
1027
+ - uses: actions/checkout@v4
1028
+
1029
+ - name: Setup Node.js
1030
+ uses: actions/setup-node@v4
1031
+ with:
1032
+ node-version: '20'
1033
+ cache: 'npm'
1034
+
1035
+ - name: Install dependencies
1036
+ run: npm ci
1037
+
1038
+ - name: Run unit tests
1039
+ run: npm run test:coverage
1040
+
1041
+ - name: Upload coverage
1042
+ uses: codecov/codecov-action@v4
1043
+ with:
1044
+ files: ./coverage/lcov.info
1045
+ fail_ci_if_error: true
1046
+
1047
+ e2e-tests:
1048
+ runs-on: ubuntu-latest
1049
+ steps:
1050
+ - uses: actions/checkout@v4
1051
+
1052
+ - name: Setup Node.js
1053
+ uses: actions/setup-node@v4
1054
+ with:
1055
+ node-version: '20'
1056
+ cache: 'npm'
1057
+
1058
+ - name: Install dependencies
1059
+ run: npm ci
1060
+
1061
+ - name: Install Playwright browsers
1062
+ run: npx playwright install --with-deps
1063
+
1064
+ - name: Run E2E tests
1065
+ run: npm run test:e2e
1066
+
1067
+ - name: Upload report
1068
+ uses: actions/upload-artifact@v4
1069
+ if: always()
1070
+ with:
1071
+ name: playwright-report
1072
+ path: playwright-report/
1073
+ retention-days: 30
1074
+ ```
1075
+ </setup_steps>
1076
+
1077
+ <best_practices>
1078
+ ## Mejores Practicas
1079
+
1080
+ ### 1. Estructura de Tests
1081
+ ```typescript
1082
+ describe('Feature/Component', () => {
1083
+ describe('Scenario/Method', () => {
1084
+ it('should [expected behavior] when [condition]', () => {
1085
+ // Arrange - setup
1086
+ // Act - execute
1087
+ // Assert - verify
1088
+ });
1089
+ });
1090
+ });
1091
+ ```
1092
+
1093
+ ### 2. Nombres Descriptivos
1094
+ ```typescript
1095
+ // MAL
1096
+ it('works', () => {});
1097
+ it('test 1', () => {});
1098
+
1099
+ // BIEN
1100
+ it('should return empty array when no items match filter', () => {});
1101
+ it('should throw ValidationError when email is invalid', () => {});
1102
+ ```
1103
+
1104
+ ### 3. Un Assert por Test (cuando posible)
1105
+ ```typescript
1106
+ // Preferir multiples tests especificos
1107
+ it('should validate email format', () => {
1108
+ expect(validateEmail('test@example.com')).toBe(true);
1109
+ });
1110
+
1111
+ it('should reject email without domain', () => {
1112
+ expect(validateEmail('test@')).toBe(false);
1113
+ });
1114
+ ```
1115
+
1116
+ ### 4. Evitar Tests Fragiles
1117
+ ```typescript
1118
+ // MAL - depende de implementacion
1119
+ expect(result).toEqual({ id: 1, name: 'Test', createdAt: '2024-01-01' });
1120
+
1121
+ // BIEN - verifica lo importante
1122
+ expect(result.id).toBe(1);
1123
+ expect(result.name).toBe('Test');
1124
+ expect(result.createdAt).toBeDefined();
1125
+ ```
1126
+
1127
+ ### 5. Data Builders para Tests
1128
+ ```typescript
1129
+ // tests/factories/user.ts
1130
+ export function createUser(overrides: Partial<User> = {}): User {
1131
+ return {
1132
+ id: '1',
1133
+ email: 'test@example.com',
1134
+ name: 'Test User',
1135
+ ...overrides,
1136
+ };
1137
+ }
1138
+
1139
+ // Uso en test
1140
+ const user = createUser({ name: 'Custom Name' });
1141
+ ```
1142
+ </best_practices>
1143
+
1144
+ <verification>
1145
+ ## Verificacion
1146
+
1147
+ 1. **Unit Tests**
1148
+ ```bash
1149
+ npm run test
1150
+ # Deberia mostrar tests pasando
1151
+ ```
1152
+
1153
+ 2. **Coverage Report**
1154
+ ```bash
1155
+ npm run test:coverage
1156
+ # Deberia generar reporte en ./coverage
1157
+ # Abrir coverage/index.html en browser
1158
+ ```
1159
+
1160
+ 3. **E2E Tests**
1161
+ ```bash
1162
+ npm run test:e2e
1163
+ # Deberia correr tests en multiples browsers
1164
+ ```
1165
+
1166
+ 4. **E2E Report**
1167
+ ```bash
1168
+ npm run test:e2e:report
1169
+ # Abre reporte HTML interactivo
1170
+ ```
1171
+
1172
+ 5. **Vitest UI**
1173
+ ```bash
1174
+ npm run test:ui
1175
+ # Abre interfaz visual para tests
1176
+ ```
1177
+ </verification>
1178
+
1179
+ <troubleshooting>
1180
+ ## Solucion de Problemas
1181
+
1182
+ ### Error: "Cannot find module"
1183
+ ```bash
1184
+ # Verificar que las dependencias estan instaladas
1185
+ npm install
1186
+
1187
+ # Verificar paths en vitest.config.ts
1188
+ ```
1189
+
1190
+ ### Error: "MSW handlers not working"
1191
+ ```typescript
1192
+ // Verificar que server.listen() se llama en setup
1193
+ // Verificar URL base en handlers
1194
+ ```
1195
+
1196
+ ### Error: "Playwright browsers not found"
1197
+ ```bash
1198
+ npx playwright install
1199
+ npx playwright install-deps
1200
+ ```
1201
+
1202
+ ### Tests lentos
1203
+ ```typescript
1204
+ // vitest.config.ts - usar pool forks
1205
+ pool: 'forks',
1206
+ poolOptions: {
1207
+ forks: {
1208
+ singleFork: true,
1209
+ },
1210
+ },
1211
+ ```
1212
+
1213
+ ### Coverage bajo
1214
+ ```bash
1215
+ # Verificar que archivos correctos estan incluidos
1216
+ # Revisar exclude patterns en vitest.config.ts
1217
+ ```
1218
+ </troubleshooting>