omgkit 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,35 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
4
|
-
category: testing
|
|
5
|
-
triggers:
|
|
6
|
-
- playwright
|
|
7
|
-
- e2e testing
|
|
8
|
-
- browser automation
|
|
9
|
-
- end-to-end
|
|
10
|
-
- visual testing
|
|
11
|
-
- cross-browser testing
|
|
2
|
+
name: Testing with Playwright
|
|
3
|
+
description: Claude writes reliable E2E tests using Playwright for browser automation. Use when writing end-to-end tests, implementing Page Object Model, visual regression testing, API mocking, or cross-browser testing.
|
|
12
4
|
---
|
|
13
5
|
|
|
14
|
-
# Playwright
|
|
6
|
+
# Testing with Playwright
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## Purpose
|
|
19
|
-
|
|
20
|
-
Build reliable end-to-end test suites:
|
|
21
|
-
|
|
22
|
-
- Write resilient E2E tests with auto-waiting
|
|
23
|
-
- Implement Page Object Model patterns
|
|
24
|
-
- Configure cross-browser and mobile testing
|
|
25
|
-
- Set up visual regression testing
|
|
26
|
-
- Integrate with CI/CD pipelines
|
|
27
|
-
- Mock APIs and intercept network requests
|
|
28
|
-
- Generate test reports and traces
|
|
29
|
-
|
|
30
|
-
## Features
|
|
31
|
-
|
|
32
|
-
### 1. Configuration Setup
|
|
8
|
+
## Quick Start
|
|
33
9
|
|
|
34
10
|
```typescript
|
|
35
11
|
// playwright.config.ts
|
|
@@ -38,744 +14,146 @@ import { defineConfig, devices } from "@playwright/test";
|
|
|
38
14
|
export default defineConfig({
|
|
39
15
|
testDir: "./tests/e2e",
|
|
40
16
|
fullyParallel: true,
|
|
41
|
-
forbidOnly: !!process.env.CI,
|
|
42
17
|
retries: process.env.CI ? 2 : 0,
|
|
43
|
-
|
|
44
|
-
reporter: [
|
|
45
|
-
["list"],
|
|
46
|
-
["html", { outputFolder: "playwright-report" }],
|
|
47
|
-
["json", { outputFile: "test-results.json" }],
|
|
48
|
-
["junit", { outputFile: "junit.xml" }],
|
|
49
|
-
],
|
|
18
|
+
reporter: [["list"], ["html"]],
|
|
50
19
|
use: {
|
|
51
|
-
baseURL:
|
|
20
|
+
baseURL: "http://localhost:3000",
|
|
52
21
|
trace: "on-first-retry",
|
|
53
22
|
screenshot: "only-on-failure",
|
|
54
|
-
video: "retain-on-failure",
|
|
55
|
-
actionTimeout: 10000,
|
|
56
|
-
navigationTimeout: 30000,
|
|
57
23
|
},
|
|
58
24
|
projects: [
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: "firefox",
|
|
65
|
-
use: { ...devices["Desktop Firefox"] },
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: "webkit",
|
|
69
|
-
use: { ...devices["Desktop Safari"] },
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: "mobile-chrome",
|
|
73
|
-
use: { ...devices["Pixel 5"] },
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: "mobile-safari",
|
|
77
|
-
use: { ...devices["iPhone 12"] },
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: "tablet",
|
|
81
|
-
use: { ...devices["iPad Pro 11"] },
|
|
82
|
-
},
|
|
25
|
+
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
|
26
|
+
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
|
27
|
+
{ name: "mobile", use: { ...devices["iPhone 12"] } },
|
|
83
28
|
],
|
|
84
|
-
webServer: {
|
|
85
|
-
command: "npm run dev",
|
|
86
|
-
url: "http://localhost:3000",
|
|
87
|
-
reuseExistingServer: !process.env.CI,
|
|
88
|
-
timeout: 120000,
|
|
89
|
-
},
|
|
29
|
+
webServer: { command: "npm run dev", url: "http://localhost:3000" },
|
|
90
30
|
});
|
|
91
31
|
```
|
|
92
32
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
// tests/pages/base.page.ts
|
|
97
|
-
import { Page, Locator, expect } from "@playwright/test";
|
|
98
|
-
|
|
99
|
-
export abstract class BasePage {
|
|
100
|
-
constructor(protected page: Page) {}
|
|
101
|
-
|
|
102
|
-
abstract get url(): string;
|
|
103
|
-
|
|
104
|
-
async navigate(): Promise<void> {
|
|
105
|
-
await this.page.goto(this.url);
|
|
106
|
-
await this.waitForPageLoad();
|
|
107
|
-
}
|
|
33
|
+
## Features
|
|
108
34
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
35
|
+
| Feature | Description | Reference |
|
|
36
|
+
|---------|-------------|-----------|
|
|
37
|
+
| Page Object Model | Maintainable test architecture pattern | [POM Guide](https://playwright.dev/docs/pom) |
|
|
38
|
+
| Auto-Waiting | Built-in waiting for elements and assertions | [Auto-Waiting](https://playwright.dev/docs/actionability) |
|
|
39
|
+
| Network Mocking | Intercept and mock API responses | [Network](https://playwright.dev/docs/network) |
|
|
40
|
+
| Visual Testing | Screenshot comparison for regression testing | [Visual Comparisons](https://playwright.dev/docs/test-snapshots) |
|
|
41
|
+
| Cross-Browser | Chrome, Firefox, Safari, mobile devices | [Browsers](https://playwright.dev/docs/browsers) |
|
|
42
|
+
| Trace Viewer | Debug failing tests with timeline | [Trace Viewer](https://playwright.dev/docs/trace-viewer) |
|
|
112
43
|
|
|
113
|
-
|
|
114
|
-
return this.page.title();
|
|
115
|
-
}
|
|
44
|
+
## Common Patterns
|
|
116
45
|
|
|
117
|
-
|
|
118
|
-
await this.page.screenshot({ path: `screenshots/${name}.png` });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
46
|
+
### Page Object Model
|
|
121
47
|
|
|
48
|
+
```typescript
|
|
122
49
|
// tests/pages/login.page.ts
|
|
123
50
|
import { Page, Locator, expect } from "@playwright/test";
|
|
124
|
-
import { BasePage } from "./base.page";
|
|
125
51
|
|
|
126
|
-
export class LoginPage
|
|
52
|
+
export class LoginPage {
|
|
127
53
|
readonly emailInput: Locator;
|
|
128
54
|
readonly passwordInput: Locator;
|
|
129
55
|
readonly submitButton: Locator;
|
|
130
|
-
readonly errorMessage: Locator;
|
|
131
|
-
readonly forgotPasswordLink: Locator;
|
|
132
|
-
readonly rememberMeCheckbox: Locator;
|
|
133
56
|
|
|
134
|
-
constructor(page: Page) {
|
|
135
|
-
super(page);
|
|
57
|
+
constructor(private page: Page) {
|
|
136
58
|
this.emailInput = page.getByLabel("Email");
|
|
137
59
|
this.passwordInput = page.getByLabel("Password");
|
|
138
60
|
this.submitButton = page.getByRole("button", { name: "Sign in" });
|
|
139
|
-
this.errorMessage = page.getByRole("alert");
|
|
140
|
-
this.forgotPasswordLink = page.getByRole("link", { name: "Forgot password?" });
|
|
141
|
-
this.rememberMeCheckbox = page.getByLabel("Remember me");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
get url(): string {
|
|
145
|
-
return "/login";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async login(email: string, password: string): Promise<void> {
|
|
149
|
-
await this.emailInput.fill(email);
|
|
150
|
-
await this.passwordInput.fill(password);
|
|
151
|
-
await this.submitButton.click();
|
|
152
61
|
}
|
|
153
62
|
|
|
154
|
-
async
|
|
63
|
+
async login(email: string, password: string) {
|
|
155
64
|
await this.emailInput.fill(email);
|
|
156
65
|
await this.passwordInput.fill(password);
|
|
157
|
-
await this.rememberMeCheckbox.check();
|
|
158
66
|
await this.submitButton.click();
|
|
159
67
|
}
|
|
160
68
|
|
|
161
|
-
async
|
|
162
|
-
await expect(this.
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async expectLoginFormVisible(): Promise<void> {
|
|
166
|
-
await expect(this.emailInput).toBeVisible();
|
|
167
|
-
await expect(this.passwordInput).toBeVisible();
|
|
168
|
-
await expect(this.submitButton).toBeVisible();
|
|
69
|
+
async expectError(message: string) {
|
|
70
|
+
await expect(this.page.getByRole("alert")).toContainText(message);
|
|
169
71
|
}
|
|
170
72
|
}
|
|
73
|
+
```
|
|
171
74
|
|
|
172
|
-
|
|
173
|
-
import { Page, Locator, expect } from "@playwright/test";
|
|
174
|
-
import { BasePage } from "./base.page";
|
|
175
|
-
|
|
176
|
-
export class DashboardPage extends BasePage {
|
|
177
|
-
readonly welcomeMessage: Locator;
|
|
178
|
-
readonly userMenu: Locator;
|
|
179
|
-
readonly logoutButton: Locator;
|
|
180
|
-
readonly navigationItems: Locator;
|
|
181
|
-
readonly searchInput: Locator;
|
|
182
|
-
|
|
183
|
-
constructor(page: Page) {
|
|
184
|
-
super(page);
|
|
185
|
-
this.welcomeMessage = page.getByTestId("welcome-message");
|
|
186
|
-
this.userMenu = page.getByTestId("user-menu");
|
|
187
|
-
this.logoutButton = page.getByRole("button", { name: "Logout" });
|
|
188
|
-
this.navigationItems = page.getByRole("navigation").getByRole("link");
|
|
189
|
-
this.searchInput = page.getByPlaceholder("Search...");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
get url(): string {
|
|
193
|
-
return "/dashboard";
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async expectWelcomeMessage(name: string): Promise<void> {
|
|
197
|
-
await expect(this.welcomeMessage).toContainText(`Welcome, ${name}`);
|
|
198
|
-
}
|
|
75
|
+
### API Mocking
|
|
199
76
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
await this.logoutButton.click();
|
|
203
|
-
}
|
|
77
|
+
```typescript
|
|
78
|
+
import { test, expect } from "@playwright/test";
|
|
204
79
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
80
|
+
test("mock API response", async ({ page }) => {
|
|
81
|
+
await page.route("**/api/users", (route) =>
|
|
82
|
+
route.fulfill({
|
|
83
|
+
status: 200,
|
|
84
|
+
contentType: "application/json",
|
|
85
|
+
body: JSON.stringify({ users: [{ id: 1, name: "John" }] }),
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await page.goto("/users");
|
|
90
|
+
await expect(page.getByText("John")).toBeVisible();
|
|
91
|
+
});
|
|
209
92
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
93
|
+
test("capture network requests", async ({ page }) => {
|
|
94
|
+
const requestPromise = page.waitForRequest("**/api/analytics");
|
|
95
|
+
await page.goto("/dashboard");
|
|
96
|
+
const request = await requestPromise;
|
|
97
|
+
expect(request.postDataJSON()).toMatchObject({ event: "page_view" });
|
|
98
|
+
});
|
|
214
99
|
```
|
|
215
100
|
|
|
216
|
-
###
|
|
101
|
+
### Authentication Fixture
|
|
217
102
|
|
|
218
103
|
```typescript
|
|
219
104
|
// tests/fixtures/auth.fixture.ts
|
|
220
|
-
import { test as base
|
|
105
|
+
import { test as base } from "@playwright/test";
|
|
221
106
|
import { LoginPage } from "../pages/login.page";
|
|
222
|
-
import { DashboardPage } from "../pages/dashboard.page";
|
|
223
|
-
|
|
224
|
-
interface TestUser {
|
|
225
|
-
email: string;
|
|
226
|
-
password: string;
|
|
227
|
-
name: string;
|
|
228
|
-
}
|
|
229
107
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
testUser: TestUser;
|
|
234
|
-
authenticatedPage: DashboardPage;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export const test = base.extend<AuthFixtures>({
|
|
238
|
-
testUser: {
|
|
239
|
-
email: "test@example.com",
|
|
240
|
-
password: "SecurePassword123!",
|
|
241
|
-
name: "Test User",
|
|
242
|
-
},
|
|
243
|
-
|
|
244
|
-
loginPage: async ({ page }, use) => {
|
|
245
|
-
const loginPage = new LoginPage(page);
|
|
246
|
-
await use(loginPage);
|
|
247
|
-
},
|
|
248
|
-
|
|
249
|
-
dashboardPage: async ({ page }, use) => {
|
|
250
|
-
const dashboardPage = new DashboardPage(page);
|
|
251
|
-
await use(dashboardPage);
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
authenticatedPage: async ({ page, testUser }, use) => {
|
|
255
|
-
// Login via API for faster test setup
|
|
108
|
+
export const test = base.extend<{ authenticatedPage: Page }>({
|
|
109
|
+
authenticatedPage: async ({ page }, use) => {
|
|
110
|
+
// Fast auth via API
|
|
256
111
|
const response = await page.request.post("/api/auth/login", {
|
|
257
|
-
data: {
|
|
258
|
-
email: testUser.email,
|
|
259
|
-
password: testUser.password,
|
|
260
|
-
},
|
|
112
|
+
data: { email: "test@example.com", password: "password" },
|
|
261
113
|
});
|
|
262
|
-
|
|
263
114
|
const { token } = await response.json();
|
|
264
115
|
|
|
265
|
-
// Set auth cookie/storage
|
|
266
116
|
await page.context().addCookies([
|
|
267
|
-
{
|
|
268
|
-
name: "auth_token",
|
|
269
|
-
value: token,
|
|
270
|
-
domain: "localhost",
|
|
271
|
-
path: "/",
|
|
272
|
-
},
|
|
117
|
+
{ name: "auth_token", value: token, domain: "localhost", path: "/" },
|
|
273
118
|
]);
|
|
274
119
|
|
|
275
|
-
const dashboardPage = new DashboardPage(page);
|
|
276
|
-
await dashboardPage.navigate();
|
|
277
|
-
await use(dashboardPage);
|
|
278
|
-
},
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
export { expect };
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### 4. API Mocking and Network Interception
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
// tests/e2e/api-mocking.spec.ts
|
|
288
|
-
import { test, expect } from "@playwright/test";
|
|
289
|
-
|
|
290
|
-
test.describe("API Mocking", () => {
|
|
291
|
-
test("mock successful API response", async ({ page }) => {
|
|
292
|
-
await page.route("**/api/users", (route) =>
|
|
293
|
-
route.fulfill({
|
|
294
|
-
status: 200,
|
|
295
|
-
contentType: "application/json",
|
|
296
|
-
body: JSON.stringify({
|
|
297
|
-
users: [
|
|
298
|
-
{ id: 1, name: "John Doe", email: "john@example.com" },
|
|
299
|
-
{ id: 2, name: "Jane Smith", email: "jane@example.com" },
|
|
300
|
-
],
|
|
301
|
-
}),
|
|
302
|
-
})
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
await page.goto("/users");
|
|
306
|
-
|
|
307
|
-
await expect(page.getByText("John Doe")).toBeVisible();
|
|
308
|
-
await expect(page.getByText("Jane Smith")).toBeVisible();
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("mock API error response", async ({ page }) => {
|
|
312
|
-
await page.route("**/api/users", (route) =>
|
|
313
|
-
route.fulfill({
|
|
314
|
-
status: 500,
|
|
315
|
-
contentType: "application/json",
|
|
316
|
-
body: JSON.stringify({ error: "Internal server error" }),
|
|
317
|
-
})
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
await page.goto("/users");
|
|
321
|
-
|
|
322
|
-
await expect(page.getByText("Failed to load users")).toBeVisible();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
test("intercept and modify requests", async ({ page }) => {
|
|
326
|
-
await page.route("**/api/search**", (route) => {
|
|
327
|
-
const url = new URL(route.request().url());
|
|
328
|
-
url.searchParams.set("limit", "10");
|
|
329
|
-
|
|
330
|
-
route.continue({ url: url.toString() });
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
await page.goto("/search");
|
|
334
|
-
await page.fill('[name="query"]', "test");
|
|
335
|
-
await page.click('button[type="submit"]');
|
|
336
|
-
|
|
337
|
-
// Verify modified request was made
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("delay API response", async ({ page }) => {
|
|
341
|
-
await page.route("**/api/slow-endpoint", async (route) => {
|
|
342
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
343
|
-
await route.fulfill({
|
|
344
|
-
status: 200,
|
|
345
|
-
body: JSON.stringify({ data: "delayed response" }),
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
await page.goto("/slow-page");
|
|
350
|
-
|
|
351
|
-
// Verify loading state appears
|
|
352
|
-
await expect(page.getByTestId("loading-spinner")).toBeVisible();
|
|
353
|
-
|
|
354
|
-
// Verify content appears after delay
|
|
355
|
-
await expect(page.getByText("delayed response")).toBeVisible({
|
|
356
|
-
timeout: 5000,
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test("capture and assert network requests", async ({ page }) => {
|
|
361
|
-
const requestPromise = page.waitForRequest("**/api/analytics");
|
|
362
|
-
|
|
363
120
|
await page.goto("/dashboard");
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const postData = request.postDataJSON();
|
|
367
|
-
|
|
368
|
-
expect(postData).toMatchObject({
|
|
369
|
-
event: "page_view",
|
|
370
|
-
page: "/dashboard",
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
test("wait for specific response", async ({ page }) => {
|
|
375
|
-
const responsePromise = page.waitForResponse(
|
|
376
|
-
(response) =>
|
|
377
|
-
response.url().includes("/api/data") && response.status() === 200
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
await page.goto("/data-page");
|
|
381
|
-
|
|
382
|
-
const response = await responsePromise;
|
|
383
|
-
const data = await response.json();
|
|
384
|
-
|
|
385
|
-
expect(data).toHaveProperty("items");
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### 5. Visual Regression Testing
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
// tests/e2e/visual.spec.ts
|
|
394
|
-
import { test, expect } from "@playwright/test";
|
|
395
|
-
|
|
396
|
-
test.describe("Visual Regression Tests", () => {
|
|
397
|
-
test.beforeEach(async ({ page }) => {
|
|
398
|
-
// Disable animations for consistent screenshots
|
|
399
|
-
await page.addStyleTag({
|
|
400
|
-
content: `
|
|
401
|
-
*, *::before, *::after {
|
|
402
|
-
animation-duration: 0s !important;
|
|
403
|
-
transition-duration: 0s !important;
|
|
404
|
-
}
|
|
405
|
-
`,
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
test("homepage visual snapshot", async ({ page }) => {
|
|
410
|
-
await page.goto("/");
|
|
411
|
-
await page.waitForLoadState("networkidle");
|
|
412
|
-
|
|
413
|
-
await expect(page).toHaveScreenshot("homepage.png", {
|
|
414
|
-
fullPage: true,
|
|
415
|
-
maxDiffPixels: 100,
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
test("component visual snapshot", async ({ page }) => {
|
|
420
|
-
await page.goto("/components/button");
|
|
421
|
-
|
|
422
|
-
const button = page.getByTestId("primary-button");
|
|
423
|
-
await expect(button).toHaveScreenshot("primary-button.png");
|
|
424
|
-
|
|
425
|
-
// Hover state
|
|
426
|
-
await button.hover();
|
|
427
|
-
await expect(button).toHaveScreenshot("primary-button-hover.png");
|
|
428
|
-
|
|
429
|
-
// Focus state
|
|
430
|
-
await button.focus();
|
|
431
|
-
await expect(button).toHaveScreenshot("primary-button-focus.png");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
test("responsive visual snapshots", async ({ page }) => {
|
|
435
|
-
await page.goto("/");
|
|
436
|
-
|
|
437
|
-
// Desktop
|
|
438
|
-
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
439
|
-
await expect(page).toHaveScreenshot("homepage-desktop.png");
|
|
440
|
-
|
|
441
|
-
// Tablet
|
|
442
|
-
await page.setViewportSize({ width: 768, height: 1024 });
|
|
443
|
-
await expect(page).toHaveScreenshot("homepage-tablet.png");
|
|
444
|
-
|
|
445
|
-
// Mobile
|
|
446
|
-
await page.setViewportSize({ width: 375, height: 667 });
|
|
447
|
-
await expect(page).toHaveScreenshot("homepage-mobile.png");
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test("dark mode visual snapshot", async ({ page }) => {
|
|
451
|
-
await page.goto("/");
|
|
452
|
-
|
|
453
|
-
// Toggle dark mode
|
|
454
|
-
await page.click('[data-testid="theme-toggle"]');
|
|
455
|
-
await page.waitForSelector('[data-theme="dark"]');
|
|
456
|
-
|
|
457
|
-
await expect(page).toHaveScreenshot("homepage-dark-mode.png", {
|
|
458
|
-
fullPage: true,
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
test("mask dynamic content", async ({ page }) => {
|
|
463
|
-
await page.goto("/dashboard");
|
|
464
|
-
|
|
465
|
-
await expect(page).toHaveScreenshot("dashboard.png", {
|
|
466
|
-
mask: [
|
|
467
|
-
page.getByTestId("timestamp"),
|
|
468
|
-
page.getByTestId("user-avatar"),
|
|
469
|
-
page.getByTestId("random-content"),
|
|
470
|
-
],
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
### 6. Authentication and Session Management
|
|
477
|
-
|
|
478
|
-
```typescript
|
|
479
|
-
// tests/e2e/auth.spec.ts
|
|
480
|
-
import { test, expect } from "../fixtures/auth.fixture";
|
|
481
|
-
|
|
482
|
-
test.describe("Authentication Flow", () => {
|
|
483
|
-
test("successful login redirects to dashboard", async ({ page, loginPage, testUser }) => {
|
|
484
|
-
await loginPage.navigate();
|
|
485
|
-
await loginPage.login(testUser.email, testUser.password);
|
|
486
|
-
|
|
487
|
-
await expect(page).toHaveURL("/dashboard");
|
|
488
|
-
await expect(page.getByTestId("welcome-message")).toContainText(
|
|
489
|
-
testUser.name
|
|
490
|
-
);
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
test("invalid credentials show error message", async ({ loginPage }) => {
|
|
494
|
-
await loginPage.navigate();
|
|
495
|
-
await loginPage.login("wrong@example.com", "wrongpassword");
|
|
496
|
-
|
|
497
|
-
await loginPage.expectErrorMessage("Invalid email or password");
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
test("logout clears session", async ({ page, authenticatedPage }) => {
|
|
501
|
-
await authenticatedPage.logout();
|
|
502
|
-
|
|
503
|
-
await expect(page).toHaveURL("/login");
|
|
504
|
-
|
|
505
|
-
// Verify session is cleared by trying to access protected route
|
|
506
|
-
await page.goto("/dashboard");
|
|
507
|
-
await expect(page).toHaveURL("/login");
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test("remember me persists session", async ({ page, loginPage, testUser, context }) => {
|
|
511
|
-
await loginPage.navigate();
|
|
512
|
-
await loginPage.loginWithRememberMe(testUser.email, testUser.password);
|
|
513
|
-
|
|
514
|
-
// Create new page in same context (simulates new tab)
|
|
515
|
-
const newPage = await context.newPage();
|
|
516
|
-
await newPage.goto("/dashboard");
|
|
517
|
-
|
|
518
|
-
await expect(newPage).toHaveURL("/dashboard");
|
|
519
|
-
await newPage.close();
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
test("session expires after timeout", async ({ page, authenticatedPage }) => {
|
|
523
|
-
// Mock session expiration
|
|
524
|
-
await page.route("**/api/session/check", (route) =>
|
|
525
|
-
route.fulfill({
|
|
526
|
-
status: 401,
|
|
527
|
-
body: JSON.stringify({ error: "Session expired" }),
|
|
528
|
-
})
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
// Trigger session check
|
|
532
|
-
await page.click('[data-testid="refresh-data"]');
|
|
533
|
-
|
|
534
|
-
await expect(page.getByText("Session expired")).toBeVisible();
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
// tests/auth.setup.ts - Global auth setup
|
|
539
|
-
import { test as setup, expect } from "@playwright/test";
|
|
540
|
-
import path from "path";
|
|
541
|
-
|
|
542
|
-
const authFile = path.join(__dirname, "../.auth/user.json");
|
|
543
|
-
|
|
544
|
-
setup("authenticate", async ({ page }) => {
|
|
545
|
-
await page.goto("/login");
|
|
546
|
-
await page.fill('[name="email"]', process.env.TEST_USER_EMAIL!);
|
|
547
|
-
await page.fill('[name="password"]', process.env.TEST_USER_PASSWORD!);
|
|
548
|
-
await page.click('button[type="submit"]');
|
|
549
|
-
|
|
550
|
-
await expect(page).toHaveURL("/dashboard");
|
|
551
|
-
|
|
552
|
-
await page.context().storageState({ path: authFile });
|
|
553
|
-
});
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
### 7. Form and User Interaction Testing
|
|
557
|
-
|
|
558
|
-
```typescript
|
|
559
|
-
// tests/e2e/forms.spec.ts
|
|
560
|
-
import { test, expect } from "@playwright/test";
|
|
561
|
-
|
|
562
|
-
test.describe("Form Interactions", () => {
|
|
563
|
-
test("complete multi-step form", async ({ page }) => {
|
|
564
|
-
await page.goto("/onboarding");
|
|
565
|
-
|
|
566
|
-
// Step 1: Personal Info
|
|
567
|
-
await page.fill('[name="firstName"]', "John");
|
|
568
|
-
await page.fill('[name="lastName"]', "Doe");
|
|
569
|
-
await page.fill('[name="email"]', "john.doe@example.com");
|
|
570
|
-
await page.click('button:has-text("Next")');
|
|
571
|
-
|
|
572
|
-
// Step 2: Preferences
|
|
573
|
-
await page.check('[name="newsletter"]');
|
|
574
|
-
await page.selectOption('[name="timezone"]', "America/New_York");
|
|
575
|
-
await page.click('button:has-text("Next")');
|
|
576
|
-
|
|
577
|
-
// Step 3: Confirmation
|
|
578
|
-
await expect(page.getByText("John Doe")).toBeVisible();
|
|
579
|
-
await expect(page.getByText("john.doe@example.com")).toBeVisible();
|
|
580
|
-
await page.click('button:has-text("Complete")');
|
|
581
|
-
|
|
582
|
-
await expect(page).toHaveURL("/welcome");
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
test("form validation errors", async ({ page }) => {
|
|
586
|
-
await page.goto("/register");
|
|
587
|
-
|
|
588
|
-
// Submit empty form
|
|
589
|
-
await page.click('button[type="submit"]');
|
|
590
|
-
|
|
591
|
-
// Check validation messages
|
|
592
|
-
await expect(page.getByText("Email is required")).toBeVisible();
|
|
593
|
-
await expect(page.getByText("Password is required")).toBeVisible();
|
|
594
|
-
|
|
595
|
-
// Fill invalid email
|
|
596
|
-
await page.fill('[name="email"]', "invalid-email");
|
|
597
|
-
await page.click('button[type="submit"]');
|
|
598
|
-
await expect(page.getByText("Invalid email format")).toBeVisible();
|
|
599
|
-
|
|
600
|
-
// Fill weak password
|
|
601
|
-
await page.fill('[name="email"]', "valid@example.com");
|
|
602
|
-
await page.fill('[name="password"]', "123");
|
|
603
|
-
await page.click('button[type="submit"]');
|
|
604
|
-
await expect(page.getByText("Password must be at least 8 characters")).toBeVisible();
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test("file upload", async ({ page }) => {
|
|
608
|
-
await page.goto("/upload");
|
|
609
|
-
|
|
610
|
-
const fileInput = page.locator('input[type="file"]');
|
|
611
|
-
await fileInput.setInputFiles({
|
|
612
|
-
name: "test-file.pdf",
|
|
613
|
-
mimeType: "application/pdf",
|
|
614
|
-
buffer: Buffer.from("PDF content"),
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
await expect(page.getByText("test-file.pdf")).toBeVisible();
|
|
618
|
-
await page.click('button:has-text("Upload")');
|
|
619
|
-
|
|
620
|
-
await expect(page.getByText("Upload successful")).toBeVisible();
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
test("drag and drop", async ({ page }) => {
|
|
624
|
-
await page.goto("/kanban");
|
|
625
|
-
|
|
626
|
-
const sourceCard = page.locator('[data-testid="card-1"]');
|
|
627
|
-
const targetColumn = page.locator('[data-testid="done-column"]');
|
|
628
|
-
|
|
629
|
-
await sourceCard.dragTo(targetColumn);
|
|
630
|
-
|
|
631
|
-
await expect(targetColumn.locator('[data-testid="card-1"]')).toBeVisible();
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
test("autocomplete interaction", async ({ page }) => {
|
|
635
|
-
await page.goto("/search");
|
|
636
|
-
|
|
637
|
-
await page.fill('[name="search"]', "pla");
|
|
638
|
-
await expect(page.getByRole("listbox")).toBeVisible();
|
|
639
|
-
|
|
640
|
-
const suggestions = page.getByRole("option");
|
|
641
|
-
await expect(suggestions).toHaveCount(5);
|
|
642
|
-
|
|
643
|
-
await suggestions.filter({ hasText: "Playwright" }).click();
|
|
644
|
-
await expect(page.locator('[name="search"]')).toHaveValue("Playwright");
|
|
645
|
-
});
|
|
121
|
+
await use(page);
|
|
122
|
+
},
|
|
646
123
|
});
|
|
647
124
|
```
|
|
648
125
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
### CI/CD Integration
|
|
652
|
-
|
|
653
|
-
```yaml
|
|
654
|
-
# .github/workflows/e2e.yml
|
|
655
|
-
name: E2E Tests
|
|
656
|
-
|
|
657
|
-
on:
|
|
658
|
-
push:
|
|
659
|
-
branches: [main]
|
|
660
|
-
pull_request:
|
|
661
|
-
branches: [main]
|
|
662
|
-
|
|
663
|
-
jobs:
|
|
664
|
-
e2e:
|
|
665
|
-
runs-on: ubuntu-latest
|
|
666
|
-
steps:
|
|
667
|
-
- uses: actions/checkout@v4
|
|
668
|
-
|
|
669
|
-
- name: Setup Node.js
|
|
670
|
-
uses: actions/setup-node@v4
|
|
671
|
-
with:
|
|
672
|
-
node-version: "20"
|
|
673
|
-
cache: "npm"
|
|
674
|
-
|
|
675
|
-
- name: Install dependencies
|
|
676
|
-
run: npm ci
|
|
677
|
-
|
|
678
|
-
- name: Install Playwright browsers
|
|
679
|
-
run: npx playwright install --with-deps
|
|
680
|
-
|
|
681
|
-
- name: Build application
|
|
682
|
-
run: npm run build
|
|
683
|
-
|
|
684
|
-
- name: Run E2E tests
|
|
685
|
-
run: npx playwright test
|
|
686
|
-
env:
|
|
687
|
-
BASE_URL: http://localhost:3000
|
|
688
|
-
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
|
|
689
|
-
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
|
|
690
|
-
|
|
691
|
-
- name: Upload test results
|
|
692
|
-
uses: actions/upload-artifact@v4
|
|
693
|
-
if: always()
|
|
694
|
-
with:
|
|
695
|
-
name: playwright-report
|
|
696
|
-
path: playwright-report/
|
|
697
|
-
retention-days: 30
|
|
698
|
-
|
|
699
|
-
- name: Upload test traces
|
|
700
|
-
uses: actions/upload-artifact@v4
|
|
701
|
-
if: failure()
|
|
702
|
-
with:
|
|
703
|
-
name: test-traces
|
|
704
|
-
path: test-results/
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
### Accessibility Testing
|
|
126
|
+
### Visual Regression Testing
|
|
708
127
|
|
|
709
128
|
```typescript
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
test.describe("Accessibility", () => {
|
|
715
|
-
test("homepage has no accessibility violations", async ({ page }) => {
|
|
716
|
-
await page.goto("/");
|
|
717
|
-
|
|
718
|
-
const results = await new AxeBuilder({ page })
|
|
719
|
-
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
|
720
|
-
.analyze();
|
|
721
|
-
|
|
722
|
-
expect(results.violations).toEqual([]);
|
|
129
|
+
test("visual snapshot", async ({ page }) => {
|
|
130
|
+
await page.goto("/");
|
|
131
|
+
await page.addStyleTag({
|
|
132
|
+
content: "*, *::before, *::after { animation-duration: 0s !important; }",
|
|
723
133
|
});
|
|
724
134
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const results = await new AxeBuilder({ page })
|
|
729
|
-
.include("form")
|
|
730
|
-
.analyze();
|
|
731
|
-
|
|
732
|
-
expect(results.violations).toEqual([]);
|
|
135
|
+
await expect(page).toHaveScreenshot("homepage.png", {
|
|
136
|
+
fullPage: true,
|
|
137
|
+
maxDiffPixels: 100,
|
|
733
138
|
});
|
|
734
139
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
// Tab through interactive elements
|
|
739
|
-
await page.keyboard.press("Tab");
|
|
740
|
-
await expect(page.locator(":focus")).toHaveAttribute("role", "link");
|
|
741
|
-
|
|
742
|
-
await page.keyboard.press("Tab");
|
|
743
|
-
await expect(page.locator(":focus")).toHaveAttribute("role", "button");
|
|
744
|
-
|
|
745
|
-
// Activate with Enter
|
|
746
|
-
await page.keyboard.press("Enter");
|
|
747
|
-
await expect(page).toHaveURL(/\/about/);
|
|
140
|
+
// Mask dynamic content
|
|
141
|
+
await expect(page).toHaveScreenshot("dashboard.png", {
|
|
142
|
+
mask: [page.getByTestId("timestamp"), page.getByTestId("avatar")],
|
|
748
143
|
});
|
|
749
144
|
});
|
|
750
145
|
```
|
|
751
146
|
|
|
752
147
|
## Best Practices
|
|
753
148
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
- Implement visual regression for UI consistency
|
|
763
|
-
- Use test.describe for logical grouping
|
|
764
|
-
- Mock external APIs for reliable tests
|
|
765
|
-
- Test across multiple browsers and devices
|
|
766
|
-
|
|
767
|
-
### Don'ts
|
|
768
|
-
|
|
769
|
-
- Don't use fragile CSS selectors
|
|
770
|
-
- Don't rely on timing with arbitrary waits
|
|
771
|
-
- Don't share state between tests
|
|
772
|
-
- Don't test third-party services directly
|
|
773
|
-
- Don't skip flaky tests without investigating
|
|
774
|
-
- Don't ignore accessibility testing
|
|
775
|
-
- Don't hardcode test data
|
|
776
|
-
- Don't run all browsers in CI by default
|
|
777
|
-
- Don't ignore test failures in PRs
|
|
778
|
-
- Don't forget to clean up test artifacts
|
|
149
|
+
| Do | Avoid |
|
|
150
|
+
|----|-------|
|
|
151
|
+
| Use Page Object Model for maintainability | Fragile CSS selectors |
|
|
152
|
+
| Prefer user-facing locators (getByRole, getByLabel) | Relying on arbitrary waits |
|
|
153
|
+
| Use API auth for faster test setup | Sharing state between tests |
|
|
154
|
+
| Enable traces and screenshots for debugging | Testing third-party services directly |
|
|
155
|
+
| Run tests in parallel for speed | Skipping flaky tests without fixing |
|
|
156
|
+
| Mock external APIs for reliability | Hardcoding test data |
|
|
779
157
|
|
|
780
158
|
## References
|
|
781
159
|
|
|
@@ -783,4 +161,3 @@ test.describe("Accessibility", () => {
|
|
|
783
161
|
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
|
|
784
162
|
- [Page Object Model](https://playwright.dev/docs/pom)
|
|
785
163
|
- [Visual Comparisons](https://playwright.dev/docs/test-snapshots)
|
|
786
|
-
- [Accessibility Testing](https://playwright.dev/docs/accessibility-testing)
|