Testing with Playwright by doanchienthangdev/omgkit
npx skills add https://github.com/doanchienthangdev/omgkit --skill 'Testing with Playwright'// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [["list"], ["html"]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "mobile", use: { ...devices["iPhone 12"] } },
],
webServer: { command: "npm run dev", url: "http://localhost:3000" },
});
| 功能 | 描述 | 参考链接 |
|---|---|---|
| 页面对象模型 | 可维护的测试架构模式 | POM 指南 |
| 自动等待 | 内置的元素和断言等待机制 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 网络模拟 | 拦截和模拟 API 响应 | 网络 |
| 视觉测试 | 用于回归测试的截图对比 | 视觉对比 |
| 跨浏览器 | Chrome、Firefox、Safari、移动设备 | 浏览器 |
| 追踪查看器 | 通过时间线调试失败的测试 | 追踪查看器 |
// tests/pages/login.page.ts
import { Page, Locator, expect } from "@playwright/test";
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.submitButton = page.getByRole("button", { name: "Sign in" });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.page.getByRole("alert")).toContainText(message);
}
}
import { test, expect } from "@playwright/test";
test("mock API response", async ({ page }) => {
await page.route("**/api/users", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ users: [{ id: 1, name: "John" }] }),
})
);
await page.goto("/users");
await expect(page.getByText("John")).toBeVisible();
});
test("capture network requests", async ({ page }) => {
const requestPromise = page.waitForRequest("**/api/analytics");
await page.goto("/dashboard");
const request = await requestPromise;
expect(request.postDataJSON()).toMatchObject({ event: "page_view" });
});
// tests/fixtures/auth.fixture.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
export const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ page }, use) => {
// 通过 API 快速认证
const response = await page.request.post("/api/auth/login", {
data: { email: "test@example.com", password: "password" },
});
const { token } = await response.json();
await page.context().addCookies([
{ name: "auth_token", value: token, domain: "localhost", path: "/" },
]);
await page.goto("/dashboard");
await use(page);
},
});
test("visual snapshot", async ({ page }) => {
await page.goto("/");
await page.addStyleTag({
content: "*, *::before, *::after { animation-duration: 0s !important; }",
});
await expect(page).toHaveScreenshot("homepage.png", {
fullPage: true,
maxDiffPixels: 100,
});
// 屏蔽动态内容
await expect(page).toHaveScreenshot("dashboard.png", {
mask: [page.getByTestId("timestamp"), page.getByTestId("avatar")],
});
});
| 推荐做法 | 避免做法 |
|---|---|
| 使用页面对象模型以提高可维护性 | 使用脆弱的 CSS 选择器 |
| 优先使用面向用户的定位器(getByRole, getByLabel) | 依赖任意等待 |
| 使用 API 认证以加速测试设置 | 在测试之间共享状态 |
| 启用追踪和截图以辅助调试 | 直接测试第三方服务 |
| 并行运行测试以提高速度 | 跳过不稳定的测试而不修复 |
| 模拟外部 API 以提高可靠性 | 硬编码测试数据 |
每周安装量
0
代码仓库
GitHub 星标数
3
首次出现
1970年1月1日
安全审计
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [["list"], ["html"]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "mobile", use: { ...devices["iPhone 12"] } },
],
webServer: { command: "npm run dev", url: "http://localhost:3000" },
});
| Feature | Description | Reference |
|---|---|---|
| Page Object Model | Maintainable test architecture pattern | POM Guide |
| Auto-Waiting | Built-in waiting for elements and assertions | Auto-Waiting |
| Network Mocking | Intercept and mock API responses | Network |
| Visual Testing | Screenshot comparison for regression testing | Visual Comparisons |
| Cross-Browser | Chrome, Firefox, Safari, mobile devices | Browsers |
| Trace Viewer | Debug failing tests with timeline | Trace Viewer |
// tests/pages/login.page.ts
import { Page, Locator, expect } from "@playwright/test";
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.submitButton = page.getByRole("button", { name: "Sign in" });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.page.getByRole("alert")).toContainText(message);
}
}
import { test, expect } from "@playwright/test";
test("mock API response", async ({ page }) => {
await page.route("**/api/users", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ users: [{ id: 1, name: "John" }] }),
})
);
await page.goto("/users");
await expect(page.getByText("John")).toBeVisible();
});
test("capture network requests", async ({ page }) => {
const requestPromise = page.waitForRequest("**/api/analytics");
await page.goto("/dashboard");
const request = await requestPromise;
expect(request.postDataJSON()).toMatchObject({ event: "page_view" });
});
// tests/fixtures/auth.fixture.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
export const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ page }, use) => {
// Fast auth via API
const response = await page.request.post("/api/auth/login", {
data: { email: "test@example.com", password: "password" },
});
const { token } = await response.json();
await page.context().addCookies([
{ name: "auth_token", value: token, domain: "localhost", path: "/" },
]);
await page.goto("/dashboard");
await use(page);
},
});
test("visual snapshot", async ({ page }) => {
await page.goto("/");
await page.addStyleTag({
content: "*, *::before, *::after { animation-duration: 0s !important; }",
});
await expect(page).toHaveScreenshot("homepage.png", {
fullPage: true,
maxDiffPixels: 100,
});
// Mask dynamic content
await expect(page).toHaveScreenshot("dashboard.png", {
mask: [page.getByTestId("timestamp"), page.getByTestId("avatar")],
});
});
| Do | Avoid |
|---|---|
| Use Page Object Model for maintainability | Fragile CSS selectors |
| Prefer user-facing locators (getByRole, getByLabel) | Relying on arbitrary waits |
| Use API auth for faster test setup | Sharing state between tests |
| Enable traces and screenshots for debugging | Testing third-party services directly |
| Run tests in parallel for speed | Skipping flaky tests without fixing |
| Mock external APIs for reliability | Hardcoding test data |
Weekly Installs
0
Repository
GitHub Stars
3
First Seen
Jan 1, 1970
Security Audits
通过 LiteLLM 代理让 Claude Code 对接 GitHub Copilot 运行 | 高级变通方案指南
31,600 周安装