pdd-skills 3.1.12 → 3.1.13

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/lib/init.js CHANGED
@@ -35,6 +35,8 @@ const AI_TEST_DIRS = [
35
35
  const AI_TEST_SOURCE_FILES = [
36
36
  'tests/testcase-ai.py',
37
37
  'tests/login_manager.py',
38
+ 'tests/recorder.py',
39
+ 'tests/__init__.py',
38
40
  ];
39
41
 
40
42
  const AI_TEST_EXAMPLE_FILES = [
@@ -498,7 +500,7 @@ export async function initProject(targetPath = '.', options = {}) {
498
500
  console.log(chalk.gray(` [OK] ${file.path}`));
499
501
  }
500
502
 
501
- // Copy AI Test Framework source files (testcase-ai.py, login_manager.py)
503
+ // Copy AI Test Framework source files (testcase-ai.py, login_manager.py, recorder.py, __init__.py)
502
504
  const srcRoot = path.resolve(__dirname, '..');
503
505
  console.log(chalk.blue('\n>> Copying AI Test source files...\n'));
504
506
  for (const rel of AI_TEST_SOURCE_FILES) {
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"pdd-skills","version":"3.1.12","type":"module","description":"PDD Skills - PRD驱动开发框架 | 41+技能: 核心(12)/专家(8)/熵减(4)/OpenSpec(10)/PR管理(7) | AI原生开发工作流 | Bug模式库(14) | PRD规则(30) | 4级门控引擎","main":"index.js","bin":{"pdd":"./bin/pdd.js","pdd-skills":"./bin/pdd.js"},"files":["bin/","lib/","skills/","templates/","scaffolds/","scripts/","config/","hooks/","docs/","index.js","tests/testcase-ai.py","tests/login_manager.py","testcases/examples/"],"scripts":{"start":"node bin/pdd.js","list":"node bin/pdd.js list","lint":"node bin/pdd.js linter --type code prd sql activiti","generate":"node bin/pdd.js generate","verify":"node bin/pdd.js verify","report":"node bin/pdd.js report","config":"node bin/pdd.js config --list","api":"node bin/pdd.js api","api:dev":"node bin/pdd.js api -p 3000 --cors","init":"node bin/pdd.js init","update":"node bin/pdd.js update","cso":"node bin/pdd.js cso","eval":"node bin/pdd.js eval","token":"node bin/pdd.js token","i18n":"node bin/pdd.js i18n"},"keywords":["pdd","prd-driven-development","ai","claude","skills","tdd","code-generation","linter","api-server","cli"],"author":"PDD Team","license":"MIT","engines":{"node":">=18.0.0"},"dependencies":{"chalk":"^5.3.0","commander":"^12.0.0","fs-extra":"^11.2.0","yaml":"^2.3.0"}}
1
+ {"name":"pdd-skills","version":"3.1.13","type":"module","description":"PDD Skills - PRD驱动开发框架 | 41+技能: 核心(12)/专家(8)/熵减(4)/OpenSpec(10)/PR管理(7) | AI原生开发工作流 | Bug模式库(14) | PRD规则(30) | 4级门控引擎","main":"index.js","bin":{"pdd":"./bin/pdd.js","pdd-skills":"./bin/pdd.js"},"files":["bin/","lib/","skills/","templates/","scaffolds/","scripts/","config/","hooks/","docs/","index.js","tests/testcase-ai.py","tests/login_manager.py","tests/recorder.py","tests/__init__.py","testcases/examples/"],"scripts":{"start":"node bin/pdd.js","list":"node bin/pdd.js list","lint":"node bin/pdd.js linter --type code prd sql activiti","generate":"node bin/pdd.js generate","verify":"node bin/pdd.js verify","report":"node bin/pdd.js report","config":"node bin/pdd.js config --list","api":"node bin/pdd.js api","api:dev":"node bin/pdd.js api -p 3000 --cors","init":"node bin/pdd.js init","update":"node bin/pdd.js update","cso":"node bin/pdd.js cso","eval":"node bin/pdd.js eval","token":"node bin/pdd.js token","i18n":"node bin/pdd.js i18n"},"keywords":["pdd","prd-driven-development","ai","claude","skills","tdd","code-generation","linter","api-server","cli"],"author":"PDD Team","license":"MIT","engines":{"node":">=18.0.0"},"dependencies":{"chalk":"^5.3.0","commander":"^12.0.0","fs-extra":"^11.2.0","yaml":"^2.3.0"}}
File without changes
@@ -0,0 +1,338 @@
1
+ """
2
+ Testcase Recorder v1.0 - 交互式浏览器操作录制器
3
+ ==============================================
4
+ 从 testcase-ai.py 中提取的独立录制模块,遵循 SRP 原则。
5
+
6
+ 功能:
7
+ 🎬 注入 JS 事件监听器,捕获用户在浏览器中的操作
8
+ 📝 将操作事件自动转换为 YAML 测试步骤
9
+ 💾 同时生成 .yaml 和 .env 文件
10
+
11
+ 用法:
12
+ from tests.recorder import run_record_mode
13
+ asyncio.run(run_record_mode("output.yaml"))
14
+
15
+ 或通过 CLI:
16
+ python tests/testcase-ai.py --record [output_path]
17
+ """
18
+
19
+ import asyncio
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ import yaml
27
+
28
+
29
+ if sys.platform == "win32":
30
+ import io
31
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
32
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
33
+
34
+
35
+ # ============================================================
36
+ # JS 注入脚本:注入到浏览器页面中捕获用户操作
37
+ # ============================================================
38
+
39
+ RECORDER_JS = """() => {
40
+ if (window.__recorder) {
41
+ return { ok: false, message: 'Already running', count: window.__recorder.events.length };
42
+ }
43
+ window.__recorder = { events: [], start: Date.now(), version: '1.0' };
44
+
45
+ function getXPath(el) {
46
+ const parts = [];
47
+ while (el && el.nodeType === 1) {
48
+ let idx = 1, sib = el.previousSibling;
49
+ while (sib) { if (sib.nodeType === 1) idx++; sib = sib.previousSibling; }
50
+ parts.unshift(el.tagName.toLowerCase() + '[' + idx + ']');
51
+ el = el.parentNode;
52
+ }
53
+ return '/' + parts.join('/');
54
+ }
55
+
56
+ function capture(e, extra) {
57
+ const t = e.target;
58
+ window.__recorder.events.push({
59
+ type: extra.type || e.type,
60
+ ts: Date.now(),
61
+ tag: t.tagName,
62
+ id: t.id || '',
63
+ cls: (t.className || '').toString().slice(0, 60),
64
+ text: (t.textContent || '').trim().slice(0, 80),
65
+ ph: t.placeholder || '',
66
+ role: t.getAttribute('role') || '',
67
+ val: (t.value || '').slice(0, 100),
68
+ href: t.href || '',
69
+ name: t.name || '',
70
+ xpath: getXPath(t),
71
+ url: location.href,
72
+ ...extra
73
+ });
74
+ }
75
+
76
+ document.addEventListener('click', (e) => capture(e, { type: 'click' }), true);
77
+ document.addEventListener('input', (e) => capture(e, { type: 'input' }), true);
78
+ document.addEventListener('change', (e) => capture(e, { type: 'change' }), true);
79
+ document.addEventListener('keydown', (e) => {
80
+ if (e.key === 'Enter') capture(e, { type: 'enter' });
81
+ }, true);
82
+
83
+ const origPush = history.pushState;
84
+ history.pushState = function() {
85
+ window.__recorder.events.push({ type: 'navigate', ts: Date.now(), url: arguments[2] || location.href });
86
+ return origPush.apply(this, arguments);
87
+ };
88
+
89
+ return { ok: true, message: 'Recorder started', pid: performance.now().toString(36) };
90
+ }"""
91
+
92
+ POLL_JS = """() => {
93
+ if (!window.__recorder) return { ok: false };
94
+ const evts = window.__recorder.events;
95
+ const result = [...evts];
96
+ window.__recorder.events = [];
97
+ return { ok: true, elapsed: Math.round((Date.now() - window.__recorder.start) / 1000), events: result };
98
+ }"""
99
+
100
+ STOP_JS = """() => {
101
+ if (!window.__recorder) return { ok: false, events: [] };
102
+ const all = window.__recorder.events;
103
+ window.__recorder = null;
104
+ return { ok: true, totalEvents: all.length, events: all };
105
+ }"""
106
+
107
+
108
+ # ============================================================
109
+ # 事件 → 步骤转换器
110
+ # ============================================================
111
+
112
+ def _event_to_step(evt: Dict, step_num: int) -> Optional[Dict]:
113
+ """将录制事件转换为 YAML 步骤
114
+
115
+ Args:
116
+ evt: 从浏览器捕获的原始事件数据
117
+ step_num: 当前步骤序号
118
+
119
+ Returns:
120
+ YAML 步骤字典,或 None(跳过该事件)
121
+ """
122
+ etype = evt.get("type", "")
123
+
124
+ if etype == "click":
125
+ text = evt.get("text", "") or ""
126
+ ph = evt.get("ph", "") or ""
127
+ href = evt.get("href", "")
128
+ tag = evt.get("tag", "")
129
+
130
+ target = ph or text or href or f"{tag}元素"
131
+ if len(target) > 50:
132
+ target = target[:47] + "..."
133
+
134
+ step = {
135
+ "step": step_num,
136
+ "desc": f"点击'{target}'",
137
+ "action": "click",
138
+ "target": target,
139
+ }
140
+ if href and href.startswith("http"):
141
+ step["wait_after"] = {"type": "navigation", "timeout": 8000}
142
+ return step
143
+
144
+ elif etype in ("input", "change"):
145
+ val = evt.get("val", "")
146
+ ph = evt.get("ph", "") or ""
147
+ target = ph or evt.get("name") or f"{evt.get('tag','')}输入框"
148
+ if not val:
149
+ return None
150
+
151
+ is_number = val.replace(".", "").replace("-", "").isdigit()
152
+ action = "fill"
153
+
154
+ step = {
155
+ "step": step_num,
156
+ "desc": f"填写'{target}'为'{val}'",
157
+ "action": action,
158
+ "target": target,
159
+ "value": val,
160
+ }
161
+ if is_number:
162
+ step["desc"] = f"填写数值'{val}'到{target}"
163
+ return step
164
+
165
+ elif etype == "navigate":
166
+ return {
167
+ "step": step_num,
168
+ "desc": f"导航至 '{evt.get('url', '')}'",
169
+ "action": "navigate",
170
+ "url": evt.get("url", ""),
171
+ "wait_after": {"type": "time", "duration": 2000},
172
+ }
173
+
174
+ elif etype == "enter":
175
+ return None
176
+
177
+ return None
178
+
179
+
180
+ # ============================================================
181
+ # 录制模式主流程
182
+ # ============================================================
183
+
184
+ async def run_record_mode(output_path: str):
185
+ """交互式录制模式:监听用户操作 → 自动生成 YAML + .env
186
+
187
+ 流程:
188
+ 1. 通过 LoginManager 连接 Chrome DevTools MCP
189
+ 2. 向页面注入 RECORDER_JS 事件监听器
190
+ 3. 轮询 POLL_JS 获取捕获的事件,实时转换为步骤显示
191
+ 4. 用户按 Enter 停止,调用 STOP_JS 收尾
192
+ 5. 生成 YAML 用例文件和 .env 环境变量文件
193
+
194
+ Args:
195
+ output_path: 输出 YAML 文件路径(.env 同目录同名生成)
196
+ """
197
+ from .login_manager import LoginManager
198
+
199
+ print("=" * 60)
200
+ print(" Testcase Recorder v1.0")
201
+ print(" Interactive Browser Operation Recorder")
202
+ print("=" * 60)
203
+
204
+ lm = LoginManager()
205
+ session = await lm.create_session()
206
+ if not session:
207
+ print("[ERROR] Cannot connect to Chrome DevTools MCP service")
208
+ sys.exit(1)
209
+
210
+ print(f"\n[1/4] Browser connected... OK")
211
+
212
+ try:
213
+ await session.call_tool("evaluate_script", {"function": RECORDER_JS})
214
+ print("[2/4] Event listeners injected... OK")
215
+ except Exception as e:
216
+ print(f"[ERROR] Injection failed: {e}")
217
+ sys.exit(1)
218
+
219
+ print("""
220
+ +--------------------------------------------------+
221
+ | You can operate in browser now! |
222
+ | |
223
+ | - Click buttons, links, inputs |
224
+ | - Fill forms, select dropdowns |
225
+ | - Navigate to different pages |
226
+ | |
227
+ | Press Enter to stop recording |
228
+ +--------------------------------------------------+
229
+ """)
230
+
231
+ all_events: List[Dict] = []
232
+ step_num = 0
233
+ last_url = ""
234
+
235
+ import msvcrt
236
+ print("\n[3/4] Recording... (Press Enter to stop)\n")
237
+
238
+ while True:
239
+ await asyncio.sleep(1.5)
240
+ try:
241
+ raw = await session.call_tool("evaluate_script", {"function": POLL_JS})
242
+ data = json.loads(raw[0]["text"]) if isinstance(raw, list) else raw
243
+ if data.get("ok") and data.get("events"):
244
+ new_evts = data["events"]
245
+ for evt in new_evts:
246
+ step_num += 1
247
+ step = _event_to_step(evt, step_num)
248
+ if step:
249
+ all_events.append(step)
250
+ action_icon = {"click": "[click]", "fill": "[fill]", "navigate": "[nav]"}.get(step["action"], "[act]")
251
+ val_info = f" -> '{step.get('value','')}'" if step.get("value") else ""
252
+ print(f" [{step_num:02d}] {action_icon} {step['desc']}{val_info}")
253
+
254
+ if new_evts:
255
+ last_url = new_evts[-1].get("url", last_url)
256
+ except Exception:
257
+ pass
258
+
259
+ if msvcrt.kbhit():
260
+ key = msvcrt.getwch()
261
+ if key == '\r':
262
+ break
263
+
264
+ print(f"\n[4/4] Stopped. Captured {len(all_events)} operation steps\n")
265
+
266
+ try:
267
+ raw = await session.call_tool("evaluate_script", {"function": STOP_JS})
268
+ final_data = json.loads(raw[0]["text"]) if isinstance(raw, list) else raw
269
+ remaining = final_data.get("events", [])
270
+ for evt in remaining:
271
+ step_num += 1
272
+ step = _event_to_step(evt, step_num)
273
+ if step:
274
+ all_events.append(step)
275
+ except Exception:
276
+ pass
277
+
278
+ if not all_events:
279
+ print("[WARN] No operations captured, no files generated.")
280
+ return
281
+
282
+ yaml_data = {
283
+ "test_id": f"REC-{int(time.time())}-recorded",
284
+ "title": "Browser Operation Recording (auto-generated)",
285
+ "priority": "P1",
286
+ "tags": ["recorded", "auto-generated"],
287
+ "author": "Testcase Recorder v1.0",
288
+ "context_check": {
289
+ "login_url": last_url.split("/")[0] + "//" + last_url.split("/")[2] if "://" in last_url else "",
290
+ "home_indicator": "",
291
+ "credentials": {"username": "${TEST_USER}", "password": "${TEST_PASS}"},
292
+ "captcha_required": False,
293
+ },
294
+ "steps": all_events,
295
+ "teardown": [
296
+ {"action": "screenshot", "name": "recorded-result-{timestamp}.png", "fullPage": True}
297
+ ],
298
+ }
299
+
300
+ env_vars = set()
301
+ for step in all_events:
302
+ val = str(step.get("value", ""))
303
+ if "${" in val:
304
+ env_vars.add(val[2:-1])
305
+
306
+ out_dir = os.path.dirname(output_path)
307
+ os.makedirs(out_dir, exist_ok=True)
308
+
309
+ with open(output_path, 'w', encoding='utf-8') as f:
310
+ yaml.dump(yaml_data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
311
+ print(f" OK YAML: {output_path}")
312
+
313
+ env_path = output_path.rsplit('.', 1)[0] + ".env"
314
+ if env_vars:
315
+ with open(env_path, 'w', encoding='utf-8') as f:
316
+ f.write("# Recorded test case environment variables (auto-generated)\n")
317
+ for var in sorted(env_vars):
318
+ f.write(f"{var}=<please fill>\n")
319
+ print(f" OK ENV: {env_path} ({len(env_vars)} vars pending)")
320
+ else:
321
+ with open(env_path, 'w', encoding='utf-8') as f:
322
+ f.write("# Recorded test case environment variables (auto-generated)\nTEST_USER=yuanye\nTEST_PASS=yuanye\n")
323
+ print(f" OK ENV: {env_path}")
324
+
325
+ print(f"\n{'-' * 50}")
326
+ print(f"Recording complete!")
327
+ print(f" Case ID : {yaml_data['test_id']}")
328
+ print(f" Steps : {len(all_events)}")
329
+ print(f" Output:")
330
+ print(f" - {output_path}")
331
+ print(f" - {env_path}")
332
+ print(f"\nNext steps:")
333
+ print(f" 1. Edit .env file to fill real test data")
334
+ print(f" 2. Review and optimize YAML step descriptions and assertions")
335
+ print(f" 3. Run: python tests/testcase-ai.py {output_path}")
336
+
337
+
338
+ __all__ = ["run_record_mode", "_event_to_step", "RECORDER_JS", "POLL_JS", "STOP_JS"]