e2e-bugfix
npx skills add https://github.com/penkzhou/swiss-army-knife-plugin --skill e2e-bugfix
Agent 安装分布
Skill 文档
E2E Bugfix Workflow Skill
æ¬ skill æä¾ç«¯å°ç«¯æµè¯ bugfix ç宿´å·¥ä½æµç¥è¯ï¼å æ¬é误åç±»ä½ç³»ã置信度è¯åç³»ç»å E2E ç¹æçè°è¯æå·§ã
é误åç±»ä½ç³»
E2E æµè¯å¤±è´¥ä¸»è¦å为以ä¸ç±»åï¼æé¢çæåºï¼ï¼
1. è¶ æ¶é误ï¼35%ï¼
çç¶ï¼å ç´ çå¾ è¶ æ¶ãæä½è¶ æ¶
è¯å«ç¹å¾ï¼
Timeout 30000ms exceededwaiting for locatorwaiting for elementTimeoutError
è§£å³çç¥ï¼ä½¿ç¨æ¾å¼çå¾ ååçè¶ æ¶
// Before - 硬ç¼ç çå¾
await page.waitForTimeout(5000);
await page.click('.submit-button');
// After - çå¾
ç¹å®æ¡ä»¶
await page.waitForSelector('.submit-button', { state: 'visible' });
await page.click('.submit-button');
// æä½¿ç¨ Playwright çèªå¨çå¾
await page.getByRole('button', { name: 'Submit' }).click();
常è§åå ï¼
- 页é¢å è½½æ ¢
- 卿å å®¹æªæ¸²æ
- ç½ç»è¯·æ±å»¶è¿
- å ç´ è¢«é®æ¡æä¸å¯è§
2. éæ©å¨é误ï¼25%ï¼
çç¶ï¼æ¾ä¸å°å ç´ ãéæ©å¨å¹é å¤ä¸ªå ç´
è¯å«ç¹å¾ï¼
strict mode violationresolved to X elementselement not foundlocator.click: Error
è§£å³çç¥ï¼ä½¿ç¨æ´ç²¾ç¡®çéæ©å¨
// Before - 模ç³éæ©å¨
await page.click('button'); // å¯è½å¹é
å¤ä¸ª
// After - ç²¾ç¡®éæ©å¨
// æ¹æ³ 1ï¼ä½¿ç¨ data-testid
await page.click('[data-testid="submit-button"]');
// æ¹æ³ 2ï¼ä½¿ç¨è§è²åææ¬
await page.getByRole('button', { name: 'Submit' }).click();
// æ¹æ³ 3ï¼ä½¿ç¨ç»åéæ©å¨
await page.locator('.form-container').getByRole('button').click();
Playwright æ¨èéæ©å¨ä¼å 级ï¼
getByRole()– æè¯ä¹ågetByTestId()– æç¨³å®getByText()– ç¨æ·å¯è§- CSS/XPath – æåææ®µ
3. æè¨é误ï¼15%ï¼
çç¶ï¼ææå¼ä¸å®é å¼ä¸å¹é
è¯å«ç¹å¾ï¼
expect(...).toHave*Expected:vsReceived:AssertionError
è§£å³çç¥ï¼ä½¿ç¨æ£ç¡®çæè¨åçå¾
// Before - ç«å³æè¨
expect(await page.textContent('.message')).toBe('Success');
// After - 使ç¨èªå¨éè¯çæè¨
await expect(page.locator('.message')).toHaveText('Success');
// 弿¥å
容æè¨
await expect(page.locator('.user-list')).toContainText('John');
// å¯è§æ§æè¨
await expect(page.locator('.modal')).toBeVisible();
4. ç½ç»é误ï¼12%ï¼
çç¶ï¼API 请æ±å¤±è´¥ãç½ç»æ¦æªé®é¢
è¯å«ç¹å¾ï¼
Route handleré误net::ERR_*request failed- Mock æ°æ®ä¸çæ
è§£å³çç¥ï¼æ£ç¡®é ç½®ç½ç»æ¦æª
// Mock API ååº
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ users: [{ id: 1, name: 'Test' }] }),
});
});
// çå¾
ç½ç»è¯·æ±å®æ
const responsePromise = page.waitForResponse('**/api/users');
await page.click('.load-users');
const response = await responsePromise;
expect(response.status()).toBe(200);
5. 导èªé误ï¼8%ï¼
çç¶ï¼é¡µé¢å¯¼èªå¤±è´¥ãURL ä¸å¹é
è¯å«ç¹å¾ï¼
page.goto: ErrorERR_NAME_NOT_RESOLVEDnavigation timeout- URL éå®åé®é¢
è§£å³çç¥ï¼æ£ç¡®å¤ç导èª
// çå¾
导èªå®æ
await page.goto('http://localhost:3000/login');
await page.waitForURL('**/dashboard');
// å¤çéå®å
await Promise.all([
page.waitForNavigation(),
page.click('.login-button'),
]);
// éªè¯ URL
await expect(page).toHaveURL(/.*dashboard/);
6. ç¯å¢é误ï¼3%ï¼
çç¶ï¼æµè§å¨å¯å¨å¤±è´¥ãæµè¯ç¯å¢é®é¢
è¯å«ç¹å¾ï¼
browser.launch失败Target closedcontexté误- 端å£å²çª
è§£å³çç¥ï¼æ£æ¥ç¯å¢é ç½®
// playwright.config.ts
export default defineConfig({
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
});
置信度è¯åç³»ç»
è¯åæ åï¼0-100ï¼
| åæ° | çº§å« | è¡ä¸º |
|---|---|---|
| 80+ | é« | èªå¨æ§è¡ |
| 60-79 | ä¸ | æ è®°éªè¯åç»§ç» |
| 40-59 | ä½ | æå询é®ç¨æ· |
| <40 | ä¸ç¡®å® | 忢æ¶éä¿¡æ¯ |
置信度计ç®
置信度 = è¯æ®è´¨é(40%) + 模å¼å¹é
(30%) + ä¸ä¸æå®æ´æ§(20%) + å¯å¤ç°æ§(10%)
è¯æ®è´¨éï¼
- é«ï¼ææªå¾ãtraceã宿´å æ
- ä¸ï¼æé误信æ¯ä½ç¼ºä¸ä¸æ
- ä½ï¼ä» æå¤±è´¥æè¿°
模å¼å¹é ï¼
- é«ï¼å®å ¨å¹é å·²ç¥é误模å¼
- ä¸ï¼é¨åå¹é
- ä½ï¼æªç¥é误类å
ä¸ä¸æå®æ´æ§ï¼
- é«ï¼æµè¯ä»£ç + 页é¢ä»£ç + trace + æªå¾
- ä¸ï¼åªææµè¯ä»£ç
- ä½ï¼åªæé误信æ¯
å¯å¤ç°æ§ï¼
- é«ï¼æ¯æ¬¡è¿è¡é½å¤ç°
- ä¸ï¼å¶åï¼flaky testï¼
- ä½ï¼ä» å¨ç¹å®ç¯å¢å¤±è´¥
E2E è°è¯æå·§
ä½¿ç¨ Trace Viewer
# è¿è¡æµè¯å¹¶æ¶é trace
npx playwright test --trace on
# æ¥ç trace
npx playwright show-trace trace.zip
ä½¿ç¨ UI 模å¼è°è¯
# å¯å¨ UI 模å¼
npx playwright test --ui
# æä½¿ç¨è°è¯æ¨¡å¼
npx playwright test --debug
æªå¾åå½å
// æµè¯å¤±è´¥æ¶èªå¨æªå¾
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== 'passed') {
await page.screenshot({ path: `screenshots/${testInfo.title}.png` });
}
});
// å½å¶è§é¢
// playwright.config.ts
use: {
video: 'on-first-retry',
}
å¤ç Flaky Tests
// éè¯ä¸ç¨³å®çæµè¯
test.describe.configure({ retries: 2 });
// æå¨é
ç½®ä¸è®¾ç½®
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});
TDD æµç¨
RED Phaseï¼å失败æµè¯ï¼
import { test, expect } from '@playwright/test';
test('should display error message on invalid login', async ({ page }) => {
// 1. 导èªå°é¡µé¢
await page.goto('/login');
// 2. æ§è¡æä½
await page.fill('[data-testid="email"]', 'invalid@email');
await page.fill('[data-testid="password"]', 'wrong');
await page.click('[data-testid="submit"]');
// 3. æè¨ææç»æ
await expect(page.locator('.error-message')).toHaveText('Invalid credentials');
});
GREEN Phaseï¼æå°å®ç°ï¼
// åªå®ç°è®©æµè¯éè¿çæå°åè½
// ä¸è¦ä¼åï¼ä¸è¦æ·»å é¢å¤åè½
REFACTOR Phaseï¼éæï¼
// æ¹åæµè¯ç»æ
// æå Page Object
// å¤ç¨æµè¯è¾
å©å½æ°
Page Object 模å¼
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="submit"]');
}
async getErrorMessage() {
return this.page.locator('.error-message');
}
}
// ä½¿ç¨ Page Object
test('login with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('invalid@email', 'wrong');
await expect(loginPage.getErrorMessage()).toHaveText('Invalid credentials');
});
è´¨éé¨ç¦
| æ£æ¥é¡¹ | æ å |
|---|---|
| æµè¯éè¿ç | 100% |
| 代ç è¦çç | >= 90%ï¼å¦éç¨ï¼ |
| Lint | æ é误 |
| Flaky Rate | < 5% |
常ç¨å½ä»¤
# è¿è¡ææ E2E æµè¯
make test TARGET=e2e
# æä½¿ç¨ Playwright ç´æ¥è¿è¡
npx playwright test
# è¿è¡ç¹å®æµè¯æä»¶
npx playwright test tests/login.spec.ts
# è¿è¡å¸¦æ ç¾çæµè¯
npx playwright test --grep @smoke
# è¿è¡ UI 模å¼
npx playwright test --ui
# çææµè¯ä»£ç
npx playwright codegen localhost:3000
# æ¥çæµè¯æ¥å
npx playwright show-report
Playwright é 置示ä¾
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: '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: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
ç¸å ³ææ¡£
ææ¡£è·¯å¾ç±é
ç½®æå®ï¼best_practices_dirï¼ï¼ä½¿ç¨ä»¥ä¸å
³é®è¯æç´¢ï¼
- éæ©å¨çç¥ï¼å ³é®è¯ “selector”, “locator”, “data-testid”
- çå¾ çç¥ï¼å ³é®è¯ “wait”, “timeout”, “retry”
- ç½ç»æ¦æªï¼å ³é®è¯ “intercept”, “mock”, “route”
- é®é¢è¯æï¼å ³é®è¯ “troubleshooting”, “debugging”, “flaky”