omgkit 2.1.0 → 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.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,60 +1,786 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: playwright
|
|
3
|
-
description: Playwright E2E testing
|
|
3
|
+
description: Playwright E2E testing with browser automation, visual regression, API testing, and CI integration
|
|
4
|
+
category: testing
|
|
5
|
+
triggers:
|
|
6
|
+
- playwright
|
|
7
|
+
- e2e testing
|
|
8
|
+
- browser automation
|
|
9
|
+
- end-to-end
|
|
10
|
+
- visual testing
|
|
11
|
+
- cross-browser testing
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
# Playwright
|
|
14
|
+
# Playwright
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
import { test, expect } from '@playwright/test';
|
|
16
|
+
Enterprise-grade **E2E testing framework** following industry best practices. This skill covers browser automation, visual regression testing, API testing, network interception, and CI/CD integration patterns used by top engineering teams.
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
await page.goto('/');
|
|
14
|
-
await expect(page).toHaveTitle(/My App/);
|
|
15
|
-
});
|
|
18
|
+
## Purpose
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// playwright.config.ts
|
|
36
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
testDir: "./tests/e2e",
|
|
40
|
+
fullyParallel: true,
|
|
41
|
+
forbidOnly: !!process.env.CI,
|
|
42
|
+
retries: process.env.CI ? 2 : 0,
|
|
43
|
+
workers: process.env.CI ? 1 : undefined,
|
|
44
|
+
reporter: [
|
|
45
|
+
["list"],
|
|
46
|
+
["html", { outputFolder: "playwright-report" }],
|
|
47
|
+
["json", { outputFile: "test-results.json" }],
|
|
48
|
+
["junit", { outputFile: "junit.xml" }],
|
|
49
|
+
],
|
|
50
|
+
use: {
|
|
51
|
+
baseURL: process.env.BASE_URL || "http://localhost:3000",
|
|
52
|
+
trace: "on-first-retry",
|
|
53
|
+
screenshot: "only-on-failure",
|
|
54
|
+
video: "retain-on-failure",
|
|
55
|
+
actionTimeout: 10000,
|
|
56
|
+
navigationTimeout: 30000,
|
|
57
|
+
},
|
|
58
|
+
projects: [
|
|
59
|
+
{
|
|
60
|
+
name: "chromium",
|
|
61
|
+
use: { ...devices["Desktop Chrome"] },
|
|
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
|
+
},
|
|
83
|
+
],
|
|
84
|
+
webServer: {
|
|
85
|
+
command: "npm run dev",
|
|
86
|
+
url: "http://localhost:3000",
|
|
87
|
+
reuseExistingServer: !process.env.CI,
|
|
88
|
+
timeout: 120000,
|
|
89
|
+
},
|
|
23
90
|
});
|
|
24
91
|
```
|
|
25
92
|
|
|
26
|
-
|
|
93
|
+
### 2. Page Object Model
|
|
94
|
+
|
|
27
95
|
```typescript
|
|
28
|
-
|
|
29
|
-
|
|
96
|
+
// tests/pages/base.page.ts
|
|
97
|
+
import { Page, Locator, expect } from "@playwright/test";
|
|
30
98
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
108
|
+
|
|
109
|
+
async waitForPageLoad(): Promise<void> {
|
|
110
|
+
await this.page.waitForLoadState("networkidle");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getTitle(): Promise<string> {
|
|
114
|
+
return this.page.title();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async screenshot(name: string): Promise<void> {
|
|
118
|
+
await this.page.screenshot({ path: `screenshots/${name}.png` });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// tests/pages/login.page.ts
|
|
123
|
+
import { Page, Locator, expect } from "@playwright/test";
|
|
124
|
+
import { BasePage } from "./base.page";
|
|
125
|
+
|
|
126
|
+
export class LoginPage extends BasePage {
|
|
127
|
+
readonly emailInput: Locator;
|
|
128
|
+
readonly passwordInput: Locator;
|
|
129
|
+
readonly submitButton: Locator;
|
|
130
|
+
readonly errorMessage: Locator;
|
|
131
|
+
readonly forgotPasswordLink: Locator;
|
|
132
|
+
readonly rememberMeCheckbox: Locator;
|
|
133
|
+
|
|
134
|
+
constructor(page: Page) {
|
|
135
|
+
super(page);
|
|
136
|
+
this.emailInput = page.getByLabel("Email");
|
|
137
|
+
this.passwordInput = page.getByLabel("Password");
|
|
138
|
+
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
|
+
}
|
|
153
|
+
|
|
154
|
+
async loginWithRememberMe(email: string, password: string): Promise<void> {
|
|
155
|
+
await this.emailInput.fill(email);
|
|
156
|
+
await this.passwordInput.fill(password);
|
|
157
|
+
await this.rememberMeCheckbox.check();
|
|
158
|
+
await this.submitButton.click();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async expectErrorMessage(message: string): Promise<void> {
|
|
162
|
+
await expect(this.errorMessage).toContainText(message);
|
|
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();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// tests/pages/dashboard.page.ts
|
|
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
|
+
}
|
|
199
|
+
|
|
200
|
+
async logout(): Promise<void> {
|
|
201
|
+
await this.userMenu.click();
|
|
202
|
+
await this.logoutButton.click();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async search(query: string): Promise<void> {
|
|
206
|
+
await this.searchInput.fill(query);
|
|
207
|
+
await this.searchInput.press("Enter");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async navigateTo(item: string): Promise<void> {
|
|
211
|
+
await this.navigationItems.filter({ hasText: item }).click();
|
|
35
212
|
}
|
|
36
213
|
}
|
|
37
214
|
```
|
|
38
215
|
|
|
39
|
-
|
|
216
|
+
### 3. Test Fixtures
|
|
217
|
+
|
|
40
218
|
```typescript
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
219
|
+
// tests/fixtures/auth.fixture.ts
|
|
220
|
+
import { test as base, expect } from "@playwright/test";
|
|
221
|
+
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
|
+
|
|
230
|
+
interface AuthFixtures {
|
|
231
|
+
loginPage: LoginPage;
|
|
232
|
+
dashboardPage: DashboardPage;
|
|
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",
|
|
47
242
|
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
256
|
+
const response = await page.request.post("/api/auth/login", {
|
|
257
|
+
data: {
|
|
258
|
+
email: testUser.email,
|
|
259
|
+
password: testUser.password,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const { token } = await response.json();
|
|
264
|
+
|
|
265
|
+
// Set auth cookie/storage
|
|
266
|
+
await page.context().addCookies([
|
|
267
|
+
{
|
|
268
|
+
name: "auth_token",
|
|
269
|
+
value: token,
|
|
270
|
+
domain: "localhost",
|
|
271
|
+
path: "/",
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
|
|
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
|
+
await page.goto("/dashboard");
|
|
364
|
+
|
|
365
|
+
const request = await requestPromise;
|
|
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
|
+
});
|
|
52
473
|
});
|
|
53
474
|
```
|
|
54
475
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
});
|
|
60
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
|
+
});
|
|
646
|
+
});
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
## Use Cases
|
|
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
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
// tests/e2e/accessibility.spec.ts
|
|
711
|
+
import { test, expect } from "@playwright/test";
|
|
712
|
+
import AxeBuilder from "@axe-core/playwright";
|
|
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([]);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("form has proper labels", async ({ page }) => {
|
|
726
|
+
await page.goto("/contact");
|
|
727
|
+
|
|
728
|
+
const results = await new AxeBuilder({ page })
|
|
729
|
+
.include("form")
|
|
730
|
+
.analyze();
|
|
731
|
+
|
|
732
|
+
expect(results.violations).toEqual([]);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("keyboard navigation works", async ({ page }) => {
|
|
736
|
+
await page.goto("/");
|
|
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/);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
## Best Practices
|
|
753
|
+
|
|
754
|
+
### Do's
|
|
755
|
+
|
|
756
|
+
- Use Page Object Model for maintainable tests
|
|
757
|
+
- Prefer user-facing locators (getByRole, getByLabel)
|
|
758
|
+
- Set up proper test isolation with fixtures
|
|
759
|
+
- Use API authentication for faster test setup
|
|
760
|
+
- Enable traces and screenshots for debugging
|
|
761
|
+
- Run tests in parallel for faster execution
|
|
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
|
|
779
|
+
|
|
780
|
+
## References
|
|
781
|
+
|
|
782
|
+
- [Playwright Documentation](https://playwright.dev/docs/intro)
|
|
783
|
+
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
|
|
784
|
+
- [Page Object Model](https://playwright.dev/docs/pom)
|
|
785
|
+
- [Visual Comparisons](https://playwright.dev/docs/test-snapshots)
|
|
786
|
+
- [Accessibility Testing](https://playwright.dev/docs/accessibility-testing)
|