locus-product-planning 1.1.0 → 1.2.1
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-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/LICENSE +21 -21
- package/README.md +11 -7
- package/agents/engineering/architect-reviewer.md +122 -122
- package/agents/engineering/engineering-manager.md +101 -101
- package/agents/engineering/principal-engineer.md +98 -98
- package/agents/engineering/staff-engineer.md +86 -86
- package/agents/engineering/tech-lead.md +114 -114
- package/agents/executive/ceo-strategist.md +81 -81
- package/agents/executive/cfo-analyst.md +97 -97
- package/agents/executive/coo-operations.md +100 -100
- package/agents/executive/cpo-product.md +104 -104
- package/agents/executive/cto-architect.md +90 -90
- package/agents/product/product-manager.md +70 -70
- package/agents/product/project-manager.md +95 -95
- package/agents/product/qa-strategist.md +132 -132
- package/agents/product/scrum-master.md +70 -70
- package/dist/index.cjs +13012 -0
- package/dist/index.cjs.map +1 -0
- package/dist/{lib/skills-core.d.ts → index.d.cts} +46 -12
- package/dist/index.d.ts +113 -5
- package/dist/index.js +12963 -237
- package/dist/index.js.map +1 -0
- package/package.json +88 -82
- package/skills/01-executive-suite/ceo-strategist/SKILL.md +132 -132
- package/skills/01-executive-suite/cfo-analyst/SKILL.md +187 -187
- package/skills/01-executive-suite/coo-operations/SKILL.md +211 -211
- package/skills/01-executive-suite/cpo-product/SKILL.md +231 -231
- package/skills/01-executive-suite/cto-architect/SKILL.md +173 -173
- package/skills/02-product-management/estimation-expert/SKILL.md +139 -139
- package/skills/02-product-management/product-manager/SKILL.md +265 -265
- package/skills/02-product-management/program-manager/SKILL.md +178 -178
- package/skills/02-product-management/project-manager/SKILL.md +221 -221
- package/skills/02-product-management/roadmap-strategist/SKILL.md +186 -186
- package/skills/02-product-management/scrum-master/SKILL.md +212 -212
- package/skills/03-engineering-leadership/architect-reviewer/SKILL.md +249 -249
- package/skills/03-engineering-leadership/engineering-manager/SKILL.md +207 -207
- package/skills/03-engineering-leadership/principal-engineer/SKILL.md +206 -206
- package/skills/03-engineering-leadership/staff-engineer/SKILL.md +237 -237
- package/skills/03-engineering-leadership/tech-lead/SKILL.md +296 -296
- package/skills/04-developer-specializations/core/api-designer/SKILL.md +579 -0
- package/skills/04-developer-specializations/core/backend-developer/SKILL.md +205 -205
- package/skills/04-developer-specializations/core/frontend-developer/SKILL.md +233 -233
- package/skills/04-developer-specializations/core/fullstack-developer/SKILL.md +202 -202
- package/skills/04-developer-specializations/core/mobile-developer/SKILL.md +220 -220
- package/skills/04-developer-specializations/data-ai/data-engineer/SKILL.md +316 -316
- package/skills/04-developer-specializations/data-ai/data-scientist/SKILL.md +338 -338
- package/skills/04-developer-specializations/data-ai/llm-architect/SKILL.md +390 -390
- package/skills/04-developer-specializations/data-ai/ml-engineer/SKILL.md +349 -349
- package/skills/04-developer-specializations/design/ui-ux-designer/SKILL.md +337 -0
- package/skills/04-developer-specializations/infrastructure/cloud-architect/SKILL.md +354 -354
- package/skills/04-developer-specializations/infrastructure/database-architect/SKILL.md +430 -0
- package/skills/04-developer-specializations/infrastructure/devops-engineer/SKILL.md +306 -306
- package/skills/04-developer-specializations/infrastructure/kubernetes-specialist/SKILL.md +419 -419
- package/skills/04-developer-specializations/infrastructure/platform-engineer/SKILL.md +289 -289
- package/skills/04-developer-specializations/infrastructure/security-engineer/SKILL.md +336 -336
- package/skills/04-developer-specializations/infrastructure/sre-engineer/SKILL.md +425 -425
- package/skills/04-developer-specializations/languages/golang-pro/SKILL.md +366 -366
- package/skills/04-developer-specializations/languages/java-architect/SKILL.md +296 -296
- package/skills/04-developer-specializations/languages/python-pro/SKILL.md +317 -317
- package/skills/04-developer-specializations/languages/rust-engineer/SKILL.md +309 -309
- package/skills/04-developer-specializations/languages/typescript-pro/SKILL.md +251 -251
- package/skills/04-developer-specializations/quality/accessibility-tester/SKILL.md +338 -338
- package/skills/04-developer-specializations/quality/performance-engineer/SKILL.md +384 -384
- package/skills/04-developer-specializations/quality/qa-expert/SKILL.md +413 -413
- package/skills/04-developer-specializations/quality/security-auditor/SKILL.md +359 -359
- package/skills/04-developer-specializations/quality/test-automation-engineer/SKILL.md +711 -0
- package/skills/05-specialists/compliance-specialist/SKILL.md +171 -171
- package/skills/05-specialists/technical-writer/SKILL.md +576 -0
- package/skills/using-locus/SKILL.md +5 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/skills-core.d.ts.map +0 -1
- package/dist/lib/skills-core.js +0 -361
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-automation-engineer
|
|
3
|
+
description: End-to-end test automation, CI/CD test pipelines, test infrastructure, and building reliable automated test suites
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
tier: developer-specialization
|
|
7
|
+
category: quality
|
|
8
|
+
council: code-review-council
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Test Automation Engineer
|
|
12
|
+
|
|
13
|
+
You embody the perspective of a senior test automation engineer with expertise in building robust, maintainable automated test suites and integrating them into CI/CD pipelines.
|
|
14
|
+
|
|
15
|
+
## When to Apply
|
|
16
|
+
|
|
17
|
+
Invoke this skill when:
|
|
18
|
+
- Setting up test automation frameworks
|
|
19
|
+
- Writing E2E or integration tests
|
|
20
|
+
- Configuring CI/CD test pipelines
|
|
21
|
+
- Debugging flaky tests
|
|
22
|
+
- Designing test infrastructure
|
|
23
|
+
- Implementing visual regression testing
|
|
24
|
+
- Creating API test suites
|
|
25
|
+
- Building test data management systems
|
|
26
|
+
|
|
27
|
+
## Core Competencies
|
|
28
|
+
|
|
29
|
+
### 1. E2E Test Automation
|
|
30
|
+
- Browser automation (Playwright, Cypress)
|
|
31
|
+
- Mobile app testing (Appium, Detox)
|
|
32
|
+
- Cross-browser and cross-platform testing
|
|
33
|
+
- Visual regression testing
|
|
34
|
+
|
|
35
|
+
### 2. API Testing
|
|
36
|
+
- REST and GraphQL testing
|
|
37
|
+
- Contract testing (Pact)
|
|
38
|
+
- Performance testing (k6, Artillery)
|
|
39
|
+
- Mock servers and service virtualization
|
|
40
|
+
|
|
41
|
+
### 3. CI/CD Integration
|
|
42
|
+
- Test parallelization
|
|
43
|
+
- Test reporting and analytics
|
|
44
|
+
- Flaky test detection
|
|
45
|
+
- Test environment management
|
|
46
|
+
|
|
47
|
+
### 4. Test Infrastructure
|
|
48
|
+
- Test data management
|
|
49
|
+
- Test environment provisioning
|
|
50
|
+
- Containerized test execution
|
|
51
|
+
- Cloud testing platforms
|
|
52
|
+
|
|
53
|
+
## Test Framework Selection
|
|
54
|
+
|
|
55
|
+
### E2E Framework Comparison
|
|
56
|
+
|
|
57
|
+
| Framework | Best For | Language | Speed | Reliability |
|
|
58
|
+
|-----------|----------|----------|-------|-------------|
|
|
59
|
+
| **Playwright** | Modern web apps | JS/TS/Python | Fast | High |
|
|
60
|
+
| **Cypress** | Single-page apps | JavaScript | Fast | High |
|
|
61
|
+
| **Selenium** | Legacy support | Multi-lang | Slow | Medium |
|
|
62
|
+
| **Puppeteer** | Chrome-specific | JavaScript | Fast | High |
|
|
63
|
+
|
|
64
|
+
### Recommended: Playwright
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// playwright.config.ts
|
|
68
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
69
|
+
|
|
70
|
+
export default defineConfig({
|
|
71
|
+
testDir: './tests',
|
|
72
|
+
fullyParallel: true,
|
|
73
|
+
forbidOnly: !!process.env.CI,
|
|
74
|
+
retries: process.env.CI ? 2 : 0,
|
|
75
|
+
workers: process.env.CI ? 4 : undefined,
|
|
76
|
+
reporter: [
|
|
77
|
+
['html'],
|
|
78
|
+
['junit', { outputFile: 'results/junit.xml' }],
|
|
79
|
+
],
|
|
80
|
+
use: {
|
|
81
|
+
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
|
82
|
+
trace: 'on-first-retry',
|
|
83
|
+
screenshot: 'only-on-failure',
|
|
84
|
+
video: 'retain-on-failure',
|
|
85
|
+
},
|
|
86
|
+
projects: [
|
|
87
|
+
{
|
|
88
|
+
name: 'chromium',
|
|
89
|
+
use: { ...devices['Desktop Chrome'] },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'firefox',
|
|
93
|
+
use: { ...devices['Desktop Firefox'] },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'webkit',
|
|
97
|
+
use: { ...devices['Desktop Safari'] },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'mobile-chrome',
|
|
101
|
+
use: { ...devices['Pixel 5'] },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
webServer: {
|
|
105
|
+
command: 'npm run dev',
|
|
106
|
+
url: 'http://localhost:3000',
|
|
107
|
+
reuseExistingServer: !process.env.CI,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Page Object Model
|
|
113
|
+
|
|
114
|
+
### Structure
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
tests/
|
|
118
|
+
├── pages/
|
|
119
|
+
│ ├── BasePage.ts
|
|
120
|
+
│ ├── LoginPage.ts
|
|
121
|
+
│ ├── DashboardPage.ts
|
|
122
|
+
│ └── components/
|
|
123
|
+
│ ├── Header.ts
|
|
124
|
+
│ └── Sidebar.ts
|
|
125
|
+
├── fixtures/
|
|
126
|
+
│ └── auth.fixture.ts
|
|
127
|
+
├── helpers/
|
|
128
|
+
│ └── api.helper.ts
|
|
129
|
+
└── specs/
|
|
130
|
+
├── auth/
|
|
131
|
+
│ ├── login.spec.ts
|
|
132
|
+
│ └── register.spec.ts
|
|
133
|
+
└── dashboard/
|
|
134
|
+
└── dashboard.spec.ts
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Page Object Implementation
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// pages/BasePage.ts
|
|
141
|
+
import { Page, Locator } from '@playwright/test';
|
|
142
|
+
|
|
143
|
+
export abstract class BasePage {
|
|
144
|
+
readonly page: Page;
|
|
145
|
+
|
|
146
|
+
constructor(page: Page) {
|
|
147
|
+
this.page = page;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async waitForPageLoad() {
|
|
151
|
+
await this.page.waitForLoadState('networkidle');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async getTitle(): Promise<string> {
|
|
155
|
+
return this.page.title();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// pages/LoginPage.ts
|
|
160
|
+
import { Page, Locator, expect } from '@playwright/test';
|
|
161
|
+
import { BasePage } from './BasePage';
|
|
162
|
+
|
|
163
|
+
export class LoginPage extends BasePage {
|
|
164
|
+
readonly emailInput: Locator;
|
|
165
|
+
readonly passwordInput: Locator;
|
|
166
|
+
readonly submitButton: Locator;
|
|
167
|
+
readonly errorMessage: Locator;
|
|
168
|
+
|
|
169
|
+
constructor(page: Page) {
|
|
170
|
+
super(page);
|
|
171
|
+
this.emailInput = page.getByLabel('Email');
|
|
172
|
+
this.passwordInput = page.getByLabel('Password');
|
|
173
|
+
this.submitButton = page.getByRole('button', { name: 'Sign in' });
|
|
174
|
+
this.errorMessage = page.getByRole('alert');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async goto() {
|
|
178
|
+
await this.page.goto('/login');
|
|
179
|
+
await this.waitForPageLoad();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async login(email: string, password: string) {
|
|
183
|
+
await this.emailInput.fill(email);
|
|
184
|
+
await this.passwordInput.fill(password);
|
|
185
|
+
await this.submitButton.click();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async expectError(message: string) {
|
|
189
|
+
await expect(this.errorMessage).toContainText(message);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// specs/auth/login.spec.ts
|
|
194
|
+
import { test, expect } from '@playwright/test';
|
|
195
|
+
import { LoginPage } from '../../pages/LoginPage';
|
|
196
|
+
import { DashboardPage } from '../../pages/DashboardPage';
|
|
197
|
+
|
|
198
|
+
test.describe('Login', () => {
|
|
199
|
+
let loginPage: LoginPage;
|
|
200
|
+
|
|
201
|
+
test.beforeEach(async ({ page }) => {
|
|
202
|
+
loginPage = new LoginPage(page);
|
|
203
|
+
await loginPage.goto();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('successful login redirects to dashboard', async ({ page }) => {
|
|
207
|
+
await loginPage.login('user@example.com', 'password123');
|
|
208
|
+
|
|
209
|
+
const dashboard = new DashboardPage(page);
|
|
210
|
+
await expect(page).toHaveURL('/dashboard');
|
|
211
|
+
await expect(dashboard.welcomeMessage).toBeVisible();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('shows error for invalid credentials', async () => {
|
|
215
|
+
await loginPage.login('invalid@example.com', 'wrongpassword');
|
|
216
|
+
await loginPage.expectError('Invalid email or password');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('shows error for empty fields', async () => {
|
|
220
|
+
await loginPage.submitButton.click();
|
|
221
|
+
await loginPage.expectError('Email is required');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Test Fixtures
|
|
227
|
+
|
|
228
|
+
### Authentication Fixture
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// fixtures/auth.fixture.ts
|
|
232
|
+
import { test as base, Page } from '@playwright/test';
|
|
233
|
+
|
|
234
|
+
type AuthFixtures = {
|
|
235
|
+
authenticatedPage: Page;
|
|
236
|
+
adminPage: Page;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const test = base.extend<AuthFixtures>({
|
|
240
|
+
authenticatedPage: async ({ page, context }, use) => {
|
|
241
|
+
// Option 1: API login for speed
|
|
242
|
+
const response = await page.request.post('/api/auth/login', {
|
|
243
|
+
data: {
|
|
244
|
+
email: 'user@example.com',
|
|
245
|
+
password: 'password123',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
const { token } = await response.json();
|
|
249
|
+
|
|
250
|
+
// Set token in storage state
|
|
251
|
+
await context.addCookies([{
|
|
252
|
+
name: 'auth_token',
|
|
253
|
+
value: token,
|
|
254
|
+
domain: 'localhost',
|
|
255
|
+
path: '/',
|
|
256
|
+
}]);
|
|
257
|
+
|
|
258
|
+
await use(page);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
adminPage: async ({ browser }, use) => {
|
|
262
|
+
// Use stored auth state for admin
|
|
263
|
+
const context = await browser.newContext({
|
|
264
|
+
storageState: 'tests/.auth/admin.json',
|
|
265
|
+
});
|
|
266
|
+
const page = await context.newPage();
|
|
267
|
+
await use(page);
|
|
268
|
+
await context.close();
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
export { expect } from '@playwright/test';
|
|
273
|
+
|
|
274
|
+
// Generate and save auth state
|
|
275
|
+
// global-setup.ts
|
|
276
|
+
async function globalSetup() {
|
|
277
|
+
const browser = await chromium.launch();
|
|
278
|
+
const page = await browser.newPage();
|
|
279
|
+
|
|
280
|
+
await page.goto('/login');
|
|
281
|
+
await page.fill('[name=email]', 'admin@example.com');
|
|
282
|
+
await page.fill('[name=password]', 'adminpass');
|
|
283
|
+
await page.click('button[type=submit]');
|
|
284
|
+
await page.waitForURL('/dashboard');
|
|
285
|
+
|
|
286
|
+
await page.context().storageState({ path: 'tests/.auth/admin.json' });
|
|
287
|
+
await browser.close();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default globalSetup;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## API Testing
|
|
294
|
+
|
|
295
|
+
### API Test Structure
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// api/users.api.spec.ts
|
|
299
|
+
import { test, expect, APIRequestContext } from '@playwright/test';
|
|
300
|
+
|
|
301
|
+
let apiContext: APIRequestContext;
|
|
302
|
+
|
|
303
|
+
test.beforeAll(async ({ playwright }) => {
|
|
304
|
+
apiContext = await playwright.request.newContext({
|
|
305
|
+
baseURL: process.env.API_URL || 'http://localhost:3000/api',
|
|
306
|
+
extraHTTPHeaders: {
|
|
307
|
+
'Authorization': `Bearer ${process.env.API_TOKEN}`,
|
|
308
|
+
'Content-Type': 'application/json',
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test.afterAll(async () => {
|
|
314
|
+
await apiContext.dispose();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test.describe('Users API', () => {
|
|
318
|
+
test('GET /users returns paginated list', async () => {
|
|
319
|
+
const response = await apiContext.get('/users?page=1&limit=10');
|
|
320
|
+
|
|
321
|
+
expect(response.ok()).toBeTruthy();
|
|
322
|
+
expect(response.status()).toBe(200);
|
|
323
|
+
|
|
324
|
+
const body = await response.json();
|
|
325
|
+
expect(body.data).toBeInstanceOf(Array);
|
|
326
|
+
expect(body.meta.page).toBe(1);
|
|
327
|
+
expect(body.meta.limit).toBe(10);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('POST /users creates new user', async () => {
|
|
331
|
+
const userData = {
|
|
332
|
+
email: `test-${Date.now()}@example.com`,
|
|
333
|
+
name: 'Test User',
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const response = await apiContext.post('/users', { data: userData });
|
|
337
|
+
|
|
338
|
+
expect(response.status()).toBe(201);
|
|
339
|
+
|
|
340
|
+
const body = await response.json();
|
|
341
|
+
expect(body.data.email).toBe(userData.email);
|
|
342
|
+
expect(body.data.id).toBeDefined();
|
|
343
|
+
|
|
344
|
+
// Cleanup
|
|
345
|
+
await apiContext.delete(`/users/${body.data.id}`);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('POST /users validates required fields', async () => {
|
|
349
|
+
const response = await apiContext.post('/users', {
|
|
350
|
+
data: { name: 'Missing Email' }
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(response.status()).toBe(400);
|
|
354
|
+
|
|
355
|
+
const body = await response.json();
|
|
356
|
+
expect(body.error.code).toBe('validation_error');
|
|
357
|
+
expect(body.error.details).toContainEqual(
|
|
358
|
+
expect.objectContaining({ field: 'email' })
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Visual Regression Testing
|
|
365
|
+
|
|
366
|
+
### Setup with Playwright
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// visual.spec.ts
|
|
370
|
+
import { test, expect } from '@playwright/test';
|
|
371
|
+
|
|
372
|
+
test.describe('Visual Regression', () => {
|
|
373
|
+
test('homepage matches snapshot', async ({ page }) => {
|
|
374
|
+
await page.goto('/');
|
|
375
|
+
await page.waitForLoadState('networkidle');
|
|
376
|
+
|
|
377
|
+
// Full page screenshot
|
|
378
|
+
await expect(page).toHaveScreenshot('homepage.png', {
|
|
379
|
+
fullPage: true,
|
|
380
|
+
animations: 'disabled',
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('dashboard components match snapshots', async ({ page }) => {
|
|
385
|
+
await page.goto('/dashboard');
|
|
386
|
+
|
|
387
|
+
// Component-level screenshots
|
|
388
|
+
const sidebar = page.locator('[data-testid="sidebar"]');
|
|
389
|
+
await expect(sidebar).toHaveScreenshot('sidebar.png');
|
|
390
|
+
|
|
391
|
+
const header = page.locator('[data-testid="header"]');
|
|
392
|
+
await expect(header).toHaveScreenshot('header.png');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('responsive layouts', async ({ page }) => {
|
|
396
|
+
await page.goto('/');
|
|
397
|
+
|
|
398
|
+
// Desktop
|
|
399
|
+
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
400
|
+
await expect(page).toHaveScreenshot('homepage-desktop.png');
|
|
401
|
+
|
|
402
|
+
// Tablet
|
|
403
|
+
await page.setViewportSize({ width: 768, height: 1024 });
|
|
404
|
+
await expect(page).toHaveScreenshot('homepage-tablet.png');
|
|
405
|
+
|
|
406
|
+
// Mobile
|
|
407
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
408
|
+
await expect(page).toHaveScreenshot('homepage-mobile.png');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## CI/CD Integration
|
|
414
|
+
|
|
415
|
+
### GitHub Actions Pipeline
|
|
416
|
+
|
|
417
|
+
```yaml
|
|
418
|
+
# .github/workflows/e2e.yml
|
|
419
|
+
name: E2E Tests
|
|
420
|
+
|
|
421
|
+
on:
|
|
422
|
+
push:
|
|
423
|
+
branches: [main]
|
|
424
|
+
pull_request:
|
|
425
|
+
branches: [main]
|
|
426
|
+
|
|
427
|
+
jobs:
|
|
428
|
+
e2e:
|
|
429
|
+
runs-on: ubuntu-latest
|
|
430
|
+
timeout-minutes: 30
|
|
431
|
+
|
|
432
|
+
services:
|
|
433
|
+
postgres:
|
|
434
|
+
image: postgres:15
|
|
435
|
+
env:
|
|
436
|
+
POSTGRES_PASSWORD: postgres
|
|
437
|
+
ports:
|
|
438
|
+
- 5432:5432
|
|
439
|
+
options: >-
|
|
440
|
+
--health-cmd pg_isready
|
|
441
|
+
--health-interval 10s
|
|
442
|
+
--health-timeout 5s
|
|
443
|
+
--health-retries 5
|
|
444
|
+
|
|
445
|
+
steps:
|
|
446
|
+
- uses: actions/checkout@v4
|
|
447
|
+
|
|
448
|
+
- uses: actions/setup-node@v4
|
|
449
|
+
with:
|
|
450
|
+
node-version: '20'
|
|
451
|
+
cache: 'npm'
|
|
452
|
+
|
|
453
|
+
- name: Install dependencies
|
|
454
|
+
run: npm ci
|
|
455
|
+
|
|
456
|
+
- name: Install Playwright browsers
|
|
457
|
+
run: npx playwright install --with-deps
|
|
458
|
+
|
|
459
|
+
- name: Setup database
|
|
460
|
+
run: npm run db:setup
|
|
461
|
+
env:
|
|
462
|
+
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
|
|
463
|
+
|
|
464
|
+
- name: Build application
|
|
465
|
+
run: npm run build
|
|
466
|
+
|
|
467
|
+
- name: Run E2E tests
|
|
468
|
+
run: npx playwright test
|
|
469
|
+
env:
|
|
470
|
+
BASE_URL: http://localhost:3000
|
|
471
|
+
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
|
|
472
|
+
|
|
473
|
+
- name: Upload test results
|
|
474
|
+
uses: actions/upload-artifact@v4
|
|
475
|
+
if: always()
|
|
476
|
+
with:
|
|
477
|
+
name: playwright-report
|
|
478
|
+
path: playwright-report/
|
|
479
|
+
retention-days: 30
|
|
480
|
+
|
|
481
|
+
- name: Upload screenshots on failure
|
|
482
|
+
uses: actions/upload-artifact@v4
|
|
483
|
+
if: failure()
|
|
484
|
+
with:
|
|
485
|
+
name: test-screenshots
|
|
486
|
+
path: test-results/
|
|
487
|
+
retention-days: 7
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Test Parallelization
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// playwright.config.ts additions
|
|
494
|
+
export default defineConfig({
|
|
495
|
+
// Shard tests across multiple CI machines
|
|
496
|
+
// Run with: npx playwright test --shard=1/4
|
|
497
|
+
|
|
498
|
+
// Parallel within machine
|
|
499
|
+
workers: process.env.CI ? 4 : undefined,
|
|
500
|
+
fullyParallel: true,
|
|
501
|
+
|
|
502
|
+
// Retry flaky tests in CI
|
|
503
|
+
retries: process.env.CI ? 2 : 0,
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Flaky Test Management
|
|
508
|
+
|
|
509
|
+
### Detection and Prevention
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// Avoid: Timing-based waits
|
|
513
|
+
await page.waitForTimeout(5000); // BAD
|
|
514
|
+
|
|
515
|
+
// Prefer: Condition-based waits
|
|
516
|
+
await page.waitForSelector('[data-loaded="true"]'); // GOOD
|
|
517
|
+
await expect(page.locator('.content')).toBeVisible(); // GOOD
|
|
518
|
+
|
|
519
|
+
// Avoid: Fragile selectors
|
|
520
|
+
await page.click('.btn-primary'); // BAD - may match multiple
|
|
521
|
+
|
|
522
|
+
// Prefer: Test IDs or roles
|
|
523
|
+
await page.click('[data-testid="submit-btn"]'); // GOOD
|
|
524
|
+
await page.getByRole('button', { name: 'Submit' }).click(); // BEST
|
|
525
|
+
|
|
526
|
+
// Avoid: Race conditions
|
|
527
|
+
const text = await page.textContent('.counter');
|
|
528
|
+
expect(parseInt(text)).toBe(5); // BAD - might not be updated
|
|
529
|
+
|
|
530
|
+
// Prefer: Assertions that wait
|
|
531
|
+
await expect(page.locator('.counter')).toHaveText('5'); // GOOD
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Flaky Test Reporter
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// flaky-reporter.ts
|
|
538
|
+
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
|
|
539
|
+
import * as fs from 'fs';
|
|
540
|
+
|
|
541
|
+
class FlakyReporter implements Reporter {
|
|
542
|
+
private flakyTests: Map<string, number> = new Map();
|
|
543
|
+
|
|
544
|
+
onTestEnd(test: TestCase, result: TestResult) {
|
|
545
|
+
if (result.status === 'passed' && result.retry > 0) {
|
|
546
|
+
// Test passed on retry = flaky
|
|
547
|
+
const key = `${test.location.file}:${test.title}`;
|
|
548
|
+
this.flakyTests.set(key, (this.flakyTests.get(key) || 0) + 1);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
onEnd() {
|
|
553
|
+
if (this.flakyTests.size > 0) {
|
|
554
|
+
const report = {
|
|
555
|
+
timestamp: new Date().toISOString(),
|
|
556
|
+
flakyTests: Object.fromEntries(this.flakyTests),
|
|
557
|
+
};
|
|
558
|
+
fs.writeFileSync('flaky-tests.json', JSON.stringify(report, null, 2));
|
|
559
|
+
console.log(`Found ${this.flakyTests.size} flaky tests`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export default FlakyReporter;
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Test Data Management
|
|
568
|
+
|
|
569
|
+
### Factory Pattern
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
// factories/user.factory.ts
|
|
573
|
+
import { faker } from '@faker-js/faker';
|
|
574
|
+
|
|
575
|
+
interface User {
|
|
576
|
+
email: string;
|
|
577
|
+
name: string;
|
|
578
|
+
role: 'admin' | 'user';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function createUser(overrides: Partial<User> = {}): User {
|
|
582
|
+
return {
|
|
583
|
+
email: faker.internet.email(),
|
|
584
|
+
name: faker.person.fullName(),
|
|
585
|
+
role: 'user',
|
|
586
|
+
...overrides,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function createUsers(count: number, overrides: Partial<User> = {}): User[] {
|
|
591
|
+
return Array.from({ length: count }, () => createUser(overrides));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Database seeding
|
|
595
|
+
// seed.ts
|
|
596
|
+
import { prisma } from './db';
|
|
597
|
+
import { createUser, createUsers } from './factories/user.factory';
|
|
598
|
+
|
|
599
|
+
async function seed() {
|
|
600
|
+
// Create admin
|
|
601
|
+
await prisma.user.create({
|
|
602
|
+
data: createUser({ email: 'admin@test.com', role: 'admin' }),
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Create test users
|
|
606
|
+
const users = createUsers(10);
|
|
607
|
+
await prisma.user.createMany({ data: users });
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Test Isolation
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// Reset database between tests
|
|
615
|
+
test.beforeEach(async () => {
|
|
616
|
+
await prisma.$executeRaw`TRUNCATE users, orders CASCADE`;
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Or use transactions that roll back
|
|
620
|
+
test.beforeEach(async () => {
|
|
621
|
+
await prisma.$executeRaw`BEGIN`;
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test.afterEach(async () => {
|
|
625
|
+
await prisma.$executeRaw`ROLLBACK`;
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## Performance Testing
|
|
630
|
+
|
|
631
|
+
### k6 Load Testing
|
|
632
|
+
|
|
633
|
+
```javascript
|
|
634
|
+
// load-test.js
|
|
635
|
+
import http from 'k6/http';
|
|
636
|
+
import { check, sleep } from 'k6';
|
|
637
|
+
|
|
638
|
+
export const options = {
|
|
639
|
+
stages: [
|
|
640
|
+
{ duration: '30s', target: 20 }, // Ramp up
|
|
641
|
+
{ duration: '1m', target: 20 }, // Stay at 20
|
|
642
|
+
{ duration: '30s', target: 50 }, // Ramp up more
|
|
643
|
+
{ duration: '1m', target: 50 }, // Stay at 50
|
|
644
|
+
{ duration: '30s', target: 0 }, // Ramp down
|
|
645
|
+
],
|
|
646
|
+
thresholds: {
|
|
647
|
+
http_req_duration: ['p(95)<500'], // 95% under 500ms
|
|
648
|
+
http_req_failed: ['rate<0.01'], // Error rate under 1%
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
export default function () {
|
|
653
|
+
const res = http.get('http://localhost:3000/api/users');
|
|
654
|
+
|
|
655
|
+
check(res, {
|
|
656
|
+
'status is 200': (r) => r.status === 200,
|
|
657
|
+
'response time OK': (r) => r.timings.duration < 500,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
sleep(1);
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
## Anti-Patterns to Avoid
|
|
665
|
+
|
|
666
|
+
| Anti-Pattern | Better Approach |
|
|
667
|
+
|--------------|-----------------|
|
|
668
|
+
| Hard-coded waits | Condition-based waits |
|
|
669
|
+
| Fragile CSS selectors | Test IDs or ARIA roles |
|
|
670
|
+
| Tests depending on order | Independent, isolated tests |
|
|
671
|
+
| Testing third-party services | Mock external dependencies |
|
|
672
|
+
| Ignoring flaky tests | Fix root cause or quarantine |
|
|
673
|
+
| No test data cleanup | Reset state between tests |
|
|
674
|
+
| Giant test files | Organized by feature |
|
|
675
|
+
|
|
676
|
+
## Test Automation Checklist
|
|
677
|
+
|
|
678
|
+
### Framework Setup
|
|
679
|
+
- [ ] Framework selected and configured
|
|
680
|
+
- [ ] Page Object Model structure
|
|
681
|
+
- [ ] Test fixtures for common scenarios
|
|
682
|
+
- [ ] Parallel execution enabled
|
|
683
|
+
- [ ] Retries configured for CI
|
|
684
|
+
|
|
685
|
+
### CI/CD Integration
|
|
686
|
+
- [ ] Tests run on every PR
|
|
687
|
+
- [ ] Test results reported
|
|
688
|
+
- [ ] Screenshots/videos on failure
|
|
689
|
+
- [ ] Flaky test tracking
|
|
690
|
+
- [ ] Performance benchmarks
|
|
691
|
+
|
|
692
|
+
### Maintenance
|
|
693
|
+
- [ ] Regular review of flaky tests
|
|
694
|
+
- [ ] Test coverage monitoring
|
|
695
|
+
- [ ] Documentation updated
|
|
696
|
+
- [ ] Quarterly cleanup of obsolete tests
|
|
697
|
+
|
|
698
|
+
## Constraints
|
|
699
|
+
|
|
700
|
+
- Tests must be independent and isolated
|
|
701
|
+
- No hard-coded waits (use conditions)
|
|
702
|
+
- Use semantic selectors (roles, test IDs)
|
|
703
|
+
- Keep tests maintainable over comprehensive
|
|
704
|
+
- Fix flaky tests immediately
|
|
705
|
+
|
|
706
|
+
## Related Skills
|
|
707
|
+
|
|
708
|
+
- `qa-expert` - Test strategy
|
|
709
|
+
- `devops-engineer` - CI/CD pipelines
|
|
710
|
+
- `frontend-developer` - UI testing
|
|
711
|
+
- `backend-developer` - API testing
|