sillyspec 3.7.3 → 3.7.5
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/src/setup.js +45 -1
- package/templates/execute.md +56 -5
- package/templates/explore.md +1 -3
- package/templates/quick.md +1 -1
- package/templates/skills/playwright-e2e/SKILL.md +340 -0
- package/templates/verify.md +8 -3
package/package.json
CHANGED
package/src/setup.js
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
import { execSync } from 'child_process';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import ora from 'ora';
|
|
6
7
|
import { checkbox, confirm, input, select } from '@inquirer/prompts';
|
|
7
8
|
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
// ── Skill 定义 ──
|
|
13
|
+
|
|
14
|
+
const SKILLS = [
|
|
15
|
+
{
|
|
16
|
+
id: 'playwright-e2e',
|
|
17
|
+
name: 'Playwright E2E 测试参考',
|
|
18
|
+
description: 'E2E 测试编写最佳实践,AI 执行测试任务时自动读取',
|
|
19
|
+
source: join(__dirname, '..', 'templates', 'skills', 'playwright-e2e'),
|
|
20
|
+
target: '.sillyspec/skills/playwright-e2e',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
8
24
|
// ── MCP 工具定义 ──
|
|
9
25
|
|
|
10
26
|
const MCP_TOOLS = [
|
|
@@ -266,6 +282,12 @@ export async function cmdSetup(dir, options = {}) {
|
|
|
266
282
|
...dbChoices,
|
|
267
283
|
...globalChoices.length > 0 ? [{ name: chalk.bold('── 全局工具 ──'), value: '_global_header', disabled: true }] : [],
|
|
268
284
|
...globalChoices,
|
|
285
|
+
...[{ name: chalk.bold('── AI Skills(编写参考)──'), value: '_skill_header', disabled: true }],
|
|
286
|
+
...SKILLS.filter(s => !existsSync(join(dir, s.target, 'SKILL.md'))).map(s => ({
|
|
287
|
+
name: `${s.name} — ${s.description}`,
|
|
288
|
+
value: `skill:${s.id}`,
|
|
289
|
+
checked: false,
|
|
290
|
+
})),
|
|
269
291
|
];
|
|
270
292
|
|
|
271
293
|
if (allChoices.length === 0) {
|
|
@@ -355,6 +377,24 @@ export async function cmdSetup(dir, options = {}) {
|
|
|
355
377
|
}
|
|
356
378
|
}
|
|
357
379
|
|
|
380
|
+
// ── 安装 Skills ──
|
|
381
|
+
|
|
382
|
+
const selectedSkills = SKILLS.filter(s => selected.includes(`skill:${s.id}`));
|
|
383
|
+
|
|
384
|
+
if (selectedSkills.length > 0) {
|
|
385
|
+
console.log('');
|
|
386
|
+
for (const skill of selectedSkills) {
|
|
387
|
+
const spinner = ora(`安装 ${skill.name}...`).start();
|
|
388
|
+
try {
|
|
389
|
+
const targetDir = join(dir, skill.target);
|
|
390
|
+
cpSync(skill.source, targetDir, { recursive: true });
|
|
391
|
+
spinner.succeed(`${skill.name} 安装完成 → ${skill.target}/SKILL.md`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
spinner.fail(`${skill.name} 安装失败: ${err.message}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
358
398
|
// ── 总结 ──
|
|
359
399
|
|
|
360
400
|
console.log('');
|
|
@@ -374,6 +414,10 @@ export async function cmdSetup(dir, options = {}) {
|
|
|
374
414
|
console.log(` 🛠️ ${chalk.cyan(g.name)} — ${g.description}`);
|
|
375
415
|
console.log(chalk.dim(` ${g.url}`));
|
|
376
416
|
}
|
|
417
|
+
for (const s of selectedSkills) {
|
|
418
|
+
console.log(` 📚 ${chalk.cyan(s.name)} — ${s.description}`);
|
|
419
|
+
console.log(chalk.dim(` → ${s.target}/SKILL.md`));
|
|
420
|
+
}
|
|
377
421
|
console.log('');
|
|
378
422
|
if (selectedMcp.length > 0) {
|
|
379
423
|
console.log(chalk.dim(' 重启你的 AI 工具以使 MCP 配置生效。'));
|
package/templates/execute.md
CHANGED
|
@@ -96,12 +96,55 @@ cat package.json 2>/dev/null | grep -A5 '"lint\|"format\|"typecheck\|"type-check
|
|
|
96
96
|
将此摘要注入到每个子代理 prompt 的「项目约定」段之后,并追加一条铁律:
|
|
97
97
|
> **10. 遵守编码规范:** 以上「编码规范约束」中的所有规则必须严格遵守。如规则与任务描述冲突,优先遵守规范约束并报告。
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
**测试模式扫描(主代理执行):**
|
|
100
|
+
对包含 E2E/测试任务时,扫描项目已有的测试文件,提取测试风格注入子代理 prompt 的「测试模式参考」段。
|
|
101
|
+
|
|
100
102
|
```bash
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
# 检测测试框架
|
|
104
|
+
cat package.json 2>/dev/null | grep -E "playwright|cypress|jest|vitest|mocha"
|
|
105
|
+
|
|
106
|
+
# 查找已有测试文件
|
|
107
|
+
find . -name "*.spec.ts" -o -name "*.test.ts" -o -name "*.spec.tsx" -o -name "*.spec.js" \
|
|
108
|
+
-o -name "playwright.config.*" -o -name "vitest.config.*" -o -name "jest.config.*" \
|
|
109
|
+
2>/dev/null | grep -v node_modules | head -10
|
|
110
|
+
|
|
111
|
+
# 读取 1-2 个已有测试文件作为风格参考
|
|
112
|
+
# 优先读 E2E 测试,其次读通用测试
|
|
113
|
+
find tests/e2e e2e cypress/e2e __tests__ src -name "*.spec.ts" -o -name "*.test.ts" \
|
|
114
|
+
2>/dev/null | grep -v node_modules | head -3 | xargs cat 2>/dev/null
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
扫描后生成**测试模式摘要**:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
## 测试模式参考(自动扫描)
|
|
121
|
+
|
|
122
|
+
### 测试框架
|
|
123
|
+
{检测到的框架及版本,如:Playwright 1.42、Vitest 1.2}
|
|
124
|
+
|
|
125
|
+
### 断言风格
|
|
126
|
+
{从已有测试提取的断言模式,如:expect(page).toHaveText()、toEqual、toBeTruthy 等}
|
|
127
|
+
|
|
128
|
+
### Fixtures / Helper
|
|
129
|
+
{项目自定义的 test fixtures、page objects、helper 函数}
|
|
130
|
+
|
|
131
|
+
### 文件组织
|
|
132
|
+
{测试文件目录结构、命名约定、文件内组织方式}
|
|
133
|
+
|
|
134
|
+
### 配置要点
|
|
135
|
+
{playwright.config.ts 中的 baseURL、timeout、retries 等关键配置}
|
|
104
136
|
```
|
|
137
|
+
|
|
138
|
+
将此摘要注入到每个 E2E/测试子代理 prompt 的「任务描述」段之后,并追加一条铁律:
|
|
139
|
+
> **11. 参照已有测试风格:** 编写新测试时必须参照以上「测试模式参考」中的风格,包括断言方式、fixtures 使用、文件组织。不要凭记忆写测试。
|
|
140
|
+
> **12. 参考已有实现:** 写新功能前,先 grep 项目中类似功能的已有代码(`grep -r "关键词" src/`),照着项目现有的模式、风格和封装方式写,不要凭记忆编造。
|
|
141
|
+
|
|
142
|
+
**MCP 能力检测(主代理执行):**
|
|
143
|
+
检查当前可用工具列表中是否存在以下类型的 MCP 工具(不要只依赖配置文件路径,不同客户端配置位置不同):
|
|
144
|
+
- Context7 / 文档查询工具
|
|
145
|
+
- 数据库工具(postgres/sqlite/mysql/redis)
|
|
146
|
+
- 浏览器工具(browser/chrome/playwright/devtools)
|
|
147
|
+
- 搜索工具(search/web_search)
|
|
105
148
|
根据检测结果,在子代理 prompt 的「文档查询指引」段动态注入:
|
|
106
149
|
- 有 `context7` → `遇到不熟悉的库/API,使用 Context7 MCP(resolve-library-id → query-docs)查询最新文档`
|
|
107
150
|
- 无 `context7` → `遇到不熟悉的库/API,使用 web search 查询最新官方文档`
|
|
@@ -148,6 +191,9 @@ done
|
|
|
148
191
|
## 编码规范约束(自动扫描)
|
|
149
192
|
{主代理扫描项目配置文件后生成的规范摘要,见上方「编码规范扫描」步骤}
|
|
150
193
|
|
|
194
|
+
## 测试模式参考(自动扫描,仅 E2E/测试任务注入)
|
|
195
|
+
{主代理扫描项目已有测试文件后生成的测试风格摘要,见上方「测试模式扫描」步骤。非 E2E/测试任务省略此段}
|
|
196
|
+
|
|
151
197
|
## 项目架构
|
|
152
198
|
{ARCHITECTURE.md 全文}
|
|
153
199
|
|
|
@@ -174,7 +220,12 @@ done
|
|
|
174
220
|
4. **不自行补全:** 发现缺失接口/方法,不自己写,报告 BLOCKED
|
|
175
221
|
5. **TDD 不跳步:** 按任务步骤逐步执行,每步必须运行测试命令并确认结果
|
|
176
222
|
6. **测试直接通过 = 测了已有行为,重写测试**
|
|
177
|
-
7. **E2E 任务:** 如果任务描述包含"E2E"或"端到端"
|
|
223
|
+
7. **E2E 任务:** 如果任务描述包含"E2E"或"端到端":
|
|
224
|
+
- 先 cat 相关功能代码和页面组件,理解交互逻辑
|
|
225
|
+
- 参考 prompt 中「测试模式参考」段的已有测试风格
|
|
226
|
+
- **查阅 Playwright 用法:** 优先使用已安装的 playwright skill(SKILL.md),不要凭记忆写 API。未安装则通过 Context7 MCP 或 web search 查最新文档
|
|
227
|
+
- 有测试框架则编写测试文件,无框架则编写 `.sillyspec/changes/<变更名>/e2e-steps.md` 结构化测试步骤
|
|
228
|
+
- **写完必须立即跑一遍确认通过**,失败则修复后重跑,不要"写了就算完成"
|
|
178
229
|
8. **暂存:** 完成后在工作目录执行 git add -A(不要 commit,由用户通过 /sillyspec:commit 统一提交)
|
|
179
230
|
9. **不修改计划外的文件**,如必须修改则在报告中说明
|
|
180
231
|
10. **遵守编码规范:** prompt 中「编码规范约束」段的所有规则必须严格遵守。如规范与任务描述冲突,优先遵守规范并报告
|
package/templates/explore.md
CHANGED
|
@@ -37,9 +37,7 @@ cat .sillyspec/knowledge/INDEX.md 2>/dev/null
|
|
|
37
37
|
|
|
38
38
|
## MCP 能力(按需使用)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
cat .claude/mcp.json .cursor/mcp.json 2>/dev/null
|
|
42
|
-
```
|
|
40
|
+
检查当前可用工具列表中是否存在 MCP 工具(Context7/浏览器/数据库/搜索等),不依赖配置文件路径。
|
|
43
41
|
|
|
44
42
|
- 有 Context7 → 探索时查询最新文档,验证技术方案的可行性
|
|
45
43
|
- 有浏览器 MCP → 可浏览相关网站、查竞品实现
|
package/templates/quick.md
CHANGED
|
@@ -27,7 +27,7 @@ $ARGUMENTS
|
|
|
27
27
|
cat .sillyspec/knowledge/INDEX.md 2>/dev/null
|
|
28
28
|
```
|
|
29
29
|
根据当前任务描述中的关键词匹配 INDEX.md 条目,命中时 `cat` 对应知识文件,将内容纳入后续开发考量。未命中则跳过。
|
|
30
|
-
**MCP 检测:**
|
|
30
|
+
**MCP 检测:** 检查当前可用工具列表中是否存在 MCP 工具(Context7/浏览器/数据库/搜索等),根据检测结果动态利用:
|
|
31
31
|
- 有 Context7 → 查询不熟悉库/API 的最新文档
|
|
32
32
|
- 有浏览器 MCP → 验证页面改动效果
|
|
33
33
|
- 有数据库 MCP → 查询表结构和数据验证(只读)
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playwright-e2e
|
|
3
|
+
description: Playwright E2E 测试编写参考。AI 执行 E2E 任务时自动读取,不要凭记忆编写 Playwright API。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Playwright E2E 测试编写参考
|
|
7
|
+
|
|
8
|
+
> ⚠️ 执行 E2E/测试任务时必须先读取此文件,不要凭训练数据记忆编写 Playwright API。
|
|
9
|
+
|
|
10
|
+
## 测试基本结构
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { test, expect } from '@playwright/test';
|
|
14
|
+
|
|
15
|
+
test.describe('功能名称', () => {
|
|
16
|
+
test.beforeEach(async ({ page }) => {
|
|
17
|
+
await page.goto('/');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('具体场景', async ({ page }) => {
|
|
21
|
+
// Arrange
|
|
22
|
+
// Act
|
|
23
|
+
// Assert
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 选择器优先级
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// ✅ 最推荐:data-testid(最稳定)
|
|
32
|
+
await page.locator('[data-testid="submit-btn"]').click();
|
|
33
|
+
|
|
34
|
+
// ✅ 推荐:role-based(语义化)
|
|
35
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
36
|
+
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
|
37
|
+
|
|
38
|
+
// ✅ 推荐:文本匹配
|
|
39
|
+
await page.getByText('Sign in').click();
|
|
40
|
+
await page.getByText(/welcome back/i).click();
|
|
41
|
+
|
|
42
|
+
// ✅ 可用:语义 HTML
|
|
43
|
+
await page.locator('button[type="submit"]').click();
|
|
44
|
+
await page.locator('input[name="email"]').fill('test@test.com');
|
|
45
|
+
|
|
46
|
+
// ❌ 避免:CSS 类名和 ID(重构易变)
|
|
47
|
+
// await page.locator('.btn-primary').click();
|
|
48
|
+
// await page.locator('#submit').click();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 断言
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// URL
|
|
55
|
+
await expect(page).toHaveURL('/dashboard');
|
|
56
|
+
await expect(page).toHaveURL(/.*dashboard/);
|
|
57
|
+
|
|
58
|
+
// 文本
|
|
59
|
+
await expect(page.locator('h1')).toHaveText('Welcome');
|
|
60
|
+
await expect(page.locator('.message')).toContainText('success');
|
|
61
|
+
await expect(page.locator('.items')).toHaveText(['Item 1', 'Item 2']);
|
|
62
|
+
|
|
63
|
+
// 可见性
|
|
64
|
+
await expect(page.locator('.modal')).toBeVisible();
|
|
65
|
+
await expect(page.locator('.spinner')).toBeHidden();
|
|
66
|
+
await expect(page.locator('button')).toBeEnabled();
|
|
67
|
+
await expect(page.locator('input')).toBeDisabled();
|
|
68
|
+
|
|
69
|
+
// 数量
|
|
70
|
+
await expect(page.locator('.item')).toHaveCount(3);
|
|
71
|
+
|
|
72
|
+
// 输入值
|
|
73
|
+
await expect(page.locator('input')).toHaveValue('test@example.com');
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 表单操作
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// 文本输入
|
|
80
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
81
|
+
await page.getByPlaceholder('Enter your name').fill('John Doe');
|
|
82
|
+
|
|
83
|
+
// 清空再输入
|
|
84
|
+
await page.locator('#username').clear();
|
|
85
|
+
await page.locator('#username').type('newuser', { delay: 100 });
|
|
86
|
+
|
|
87
|
+
// 复选框
|
|
88
|
+
await page.getByLabel('I agree').check();
|
|
89
|
+
await page.getByLabel('I agree').uncheck();
|
|
90
|
+
|
|
91
|
+
// 单选按钮
|
|
92
|
+
await page.getByLabel('Option 2').check();
|
|
93
|
+
|
|
94
|
+
// 下拉选择
|
|
95
|
+
await page.selectOption('select#country', 'usa');
|
|
96
|
+
await page.selectOption('select#country', { label: 'United States' });
|
|
97
|
+
|
|
98
|
+
// 多选下拉
|
|
99
|
+
await page.selectOption('select#colors', ['red', 'blue']);
|
|
100
|
+
|
|
101
|
+
// 文件上传
|
|
102
|
+
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
|
|
103
|
+
await page.setInputFiles('input[type="file"]', ['file1.pdf', 'file2.pdf']);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 鼠标与键盘
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// 点击
|
|
110
|
+
await page.click('button');
|
|
111
|
+
await page.click('button', { button: 'right' }); // 右键
|
|
112
|
+
await page.dblclick('button'); // 双击
|
|
113
|
+
|
|
114
|
+
// 悬停
|
|
115
|
+
await page.hover('.menu-item');
|
|
116
|
+
|
|
117
|
+
// 拖拽
|
|
118
|
+
await page.dragAndDrop('#source', '#target');
|
|
119
|
+
|
|
120
|
+
// 键盘
|
|
121
|
+
await page.keyboard.type('Hello', { delay: 100 });
|
|
122
|
+
await page.keyboard.press('Control+A');
|
|
123
|
+
await page.keyboard.press('Enter');
|
|
124
|
+
await page.keyboard.press('Tab');
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 测试数据准备
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// ⚠️ E2E 测试不能因为"没有数据"就跳过!必须确保测试数据存在。
|
|
131
|
+
|
|
132
|
+
// 方案 1:通过 API 创建前置数据(推荐)
|
|
133
|
+
test.beforeEach(async ({ request }) => {
|
|
134
|
+
// 调用项目 API 创建测试所需数据
|
|
135
|
+
const resp = await request.post('/api/test/setup', {
|
|
136
|
+
data: { createUser: true, createOrder: true }
|
|
137
|
+
});
|
|
138
|
+
expect(resp.ok()).toBeTruthy();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 方案 2:使用 storageState 复用认证状态
|
|
142
|
+
// 先登录一次保存状态,后续测试直接复用
|
|
143
|
+
test.use({ storageState: 'tests/auth/user.json' });
|
|
144
|
+
|
|
145
|
+
// 生成认证状态的脚本:
|
|
146
|
+
// npx playwright codegen --save-storage=tests/auth/user.json http://localhost:3000/login
|
|
147
|
+
|
|
148
|
+
// 方案 3:通过数据库 MCP 直接插入数据(有数据库 MCP 时)
|
|
149
|
+
// 在 beforeEach 中调用数据库 MCP 执行 INSERT 语句准备数据
|
|
150
|
+
|
|
151
|
+
// 方案 4:API Mock(不需要真实数据)
|
|
152
|
+
test('显示用户列表', async ({ page }) => {
|
|
153
|
+
await page.route('**/api/users', route => {
|
|
154
|
+
route.fulfill({
|
|
155
|
+
status: 200,
|
|
156
|
+
contentType: 'application/json',
|
|
157
|
+
body: JSON.stringify([{ id: 1, name: 'Test User' }])
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
await page.goto('/users');
|
|
161
|
+
await expect(page.locator('.user-name')).toHaveText('Test User');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 清理:测试后清理数据,避免影响其他测试
|
|
165
|
+
test.afterEach(async ({ request }) => {
|
|
166
|
+
await request.post('/api/test/cleanup');
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**铁律:E2E 测试禁止因"没有数据"跳过。** 必须通过以上任一方案准备数据,确保测试可独立运行。
|
|
171
|
+
|
|
172
|
+
## 弹窗与 iframe
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// 新窗口/弹窗
|
|
176
|
+
const [popup] = await Promise.all([
|
|
177
|
+
page.waitForEvent('popup'),
|
|
178
|
+
page.click('button.open-popup'),
|
|
179
|
+
]);
|
|
180
|
+
await popup.waitForLoadState();
|
|
181
|
+
|
|
182
|
+
// iframe
|
|
183
|
+
const frame = page.frameLocator('#my-iframe');
|
|
184
|
+
await frame.locator('button').click();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 截图
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// 全页截图
|
|
191
|
+
await page.screenshot({ path: 'screenshot.png', fullPage: true });
|
|
192
|
+
|
|
193
|
+
// 元素截图
|
|
194
|
+
await page.locator('.chart').screenshot({ path: 'chart.png' });
|
|
195
|
+
|
|
196
|
+
// 视觉回归对比(首次生成基线,后续对比)
|
|
197
|
+
await expect(page).toHaveScreenshot('homepage.png');
|
|
198
|
+
|
|
199
|
+
// ⚠️ playwright.config.ts 中配置失败自动截图:
|
|
200
|
+
// screenshot: 'only-on-failure'(已包含在上方 config 模板中)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 等待策略
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// ✅ 推荐:自动等待(Playwright 内置)
|
|
207
|
+
await expect(page.locator('.loaded')).toBeVisible();
|
|
208
|
+
|
|
209
|
+
// ✅ 推荐:等待 URL 变化
|
|
210
|
+
await page.waitForURL('**/dashboard');
|
|
211
|
+
|
|
212
|
+
// ✅ 推荐:等待网络响应
|
|
213
|
+
const responsePromise = page.waitForResponse('**/api/users');
|
|
214
|
+
await page.click('button#load-users');
|
|
215
|
+
const response = await responsePromise;
|
|
216
|
+
|
|
217
|
+
// ❌ 避免:硬编码 sleep
|
|
218
|
+
// await page.waitForTimeout(3000);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Page Object Model
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// pages/LoginPage.ts
|
|
225
|
+
export class LoginPage {
|
|
226
|
+
constructor(private page: Page) {}
|
|
227
|
+
|
|
228
|
+
async goto() {
|
|
229
|
+
await this.page.goto('/login');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async login(email: string, password: string) {
|
|
233
|
+
await this.page.getByLabel('Email').fill(email);
|
|
234
|
+
await this.page.getByLabel('Password').fill(password);
|
|
235
|
+
await this.page.getByRole('button', { name: 'Submit' }).click();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// tests/login.spec.ts
|
|
240
|
+
import { test, expect } from '@playwright/test';
|
|
241
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
242
|
+
|
|
243
|
+
test('登录成功', async ({ page }) => {
|
|
244
|
+
const loginPage = new LoginPage(page);
|
|
245
|
+
await loginPage.goto();
|
|
246
|
+
await loginPage.login('test@example.com', 'password123');
|
|
247
|
+
await expect(page).toHaveURL('/dashboard');
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## API Mock(隔离后端)
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
test('显示用户列表', async ({ page }) => {
|
|
255
|
+
await page.route('**/api/users', route => {
|
|
256
|
+
route.fulfill({
|
|
257
|
+
status: 200,
|
|
258
|
+
contentType: 'application/json',
|
|
259
|
+
body: JSON.stringify([{ id: 1, name: 'Test User' }])
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await page.goto('/users');
|
|
264
|
+
await expect(page.locator('.user-name')).toHaveText('Test User');
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## 常见错误
|
|
269
|
+
|
|
270
|
+
### ❌ 用 CSS 类名选择元素
|
|
271
|
+
```typescript
|
|
272
|
+
await page.click('.btn-primary'); // 脆弱!重构就坏
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### ✅ 用 data-testid
|
|
276
|
+
```typescript
|
|
277
|
+
await page.click('[data-testid="submit-btn"]'); // 稳定
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### ❌ 测试之间有依赖
|
|
281
|
+
```typescript
|
|
282
|
+
test('步骤1:登录', async ({ page }) => { /* ... */ });
|
|
283
|
+
test('步骤2:需要先登录', async ({ page }) => { /* 依赖步骤1,脆弱 */ });
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### ✅ 每个测试独立
|
|
287
|
+
```typescript
|
|
288
|
+
test.beforeEach(async ({ page }) => {
|
|
289
|
+
await page.goto('/login');
|
|
290
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
291
|
+
await page.getByLabel('Password').fill('password');
|
|
292
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
293
|
+
await expect(page).toHaveURL('/dashboard');
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### ❌ 不处理异步状态
|
|
298
|
+
```typescript
|
|
299
|
+
await page.click('[data-testid="submit"]');
|
|
300
|
+
await expect(page.locator('.result')).toBeVisible(); // 可能还没加载完
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### ✅ 等待操作完成
|
|
304
|
+
```typescript
|
|
305
|
+
await page.click('[data-testid="submit"]');
|
|
306
|
+
await expect(page.locator('.result')).toBeVisible({ timeout: 10000 });
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## playwright.config.ts 模板
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { defineConfig } from '@playwright/test';
|
|
313
|
+
|
|
314
|
+
export default defineConfig({
|
|
315
|
+
testDir: './tests/e2e',
|
|
316
|
+
fullyParallel: true,
|
|
317
|
+
retries: process.env.CI ? 2 : 0,
|
|
318
|
+
timeout: 30000,
|
|
319
|
+
use: {
|
|
320
|
+
baseURL: 'http://localhost:3000', // ⚠️ 根据项目实际端口调整
|
|
321
|
+
trace: 'on-first-retry',
|
|
322
|
+
screenshot: 'only-on-failure',
|
|
323
|
+
},
|
|
324
|
+
webServer: {
|
|
325
|
+
command: 'npm run dev', // ⚠️ 根据项目实际命令调整
|
|
326
|
+
port: 3000,
|
|
327
|
+
reuseExistingServer: !process.env.CI,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## 调试命令
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
npx playwright test --headed # 有头模式
|
|
336
|
+
npx playwright test --debug # 调试模式(带 inspector)
|
|
337
|
+
npx playwright test --slowmo=1000 # 慢放
|
|
338
|
+
npx playwright show-report # 查看报告
|
|
339
|
+
npx playwright codegen URL # 录制生成代码
|
|
340
|
+
```
|
package/templates/verify.md
CHANGED
|
@@ -167,9 +167,14 @@ grep -r "TODO\|FIXME\|HACK\|XXX" src/ lib/ app/ --include="*.ts" --include="*.ts
|
|
|
167
167
|
检测已配置的 MCP 服务,利用它们做实际验证(不只查文档):
|
|
168
168
|
|
|
169
169
|
**MCP 能力检测:**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
|
|
171
|
+
不要只检查配置文件路径(不同客户端配置位置不同),直接检查当前可用工具列表中是否存在以下工具:
|
|
172
|
+
|
|
173
|
+
- 数据库相关工具(包含 postgres/sqlite/mysql/redis 关键词)
|
|
174
|
+
- 浏览器相关工具(包含 browser/chrome/puppeteer/playwright/devtools 关键词)
|
|
175
|
+
- 搜索相关工具(包含 search/web_search 关键词)
|
|
176
|
+
|
|
177
|
+
**判断方式:** 尝试调用或列出当前可用的 MCP 工具,有就用来验证,没有就跳过。
|
|
173
178
|
|
|
174
179
|
**按检测结果执行对应验证:**
|
|
175
180
|
|