code-abyss 1.6.16 → 1.7.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 +2 -2
- package/skills/SKILL.md +24 -16
- package/skills/domains/ai/SKILL.md +2 -2
- package/skills/domains/ai/prompt-and-eval.md +279 -0
- package/skills/domains/architecture/SKILL.md +2 -3
- package/skills/domains/architecture/security-arch.md +87 -0
- package/skills/domains/data-engineering/SKILL.md +188 -26
- package/skills/domains/development/SKILL.md +1 -4
- package/skills/domains/devops/SKILL.md +3 -5
- package/skills/domains/devops/performance.md +63 -0
- package/skills/domains/devops/testing.md +97 -0
- package/skills/domains/frontend-design/SKILL.md +12 -3
- package/skills/domains/frontend-design/claymorphism/SKILL.md +117 -0
- package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/skills/domains/frontend-design/engineering.md +287 -0
- package/skills/domains/frontend-design/glassmorphism/SKILL.md +138 -0
- package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/skills/domains/frontend-design/liquid-glass/SKILL.md +135 -0
- package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/skills/domains/frontend-design/neubrutalism/SKILL.md +141 -0
- package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/skills/domains/infrastructure/SKILL.md +174 -34
- package/skills/domains/mobile/SKILL.md +211 -21
- package/skills/domains/orchestration/SKILL.md +1 -0
- package/skills/domains/security/SKILL.md +4 -6
- package/skills/domains/security/blue-team.md +57 -0
- package/skills/domains/security/red-team.md +54 -0
- package/skills/domains/security/threat-intel.md +50 -0
- package/skills/orchestration/multi-agent/SKILL.md +195 -46
- package/skills/run_skill.js +134 -0
- package/skills/tools/gen-docs/SKILL.md +6 -4
- package/skills/tools/gen-docs/scripts/doc_generator.js +349 -0
- package/skills/tools/verify-change/SKILL.md +8 -6
- package/skills/tools/verify-change/scripts/change_analyzer.js +270 -0
- package/skills/tools/verify-module/SKILL.md +6 -4
- package/skills/tools/verify-module/scripts/module_scanner.js +145 -0
- package/skills/tools/verify-quality/SKILL.md +5 -3
- package/skills/tools/verify-quality/scripts/quality_checker.js +276 -0
- package/skills/tools/verify-security/SKILL.md +7 -5
- package/skills/tools/verify-security/scripts/security_scanner.js +133 -0
- package/skills/__pycache__/run_skill.cpython-312.pyc +0 -0
- package/skills/domains/COVERAGE_PLAN.md +0 -232
- package/skills/domains/ai/model-evaluation.md +0 -790
- package/skills/domains/ai/prompt-engineering.md +0 -703
- package/skills/domains/architecture/compliance.md +0 -299
- package/skills/domains/architecture/data-security.md +0 -184
- package/skills/domains/data-engineering/data-pipeline.md +0 -762
- package/skills/domains/data-engineering/data-quality.md +0 -894
- package/skills/domains/data-engineering/stream-processing.md +0 -791
- package/skills/domains/development/dart.md +0 -963
- package/skills/domains/development/kotlin.md +0 -834
- package/skills/domains/development/php.md +0 -659
- package/skills/domains/development/swift.md +0 -755
- package/skills/domains/devops/e2e-testing.md +0 -914
- package/skills/domains/devops/performance-testing.md +0 -734
- package/skills/domains/devops/testing-strategy.md +0 -667
- package/skills/domains/frontend-design/build-tools.md +0 -743
- package/skills/domains/frontend-design/performance.md +0 -734
- package/skills/domains/frontend-design/testing.md +0 -699
- package/skills/domains/infrastructure/gitops.md +0 -735
- package/skills/domains/infrastructure/iac.md +0 -855
- package/skills/domains/infrastructure/kubernetes.md +0 -1018
- package/skills/domains/mobile/android-dev.md +0 -979
- package/skills/domains/mobile/cross-platform.md +0 -795
- package/skills/domains/mobile/ios-dev.md +0 -931
- package/skills/domains/security/secrets-management.md +0 -834
- package/skills/domains/security/supply-chain.md +0 -931
- package/skills/domains/security/threat-modeling.md +0 -828
- package/skills/run_skill.py +0 -153
- package/skills/tests/README.md +0 -225
- package/skills/tests/SUMMARY.md +0 -362
- package/skills/tests/__init__.py +0 -3
- package/skills/tests/__pycache__/test_change_analyzer.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_doc_generator.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_module_scanner.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_quality_checker.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_security_scanner.cpython-312.pyc +0 -0
- package/skills/tests/test_change_analyzer.py +0 -558
- package/skills/tests/test_doc_generator.py +0 -538
- package/skills/tests/test_module_scanner.py +0 -376
- package/skills/tests/test_quality_checker.py +0 -516
- package/skills/tests/test_security_scanner.py +0 -426
- package/skills/tools/gen-docs/scripts/__pycache__/doc_generator.cpython-312.pyc +0 -0
- package/skills/tools/gen-docs/scripts/doc_generator.py +0 -520
- package/skills/tools/verify-change/scripts/__pycache__/change_analyzer.cpython-312.pyc +0 -0
- package/skills/tools/verify-change/scripts/change_analyzer.py +0 -529
- package/skills/tools/verify-module/scripts/__pycache__/module_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-module/scripts/module_scanner.py +0 -321
- package/skills/tools/verify-quality/scripts/__pycache__/quality_checker.cpython-312.pyc +0 -0
- package/skills/tools/verify-quality/scripts/quality_checker.py +0 -481
- package/skills/tools/verify-security/scripts/__pycache__/security_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-security/scripts/security_scanner.py +0 -374
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: e2e-testing
|
|
3
|
-
description: 端到端测试。Playwright、Cypress、Selenium、页面对象模式、可视化回归测试、跨浏览器测试。当用户提到E2E测试、Playwright、Cypress、端到端测试、可视化回归、UI测试时使用。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 🎭 端到端测试 · E2E Testing
|
|
7
|
-
|
|
8
|
-
## Playwright vs Cypress
|
|
9
|
-
|
|
10
|
-
| 特性 | Playwright | Cypress |
|
|
11
|
-
|------|-----------|---------|
|
|
12
|
-
| 浏览器支持 | Chromium/Firefox/WebKit | Chromium/Firefox/Edge |
|
|
13
|
-
| 多标签页 | ✅ 原生支持 | ❌ 不支持 |
|
|
14
|
-
| iframe | ✅ 完整支持 | ⚠️ 有限支持 |
|
|
15
|
-
| 文件上传/下载 | ✅ 原生支持 | ⚠️ 需插件 |
|
|
16
|
-
| 网络拦截 | ✅ 强大 | ✅ 强大 |
|
|
17
|
-
| 并行执行 | ✅ 原生支持 | ⚠️ 需付费 |
|
|
18
|
-
| 调试体验 | ⚠️ 一般 | ✅ 优秀 |
|
|
19
|
-
| 学习曲线 | 平缓 | 平缓 |
|
|
20
|
-
| 执行速度 | 快 | 快 |
|
|
21
|
-
|
|
22
|
-
## Playwright 基础
|
|
23
|
-
|
|
24
|
-
### 安装与配置
|
|
25
|
-
```bash
|
|
26
|
-
npm init playwright@latest
|
|
27
|
-
|
|
28
|
-
# 安装浏览器
|
|
29
|
-
npx playwright install
|
|
30
|
-
|
|
31
|
-
# 运行测试
|
|
32
|
-
npx playwright test
|
|
33
|
-
npx playwright test --headed # 显示浏览器
|
|
34
|
-
npx playwright test --debug # 调试模式
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
// playwright.config.ts
|
|
39
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
40
|
-
|
|
41
|
-
export default defineConfig({
|
|
42
|
-
testDir: './tests',
|
|
43
|
-
timeout: 30000,
|
|
44
|
-
retries: process.env.CI ? 2 : 0,
|
|
45
|
-
workers: process.env.CI ? 1 : undefined,
|
|
46
|
-
|
|
47
|
-
use: {
|
|
48
|
-
baseURL: 'http://localhost:3000',
|
|
49
|
-
trace: 'on-first-retry',
|
|
50
|
-
screenshot: 'only-on-failure',
|
|
51
|
-
video: 'retain-on-failure',
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
projects: [
|
|
55
|
-
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
56
|
-
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
57
|
-
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
58
|
-
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
|
|
59
|
-
],
|
|
60
|
-
|
|
61
|
-
webServer: {
|
|
62
|
-
command: 'npm run start',
|
|
63
|
-
port: 3000,
|
|
64
|
-
reuseExistingServer: !process.env.CI,
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### 基础测试
|
|
70
|
-
```typescript
|
|
71
|
-
import { test, expect } from '@playwright/test';
|
|
72
|
-
|
|
73
|
-
test('用户登录流程', async ({ page }) => {
|
|
74
|
-
// 导航
|
|
75
|
-
await page.goto('/login');
|
|
76
|
-
|
|
77
|
-
// 填写表单
|
|
78
|
-
await page.fill('input[name="username"]', 'alice');
|
|
79
|
-
await page.fill('input[name="password"]', 'password123');
|
|
80
|
-
|
|
81
|
-
// 点击按钮
|
|
82
|
-
await page.click('button[type="submit"]');
|
|
83
|
-
|
|
84
|
-
// 等待导航
|
|
85
|
-
await page.waitForURL('/dashboard');
|
|
86
|
-
|
|
87
|
-
// 断言
|
|
88
|
-
await expect(page.locator('h1')).toHaveText('欢迎, Alice');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('搜索功能', async ({ page }) => {
|
|
92
|
-
await page.goto('/');
|
|
93
|
-
|
|
94
|
-
// 输入搜索
|
|
95
|
-
await page.fill('[data-testid="search-input"]', 'Playwright');
|
|
96
|
-
await page.press('[data-testid="search-input"]', 'Enter');
|
|
97
|
-
|
|
98
|
-
// 等待结果
|
|
99
|
-
await page.waitForSelector('.search-results');
|
|
100
|
-
|
|
101
|
-
// 断言结果数量
|
|
102
|
-
const results = page.locator('.search-result-item');
|
|
103
|
-
await expect(results).toHaveCount(10);
|
|
104
|
-
});
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### 高级选择器
|
|
108
|
-
```typescript
|
|
109
|
-
// CSS 选择器
|
|
110
|
-
await page.click('button.submit');
|
|
111
|
-
|
|
112
|
-
// 文本选择器
|
|
113
|
-
await page.click('text=登录');
|
|
114
|
-
await page.click('button:has-text("提交")');
|
|
115
|
-
|
|
116
|
-
// XPath
|
|
117
|
-
await page.click('xpath=//button[@type="submit"]');
|
|
118
|
-
|
|
119
|
-
// 组合选择器
|
|
120
|
-
await page.click('form >> button:has-text("登录")');
|
|
121
|
-
|
|
122
|
-
// data-testid (推荐)
|
|
123
|
-
await page.click('[data-testid="login-button"]');
|
|
124
|
-
|
|
125
|
-
// 角色选择器
|
|
126
|
-
await page.click('role=button[name="登录"]');
|
|
127
|
-
|
|
128
|
-
// 链式定位
|
|
129
|
-
await page
|
|
130
|
-
.locator('.user-card')
|
|
131
|
-
.filter({ hasText: 'Alice' })
|
|
132
|
-
.locator('button')
|
|
133
|
-
.click();
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### 等待策略
|
|
137
|
-
```typescript
|
|
138
|
-
// 等待元素可见
|
|
139
|
-
await page.waitForSelector('.modal', { state: 'visible' });
|
|
140
|
-
|
|
141
|
-
// 等待元素消失
|
|
142
|
-
await page.waitForSelector('.loading', { state: 'hidden' });
|
|
143
|
-
|
|
144
|
-
// 等待网络请求
|
|
145
|
-
await page.waitForResponse(resp =>
|
|
146
|
-
resp.url().includes('/api/users') && resp.status() === 200
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// 等待函数返回 true
|
|
150
|
-
await page.waitForFunction(() =>
|
|
151
|
-
document.querySelectorAll('.item').length > 5
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
// 自动等待 (推荐)
|
|
155
|
-
await page.click('button'); // 自动等待可点击
|
|
156
|
-
await expect(page.locator('h1')).toBeVisible(); // 自动等待可见
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Cypress 基础
|
|
160
|
-
|
|
161
|
-
### 安装与配置
|
|
162
|
-
```bash
|
|
163
|
-
npm install cypress --save-dev
|
|
164
|
-
|
|
165
|
-
# 打开 Cypress
|
|
166
|
-
npx cypress open
|
|
167
|
-
|
|
168
|
-
# 运行测试
|
|
169
|
-
npx cypress run
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
```javascript
|
|
173
|
-
// cypress.config.js
|
|
174
|
-
const { defineConfig } = require('cypress');
|
|
175
|
-
|
|
176
|
-
module.exports = defineConfig({
|
|
177
|
-
e2e: {
|
|
178
|
-
baseUrl: 'http://localhost:3000',
|
|
179
|
-
viewportWidth: 1280,
|
|
180
|
-
viewportHeight: 720,
|
|
181
|
-
video: true,
|
|
182
|
-
screenshotOnRunFailure: true,
|
|
183
|
-
retries: {
|
|
184
|
-
runMode: 2,
|
|
185
|
-
openMode: 0,
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### 基础测试
|
|
192
|
-
```javascript
|
|
193
|
-
describe('用户登录', () => {
|
|
194
|
-
beforeEach(() => {
|
|
195
|
-
cy.visit('/login');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('成功登录', () => {
|
|
199
|
-
cy.get('[data-cy="username"]').type('alice');
|
|
200
|
-
cy.get('[data-cy="password"]').type('password123');
|
|
201
|
-
cy.get('[data-cy="submit"]').click();
|
|
202
|
-
|
|
203
|
-
cy.url().should('include', '/dashboard');
|
|
204
|
-
cy.contains('欢迎, Alice').should('be.visible');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('密码错误', () => {
|
|
208
|
-
cy.get('[data-cy="username"]').type('alice');
|
|
209
|
-
cy.get('[data-cy="password"]').type('wrongpassword');
|
|
210
|
-
cy.get('[data-cy="submit"]').click();
|
|
211
|
-
|
|
212
|
-
cy.contains('用户名或密码错误').should('be.visible');
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe('购物车', () => {
|
|
217
|
-
it('添加商品到购物车', () => {
|
|
218
|
-
cy.visit('/products');
|
|
219
|
-
|
|
220
|
-
// 添加商品
|
|
221
|
-
cy.get('[data-cy="product-1"]').within(() => {
|
|
222
|
-
cy.contains('加入购物车').click();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// 验证购物车
|
|
226
|
-
cy.get('[data-cy="cart-count"]').should('have.text', '1');
|
|
227
|
-
|
|
228
|
-
// 打开购物车
|
|
229
|
-
cy.get('[data-cy="cart-icon"]').click();
|
|
230
|
-
cy.get('[data-cy="cart-items"]').should('have.length', 1);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Cypress 命令
|
|
236
|
-
```javascript
|
|
237
|
-
// 导航
|
|
238
|
-
cy.visit('/page');
|
|
239
|
-
cy.go('back');
|
|
240
|
-
cy.reload();
|
|
241
|
-
|
|
242
|
-
// 查找元素
|
|
243
|
-
cy.get('.class');
|
|
244
|
-
cy.contains('text');
|
|
245
|
-
cy.find('.child');
|
|
246
|
-
|
|
247
|
-
// 交互
|
|
248
|
-
cy.click();
|
|
249
|
-
cy.type('text');
|
|
250
|
-
cy.clear();
|
|
251
|
-
cy.check();
|
|
252
|
-
cy.select('option');
|
|
253
|
-
|
|
254
|
-
// 断言
|
|
255
|
-
cy.should('be.visible');
|
|
256
|
-
cy.should('have.text', 'Hello');
|
|
257
|
-
cy.should('have.class', 'active');
|
|
258
|
-
cy.should('have.length', 3);
|
|
259
|
-
|
|
260
|
-
// 别名
|
|
261
|
-
cy.get('.user').as('user');
|
|
262
|
-
cy.get('@user').click();
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## 页面对象模式 (Page Object Model)
|
|
266
|
-
|
|
267
|
-
### Playwright POM
|
|
268
|
-
```typescript
|
|
269
|
-
// pages/LoginPage.ts
|
|
270
|
-
export class LoginPage {
|
|
271
|
-
constructor(private page: Page) {}
|
|
272
|
-
|
|
273
|
-
// 定位器
|
|
274
|
-
get usernameInput() {
|
|
275
|
-
return this.page.locator('[data-testid="username"]');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
get passwordInput() {
|
|
279
|
-
return this.page.locator('[data-testid="password"]');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
get submitButton() {
|
|
283
|
-
return this.page.locator('button[type="submit"]');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
get errorMessage() {
|
|
287
|
-
return this.page.locator('.error-message');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// 操作方法
|
|
291
|
-
async goto() {
|
|
292
|
-
await this.page.goto('/login');
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async login(username: string, password: string) {
|
|
296
|
-
await this.usernameInput.fill(username);
|
|
297
|
-
await this.passwordInput.fill(password);
|
|
298
|
-
await this.submitButton.click();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async getErrorText() {
|
|
302
|
-
return await this.errorMessage.textContent();
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// tests/login.spec.ts
|
|
307
|
-
import { test, expect } from '@playwright/test';
|
|
308
|
-
import { LoginPage } from '../pages/LoginPage';
|
|
309
|
-
|
|
310
|
-
test('用户登录', async ({ page }) => {
|
|
311
|
-
const loginPage = new LoginPage(page);
|
|
312
|
-
|
|
313
|
-
await loginPage.goto();
|
|
314
|
-
await loginPage.login('alice', 'password123');
|
|
315
|
-
|
|
316
|
-
await expect(page).toHaveURL('/dashboard');
|
|
317
|
-
});
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Cypress POM
|
|
321
|
-
```javascript
|
|
322
|
-
// cypress/pages/LoginPage.js
|
|
323
|
-
export class LoginPage {
|
|
324
|
-
visit() {
|
|
325
|
-
cy.visit('/login');
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
fillUsername(username) {
|
|
329
|
-
cy.get('[data-cy="username"]').type(username);
|
|
330
|
-
return this;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
fillPassword(password) {
|
|
334
|
-
cy.get('[data-cy="password"]').type(password);
|
|
335
|
-
return this;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
submit() {
|
|
339
|
-
cy.get('[data-cy="submit"]').click();
|
|
340
|
-
return this;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
getErrorMessage() {
|
|
344
|
-
return cy.get('.error-message');
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// cypress/e2e/login.cy.js
|
|
349
|
-
import { LoginPage } from '../pages/LoginPage';
|
|
350
|
-
|
|
351
|
-
describe('登录测试', () => {
|
|
352
|
-
const loginPage = new LoginPage();
|
|
353
|
-
|
|
354
|
-
it('成功登录', () => {
|
|
355
|
-
loginPage
|
|
356
|
-
.visit()
|
|
357
|
-
.fillUsername('alice')
|
|
358
|
-
.fillPassword('password123')
|
|
359
|
-
.submit();
|
|
360
|
-
|
|
361
|
-
cy.url().should('include', '/dashboard');
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
## 网络拦截与 Mock
|
|
367
|
-
|
|
368
|
-
### Playwright 网络拦截
|
|
369
|
-
```typescript
|
|
370
|
-
test('Mock API 响应', async ({ page }) => {
|
|
371
|
-
// 拦截并 Mock
|
|
372
|
-
await page.route('**/api/users', route => {
|
|
373
|
-
route.fulfill({
|
|
374
|
-
status: 200,
|
|
375
|
-
contentType: 'application/json',
|
|
376
|
-
body: JSON.stringify([
|
|
377
|
-
{ id: 1, name: 'Alice' },
|
|
378
|
-
{ id: 2, name: 'Bob' }
|
|
379
|
-
])
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
await page.goto('/users');
|
|
384
|
-
await expect(page.locator('.user-item')).toHaveCount(2);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
test('修改请求', async ({ page }) => {
|
|
388
|
-
await page.route('**/api/login', route => {
|
|
389
|
-
const request = route.request();
|
|
390
|
-
route.continue({
|
|
391
|
-
headers: {
|
|
392
|
-
...request.headers(),
|
|
393
|
-
'X-Custom-Header': 'test'
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
await page.goto('/login');
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test('等待 API 响应', async ({ page }) => {
|
|
402
|
-
const responsePromise = page.waitForResponse('**/api/users');
|
|
403
|
-
|
|
404
|
-
await page.goto('/users');
|
|
405
|
-
|
|
406
|
-
const response = await responsePromise;
|
|
407
|
-
expect(response.status()).toBe(200);
|
|
408
|
-
|
|
409
|
-
const data = await response.json();
|
|
410
|
-
expect(data).toHaveLength(10);
|
|
411
|
-
});
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### Cypress 网络拦截
|
|
415
|
-
```javascript
|
|
416
|
-
describe('API Mock', () => {
|
|
417
|
-
it('拦截并 Mock', () => {
|
|
418
|
-
cy.intercept('GET', '/api/users', {
|
|
419
|
-
statusCode: 200,
|
|
420
|
-
body: [
|
|
421
|
-
{ id: 1, name: 'Alice' },
|
|
422
|
-
{ id: 2, name: 'Bob' }
|
|
423
|
-
]
|
|
424
|
-
}).as('getUsers');
|
|
425
|
-
|
|
426
|
-
cy.visit('/users');
|
|
427
|
-
cy.wait('@getUsers');
|
|
428
|
-
|
|
429
|
-
cy.get('.user-item').should('have.length', 2);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it('使用 Fixture', () => {
|
|
433
|
-
cy.intercept('GET', '/api/users', { fixture: 'users.json' });
|
|
434
|
-
cy.visit('/users');
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it('动态响应', () => {
|
|
438
|
-
cy.intercept('POST', '/api/users', (req) => {
|
|
439
|
-
req.reply({
|
|
440
|
-
statusCode: 201,
|
|
441
|
-
body: {
|
|
442
|
-
id: 999,
|
|
443
|
-
...req.body
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
cy.visit('/users/new');
|
|
449
|
-
cy.get('[data-cy="name"]').type('Charlie');
|
|
450
|
-
cy.get('[data-cy="submit"]').click();
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
## 可视化回归测试
|
|
456
|
-
|
|
457
|
-
### Playwright 截图对比
|
|
458
|
-
```typescript
|
|
459
|
-
test('页面截图对比', async ({ page }) => {
|
|
460
|
-
await page.goto('/');
|
|
461
|
-
|
|
462
|
-
// 全页面截图
|
|
463
|
-
await expect(page).toHaveScreenshot('homepage.png');
|
|
464
|
-
|
|
465
|
-
// 元素截图
|
|
466
|
-
await expect(page.locator('.header')).toHaveScreenshot('header.png');
|
|
467
|
-
|
|
468
|
-
// 自定义配置
|
|
469
|
-
await expect(page).toHaveScreenshot('homepage-full.png', {
|
|
470
|
-
fullPage: true,
|
|
471
|
-
mask: [page.locator('.dynamic-content')], // 遮罩动态内容
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
test('跨浏览器截图', async ({ page, browserName }) => {
|
|
476
|
-
await page.goto('/');
|
|
477
|
-
await expect(page).toHaveScreenshot(`homepage-${browserName}.png`);
|
|
478
|
-
});
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
### Percy 集成
|
|
482
|
-
```typescript
|
|
483
|
-
// Playwright + Percy
|
|
484
|
-
import { test } from '@playwright/test';
|
|
485
|
-
import percySnapshot from '@percy/playwright';
|
|
486
|
-
|
|
487
|
-
test('Percy 可视化测试', async ({ page }) => {
|
|
488
|
-
await page.goto('/');
|
|
489
|
-
|
|
490
|
-
// 拍摄快照
|
|
491
|
-
await percySnapshot(page, 'Homepage');
|
|
492
|
-
|
|
493
|
-
// 交互后再拍摄
|
|
494
|
-
await page.click('[data-testid="menu"]');
|
|
495
|
-
await percySnapshot(page, 'Homepage - Menu Open');
|
|
496
|
-
});
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
```javascript
|
|
500
|
-
// Cypress + Percy
|
|
501
|
-
describe('可视化回归', () => {
|
|
502
|
-
it('首页快照', () => {
|
|
503
|
-
cy.visit('/');
|
|
504
|
-
cy.percySnapshot('Homepage');
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it('响应式快照', () => {
|
|
508
|
-
cy.visit('/');
|
|
509
|
-
cy.percySnapshot('Homepage Desktop', {
|
|
510
|
-
widths: [1280, 1920]
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
cy.viewport('iphone-x');
|
|
514
|
-
cy.percySnapshot('Homepage Mobile');
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### Chromatic (Storybook)
|
|
520
|
-
```javascript
|
|
521
|
-
// .storybook/main.js
|
|
522
|
-
module.exports = {
|
|
523
|
-
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
|
524
|
-
addons: ['@storybook/addon-essentials'],
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
// Button.stories.tsx
|
|
528
|
-
export default {
|
|
529
|
-
title: 'Components/Button',
|
|
530
|
-
component: Button,
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
export const Primary = () => <Button variant="primary">Click me</Button>;
|
|
534
|
-
export const Secondary = () => <Button variant="secondary">Click me</Button>;
|
|
535
|
-
|
|
536
|
-
// package.json
|
|
537
|
-
{
|
|
538
|
-
"scripts": {
|
|
539
|
-
"chromatic": "chromatic --project-token=<token>"
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
## 跨浏览器测试
|
|
545
|
-
|
|
546
|
-
### Playwright 多浏览器
|
|
547
|
-
```typescript
|
|
548
|
-
// playwright.config.ts
|
|
549
|
-
export default defineConfig({
|
|
550
|
-
projects: [
|
|
551
|
-
{
|
|
552
|
-
name: 'chromium',
|
|
553
|
-
use: { ...devices['Desktop Chrome'] },
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
name: 'firefox',
|
|
557
|
-
use: { ...devices['Desktop Firefox'] },
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
name: 'webkit',
|
|
561
|
-
use: { ...devices['Desktop Safari'] },
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
name: 'mobile-chrome',
|
|
565
|
-
use: { ...devices['Pixel 5'] },
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
name: 'mobile-safari',
|
|
569
|
-
use: { ...devices['iPhone 13'] },
|
|
570
|
-
},
|
|
571
|
-
],
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// 运行特定浏览器
|
|
575
|
-
// npx playwright test --project=firefox
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
### 设备模拟
|
|
579
|
-
```typescript
|
|
580
|
-
test('移动端测试', async ({ page }) => {
|
|
581
|
-
await page.goto('/');
|
|
582
|
-
|
|
583
|
-
// 验证移动端布局
|
|
584
|
-
const menu = page.locator('.mobile-menu');
|
|
585
|
-
await expect(menu).toBeVisible();
|
|
586
|
-
|
|
587
|
-
// 触摸操作
|
|
588
|
-
await page.locator('.swipeable').swipe({ direction: 'left' });
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
test('平板测试', async ({ browser }) => {
|
|
592
|
-
const context = await browser.newContext({
|
|
593
|
-
...devices['iPad Pro'],
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
const page = await context.newPage();
|
|
597
|
-
await page.goto('/');
|
|
598
|
-
});
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
## 文件上传与下载
|
|
602
|
-
|
|
603
|
-
### Playwright 文件操作
|
|
604
|
-
```typescript
|
|
605
|
-
test('文件上传', async ({ page }) => {
|
|
606
|
-
await page.goto('/upload');
|
|
607
|
-
|
|
608
|
-
// 单文件上传
|
|
609
|
-
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
|
|
610
|
-
|
|
611
|
-
// 多文件上传
|
|
612
|
-
await page.setInputFiles('input[type="file"]', [
|
|
613
|
-
'file1.jpg',
|
|
614
|
-
'file2.jpg'
|
|
615
|
-
]);
|
|
616
|
-
|
|
617
|
-
// 从 Buffer 上传
|
|
618
|
-
await page.setInputFiles('input[type="file"]', {
|
|
619
|
-
name: 'test.txt',
|
|
620
|
-
mimeType: 'text/plain',
|
|
621
|
-
buffer: Buffer.from('file content')
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test('文件下载', async ({ page }) => {
|
|
626
|
-
const downloadPromise = page.waitForEvent('download');
|
|
627
|
-
|
|
628
|
-
await page.click('a[download]');
|
|
629
|
-
|
|
630
|
-
const download = await downloadPromise;
|
|
631
|
-
const path = await download.path();
|
|
632
|
-
|
|
633
|
-
// 验证文件
|
|
634
|
-
expect(download.suggestedFilename()).toBe('report.pdf');
|
|
635
|
-
});
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
### Cypress 文件操作
|
|
639
|
-
```javascript
|
|
640
|
-
describe('文件操作', () => {
|
|
641
|
-
it('文件上传', () => {
|
|
642
|
-
cy.visit('/upload');
|
|
643
|
-
|
|
644
|
-
// 需要 cypress-file-upload 插件
|
|
645
|
-
cy.get('input[type="file"]').attachFile('example.json');
|
|
646
|
-
|
|
647
|
-
cy.contains('上传成功').should('be.visible');
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
it('文件下载', () => {
|
|
651
|
-
cy.visit('/download');
|
|
652
|
-
|
|
653
|
-
cy.get('a[download]').click();
|
|
654
|
-
|
|
655
|
-
// 验证下载
|
|
656
|
-
cy.readFile('cypress/downloads/report.pdf').should('exist');
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
## 认证与状态管理
|
|
662
|
-
|
|
663
|
-
### Playwright 认证
|
|
664
|
-
```typescript
|
|
665
|
-
// global-setup.ts
|
|
666
|
-
async function globalSetup() {
|
|
667
|
-
const browser = await chromium.launch();
|
|
668
|
-
const page = await browser.newPage();
|
|
669
|
-
|
|
670
|
-
await page.goto('/login');
|
|
671
|
-
await page.fill('[name="username"]', 'alice');
|
|
672
|
-
await page.fill('[name="password"]', 'password123');
|
|
673
|
-
await page.click('button[type="submit"]');
|
|
674
|
-
|
|
675
|
-
// 保存认证状态
|
|
676
|
-
await page.context().storageState({ path: 'auth.json' });
|
|
677
|
-
await browser.close();
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// playwright.config.ts
|
|
681
|
-
export default defineConfig({
|
|
682
|
-
globalSetup: require.resolve('./global-setup'),
|
|
683
|
-
use: {
|
|
684
|
-
storageState: 'auth.json',
|
|
685
|
-
},
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
// 测试自动使用已登录状态
|
|
689
|
-
test('访问受保护页面', async ({ page }) => {
|
|
690
|
-
await page.goto('/dashboard'); // 已登录
|
|
691
|
-
});
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
### Cypress Session
|
|
695
|
-
```javascript
|
|
696
|
-
// cypress/support/commands.js
|
|
697
|
-
Cypress.Commands.add('login', (username, password) => {
|
|
698
|
-
cy.session([username, password], () => {
|
|
699
|
-
cy.visit('/login');
|
|
700
|
-
cy.get('[data-cy="username"]').type(username);
|
|
701
|
-
cy.get('[data-cy="password"]').type(password);
|
|
702
|
-
cy.get('[data-cy="submit"]').click();
|
|
703
|
-
cy.url().should('include', '/dashboard');
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
// 测试中使用
|
|
708
|
-
describe('Dashboard', () => {
|
|
709
|
-
beforeEach(() => {
|
|
710
|
-
cy.login('alice', 'password123');
|
|
711
|
-
cy.visit('/dashboard');
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
it('显示用户信息', () => {
|
|
715
|
-
cy.contains('Alice').should('be.visible');
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
## CI/CD 集成
|
|
721
|
-
|
|
722
|
-
### GitHub Actions - Playwright
|
|
723
|
-
```yaml
|
|
724
|
-
name: Playwright Tests
|
|
725
|
-
|
|
726
|
-
on: [push, pull_request]
|
|
727
|
-
|
|
728
|
-
jobs:
|
|
729
|
-
test:
|
|
730
|
-
runs-on: ubuntu-latest
|
|
731
|
-
steps:
|
|
732
|
-
- uses: actions/checkout@v3
|
|
733
|
-
|
|
734
|
-
- uses: actions/setup-node@v3
|
|
735
|
-
with:
|
|
736
|
-
node-version: 18
|
|
737
|
-
|
|
738
|
-
- name: Install dependencies
|
|
739
|
-
run: npm ci
|
|
740
|
-
|
|
741
|
-
- name: Install Playwright
|
|
742
|
-
run: npx playwright install --with-deps
|
|
743
|
-
|
|
744
|
-
- name: Run tests
|
|
745
|
-
run: npx playwright test
|
|
746
|
-
|
|
747
|
-
- uses: actions/upload-artifact@v3
|
|
748
|
-
if: always()
|
|
749
|
-
with:
|
|
750
|
-
name: playwright-report
|
|
751
|
-
path: playwright-report/
|
|
752
|
-
retention-days: 30
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
### GitHub Actions - Cypress
|
|
756
|
-
```yaml
|
|
757
|
-
name: Cypress Tests
|
|
758
|
-
|
|
759
|
-
on: [push]
|
|
760
|
-
|
|
761
|
-
jobs:
|
|
762
|
-
test:
|
|
763
|
-
runs-on: ubuntu-latest
|
|
764
|
-
steps:
|
|
765
|
-
- uses: actions/checkout@v3
|
|
766
|
-
|
|
767
|
-
- name: Cypress run
|
|
768
|
-
uses: cypress-io/github-action@v5
|
|
769
|
-
with:
|
|
770
|
-
start: npm start
|
|
771
|
-
wait-on: 'http://localhost:3000'
|
|
772
|
-
browser: chrome
|
|
773
|
-
|
|
774
|
-
- uses: actions/upload-artifact@v3
|
|
775
|
-
if: failure()
|
|
776
|
-
with:
|
|
777
|
-
name: cypress-screenshots
|
|
778
|
-
path: cypress/screenshots
|
|
779
|
-
|
|
780
|
-
- uses: actions/upload-artifact@v3
|
|
781
|
-
if: failure()
|
|
782
|
-
with:
|
|
783
|
-
name: cypress-videos
|
|
784
|
-
path: cypress/videos
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
### Docker 运行
|
|
788
|
-
```dockerfile
|
|
789
|
-
# Playwright Dockerfile
|
|
790
|
-
FROM mcr.microsoft.com/playwright:v1.40.0-focal
|
|
791
|
-
|
|
792
|
-
WORKDIR /app
|
|
793
|
-
COPY package*.json ./
|
|
794
|
-
RUN npm ci
|
|
795
|
-
COPY . .
|
|
796
|
-
|
|
797
|
-
CMD ["npx", "playwright", "test"]
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
```yaml
|
|
801
|
-
# docker-compose.yml
|
|
802
|
-
services:
|
|
803
|
-
e2e:
|
|
804
|
-
build: .
|
|
805
|
-
environment:
|
|
806
|
-
- CI=true
|
|
807
|
-
volumes:
|
|
808
|
-
- ./playwright-report:/app/playwright-report
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
## 调试技巧
|
|
812
|
-
|
|
813
|
-
### Playwright 调试
|
|
814
|
-
```typescript
|
|
815
|
-
// 调试模式
|
|
816
|
-
test('调试测试', async ({ page }) => {
|
|
817
|
-
await page.goto('/');
|
|
818
|
-
|
|
819
|
-
// 暂停执行
|
|
820
|
-
await page.pause();
|
|
821
|
-
|
|
822
|
-
// 慢速执行
|
|
823
|
-
await page.click('button', { delay: 1000 });
|
|
824
|
-
|
|
825
|
-
// 打印日志
|
|
826
|
-
console.log(await page.title());
|
|
827
|
-
|
|
828
|
-
// 截图
|
|
829
|
-
await page.screenshot({ path: 'debug.png' });
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
// 命令行调试
|
|
833
|
-
// npx playwright test --debug
|
|
834
|
-
// npx playwright test --headed --slowMo=1000
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
### Cypress 调试
|
|
838
|
-
```javascript
|
|
839
|
-
describe('调试', () => {
|
|
840
|
-
it('调试测试', () => {
|
|
841
|
-
cy.visit('/');
|
|
842
|
-
|
|
843
|
-
// 暂停
|
|
844
|
-
cy.pause();
|
|
845
|
-
|
|
846
|
-
// 打印日志
|
|
847
|
-
cy.get('.user').then($el => {
|
|
848
|
-
console.log($el.text());
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
// 调试命令
|
|
852
|
-
cy.get('.user').debug();
|
|
853
|
-
|
|
854
|
-
// 截图
|
|
855
|
-
cy.screenshot('debug');
|
|
856
|
-
});
|
|
857
|
-
});
|
|
858
|
-
```
|
|
859
|
-
|
|
860
|
-
## 最佳实践
|
|
861
|
-
|
|
862
|
-
### 选择器优先级
|
|
863
|
-
```
|
|
864
|
-
1. data-testid (推荐)
|
|
865
|
-
2. role + accessible name
|
|
866
|
-
3. 稳定的 class/id
|
|
867
|
-
4. 文本内容 (谨慎)
|
|
868
|
-
5. CSS 选择器 (避免)
|
|
869
|
-
6. XPath (避免)
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
### 测试独立性
|
|
873
|
-
```typescript
|
|
874
|
-
// ❌ 测试依赖
|
|
875
|
-
test('创建用户', async ({ page }) => {
|
|
876
|
-
// 创建用户
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
test('编辑用户', async ({ page }) => {
|
|
880
|
-
// 依赖上一个测试
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
// ✅ 独立测试
|
|
884
|
-
test('编辑用户', async ({ page }) => {
|
|
885
|
-
// 通过 API 准备数据
|
|
886
|
-
await request.post('/api/users', { data: testUser });
|
|
887
|
-
|
|
888
|
-
// 执行测试
|
|
889
|
-
await page.goto(`/users/${testUser.id}/edit`);
|
|
890
|
-
});
|
|
891
|
-
```
|
|
892
|
-
|
|
893
|
-
### 减少等待时间
|
|
894
|
-
```typescript
|
|
895
|
-
// ❌ 固定等待
|
|
896
|
-
await page.waitForTimeout(5000);
|
|
897
|
-
|
|
898
|
-
// ✅ 智能等待
|
|
899
|
-
await page.waitForSelector('.loaded');
|
|
900
|
-
await page.waitForLoadState('networkidle');
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
## 工具清单
|
|
904
|
-
|
|
905
|
-
| 工具 | 用途 | 特点 |
|
|
906
|
-
|------|------|------|
|
|
907
|
-
| Playwright | E2E 测试 | 多浏览器、强大 API |
|
|
908
|
-
| Cypress | E2E 测试 | 优秀调试体验 |
|
|
909
|
-
| Selenium | E2E 测试 | 老牌、多语言 |
|
|
910
|
-
| Percy | 可视化回归 | 云端对比 |
|
|
911
|
-
| Chromatic | Storybook 可视化 | 组件级测试 |
|
|
912
|
-
| Puppeteer | 浏览器自动化 | Chrome DevTools |
|
|
913
|
-
|
|
914
|
-
---
|