jufubao-mall 2.0.84 → 2.0.85-beta1

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.
Files changed (49) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.claude/skills/jfb-do-test-code/SKILL.md +398 -0
  3. package/.claude/skills/jfb-do-test-code/docs/component-patterns.md +38 -0
  4. package/.claude/skills/jfb-do-test-code/docs/patterns/cascade-select-async.md +91 -0
  5. package/.claude/skills/jfb-do-test-code/docs/patterns/date-picker.md +86 -0
  6. package/.claude/skills/jfb-do-test-code/docs/patterns/drawer-close.md +14 -0
  7. package/.claude/skills/jfb-do-test-code/docs/patterns/el-cascader.md +31 -0
  8. package/.claude/skills/jfb-do-test-code/docs/patterns/el-dialog.md +16 -0
  9. package/.claude/skills/jfb-do-test-code/docs/patterns/el-message-box.md +13 -0
  10. package/.claude/skills/jfb-do-test-code/docs/patterns/el-select.md +15 -0
  11. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-base-field.md +30 -0
  12. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-cascade-select.md +25 -0
  13. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-component-map.md +18 -0
  14. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-group-input.md +24 -0
  15. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-input-range.md +25 -0
  16. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-pagination-jfb.md +88 -0
  17. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-radio-custom.md +16 -0
  18. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-region-path.md +22 -0
  19. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-remote-cascader.md +23 -0
  20. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-remote-input.md +62 -0
  21. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-remote-select-list.md +18 -0
  22. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-search.md +39 -0
  23. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-select-list.md +27 -0
  24. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-switch.md +17 -0
  25. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-table-sort.md +20 -0
  26. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-textarea-tag.md +22 -0
  27. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-tinymce.md +22 -0
  28. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-upload.md +26 -0
  29. package/.claude/skills/jfb-do-test-code/docs/patterns/xd-wang-editor.md +12 -0
  30. package/.claude/skills/jfb-do-test-code/docs/pitfalls.md +437 -0
  31. package/.claude/skills/jfb-do-test-code/docs/plan-format.md +78 -0
  32. package/.claude/skills/jfb-do-test-code/docs/testing-strategy.md +647 -0
  33. package/.claude/skills/jfb-do-test-code/docs/wait-strategy.md +99 -0
  34. package/.claude/skills/jfb-do-test-docs/SKILL.md +673 -0
  35. package/.claude/skills/jfb-do-test-init/SKILL.md +248 -0
  36. package/.claude/skills/jfb-do-test-init/docs/pitfalls.md +126 -0
  37. package/.claude/skills/jfb-do-test-init/templates/auth.ts +115 -0
  38. package/.claude/skills/jfb-do-test-init/templates/constants.ts +13 -0
  39. package/.claude/skills/jfb-do-test-init/templates/global-setup.ts +26 -0
  40. package/.claude/skills/jfb-do-test-init/templates/login.spec.ts +9 -0
  41. package/.claude/skills/jfb-do-test-init/templates/playwright.config.ts +36 -0
  42. package/.claude/skills/jfb-do-test-init/templates/tools.ts +246 -0
  43. package/.claude/skills/jfb-do-test-init/templates/tsconfig.json +16 -0
  44. package/.claude/skills/jfb-do-test-init/templates/verify.ts +102 -0
  45. package/package.json +1 -1
  46. package/src/components/JfbMallConfirm/JfbMallConfirm.vue +175 -16
  47. package/src/components/JfbMallConfirm/orderMixin.js +4 -0
  48. package/tests/e2e/plan/JfbMallConfirm/JfbMallConfirm-test-plan.md +798 -0
  49. package/get.package.path.js +0 -22
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git checkout *)",
5
+ "Bash(npx lessc *)",
6
+ "Bash(python3 *)"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,398 @@
1
+ ---
2
+ name: jfb-do-test-code
3
+ description: E2E 测试代码编写技能,基于测试用例文档编写 Playwright 测试代码(*.spec.ts)
4
+ ---
5
+
6
+ # jfb-do-test-code
7
+
8
+ ## 一、硬性规则
9
+
10
+ ### 1.1 分支选择
11
+
12
+ 每次执行技能时,先通过 glob 判断模式:
13
+
14
+ ```
15
+ glob tests/e2e/{子目录}/*.spec.ts
16
+ ├── 无匹配 → 新建模式(§1.1.1)
17
+ └── 有匹配 → 迭代模式(§1.1.2)
18
+ ```
19
+
20
+ 子目录从 test-plan 路径提取:`tests/e2e/plan/{子目录}/{name}-test-plan.md`
21
+
22
+ ---
23
+
24
+ ### 1.1.1 新建模式(从头编写)
25
+
26
+ ```
27
+ → 第一步:完整阅读测试用例文档(tests/e2e/plan/*-test-plan.md),严格根据文档编写测试代码
28
+ → 第二步:逐步仔细阅读知识库核心文档(不可跳过,不可一次性 summarize):
29
+ 1. ./docs/component-patterns.md(组件交互索引)
30
+ 2. ./docs/wait-strategy.md(等待策略原理)
31
+ 3. ./docs/pitfalls.md(踩坑案例)
32
+ 4. ./docs/testing-strategy.md(测试策略经验)
33
+ 每个文件必须完整读取,理解后再进入下一步
34
+ → 第三步:调用 aimax-plan 制定编码计划
35
+ 读取知识库 ./docs/plan-format.md,计划输出必须包含「结构总览」+「测试用例细则」两个章节
36
+ → 第四步:等待用户确认(必须含"同意计划/同意操作/帮我实施/我已确认/确认计划"的精确关键词)才能开始编写测试代码
37
+ → 第五步:编写代码时,遇到具体组件交互场景,再读取对应的 ./docs/patterns/{component-name}.md
38
+ → 第六步:编写完成后,必须自动运行测试并调试到全部通过
39
+ 1. 首次运行:`npx playwright test tests/e2e/{子目录}/{name}.spec.ts`
40
+ 2. 检查运行结果:是否有 failed 用例
41
+ 3. 有失败用例时:分析失败原因 → 定位问题 → 修复代码
42
+ 4. 调试阶段:只运行被修改的单个用例(`-g "TC-ID"`),不要每次修改都跑全量
43
+ 5. 单用例通过后,再跑全量验证(`npx playwright test tests/e2e/{子目录}/{name}.spec.ts`)
44
+ 6. 反复调试,直到所有用例通过(passed)或合理跳过(skipped)
45
+ 7. 禁止在存在 failed 用例的情况下宣告任务完成
46
+ 8. 调试过程中的常见问题和解决方案必须追加到知识库(./docs/pitfalls.md)
47
+ ```
48
+
49
+ ### 1.1.2 迭代模式(Diff 思维)
50
+
51
+ 迭代模式的核心是 **Diff 思维**:不从头构思,而是比较 test-plan 与现有 spec 的差异,输出差异清单,逐条修复。
52
+
53
+ **上下文复用规则**:如果当前对话中已经读取过 test-plan 或 spec,直接引用已读内容,跳过该文件的读取步骤。
54
+
55
+ ```
56
+ → 第一步:上下文检查 — 已有 test-plan/spec 内容则引用,否则读文件
57
+ test-plan 已读 → 跳过读取,直接引用
58
+ spec 已读 → 跳过读取,直接引用
59
+ → 第二步:执行 Diff 分析 — 逐条对照 test-plan 与 spec,输出差异清单
60
+ ├── 遗漏:test-plan 有,spec 没有的用例/字段/断言
61
+ ├── 冗余:spec 有,但 test-plan 已删除/合并/不存在的代码
62
+ ├── 偏差:实现步骤或断言与 test-plan 不一致(操作顺序、字段值、预期结果)
63
+ ├── 结构:重复工具函数、顺序错乱、命名不规范
64
+ └── 质量:API 验证遗漏、等待策略错误、硬编码值
65
+ → 第三步:定向查阅知识库 — 只读差异清单命中类型的相关文档,不全文阅读
66
+ ├── 涉及组件交互 → 读 ./docs/patterns/{component-name}.md
67
+ ├── 涉及等待/超时 → 读 ./docs/wait-strategy.md
68
+ └── 涉及踩坑/策略 → 读 ./docs/pitfalls.md / testing-strategy.md
69
+ → 第四步:调用 aimax-plan 制定修复计划
70
+ plan 输出为「差异清单 + 逐条修复方案」,不输出结构总览
71
+ → 第五步:等待用户确认
72
+ → 第六步:逐条修复差异(按清单顺序,每条 fix 独立提交)
73
+ → 第七步:运行测试并调试到全部通过(同新建模式第六步)
74
+ ```
75
+
76
+ **迭代模式与新建模式的关键区别**:
77
+
78
+ | 维度 | 新建模式 | 迭代模式 |
79
+ |------|---------|---------|
80
+ | 认知方式 | 理解 test-plan → 构建 spec | 比较 test-plan vs spec → 修复差异 |
81
+ | 知识库 | 全文阅读(4 核心文件) | 定向查阅(按差异类型) |
82
+ | plan 格式 | 结构总览 + 用例细则 | 差异清单 + 逐条修复方案 |
83
+ | spec 文件 | 全新创建 | 最小 diff 修改 |
84
+
85
+ **禁止行为**(通用):
86
+ - ❌ 跳过知识库阅读直接写代码
87
+ - ❌ 对知识库内容做 summarize 而不逐文件阅读
88
+ - ❌ 凭记忆写代码而不核实知识库最新内容
89
+ - ❌ 在测试代码中私自发起 HTTP 请求:禁止 `page.evaluate` + `fetch`、`XMLHttpRequest`、`axios` 等任何方式绕过 UI 操作数据
90
+
91
+ **唯一例外 — 表单提交后落库校验**:提交创建/编辑后,允许通过 `page.request` 调用只读详情接口,将提交数据与接口返回数据逐字段比对校验落库正确性。操作阶段必须走完整 UI 链路,仅验证阶段可调只读接口
92
+ - ❌ 在测试数据、操作目标、选择数量中使用固定值/硬编码(测试的价值是暴露随机组合产生的错误;系统属性如超时常量、API 路径不受此限)
93
+
94
+ **禁止行为**(迭代模式专属):
95
+ - ❌ 迭代模式下走新建模式流程(全文读知识库、输出结构总览 plan)
96
+ - ❌ 不执行 Diff 分析直接开始改代码(必须输出差异清单)
97
+ - ❌ 差异清单描述模糊(必须精确到:遗漏用例 TC-ID、冗余函数名/行号、偏差的步骤编号)
98
+ - ❌ 修改与差异清单不相关的代码(只修差异,不做重构/优化/重构)
99
+ - ❌ 新增工具函数到 spec 文件(应追加到 tests/e2e/utils/tools.ts)
100
+ - ❌ 不改动 spec 文件结构(describe 层级、测试顺序、变量命名等覆盖范围外的改动)
101
+
102
+ ### 1.2 测试用例文档结构(标准格式)
103
+
104
+ test-plan.md 是标准文档,包含六个固定章节,编写测试代码时必须逐章阅读理解:
105
+
106
+ | 章节 | 内容 | 编写测试时的用途 |
107
+ |------|------|----------------|
108
+ | 一、功能树 | 页面功能模块和组件类型 | 确定 describe 嵌套结构、识别组件调用知识库 |
109
+ | 二、评分表 | 用例优先级和保留/删除决策 | 只实现"保留"和"待定"的用例,已删除的不写 |
110
+ | 三、表单字段清单 | 字段名、valueKey、组件类型、联动关系 | 确定表单操作顺序、联动字段先操作 |
111
+ | 四、涉及 API | API 函数、方法、URL、参数 | 定义 API 常量对象、waitForResponse 匹配路径 |
112
+ | 五、已删除用例 | 被删用例及原因 | 跳过,不编写 |
113
+ | **六、测试用例** | **每条用例的 TC-ID、模块、操作步骤、预期结果** | **逐条实现,操作步骤 → 测试代码,预期结果 → 断言** |
114
+
115
+ **§六 测试用例格式**:
116
+ ```
117
+ #### TC-{模块缩写}-{序号} {用例名称}
118
+ | 项目 | 内容 |
119
+ | 模块 | 搜索区域 / 主表格与子表格 / 操作功能 / 分页 / 边界情况 / 权限控制 / 页面基础 |
120
+ | 测试场景 | ... |
121
+ | 前置条件 | ... |
122
+ | 优先级 | P1 / P2 |
123
+
124
+ 操作步骤 → 逐条转为测试代码
125
+ 预期结果 → 逐条转为 expect 断言
126
+ ```
127
+
128
+ **强制对齐规则**:
129
+ - describe 名称必须与用例的「模块」字段一致
130
+ - 测试标题必须包含 TC-ID 和用例名称(如 `test('TC-SR-001 多字段组合搜索', ...)`)
131
+ - 操作步骤的顺序必须与代码执行顺序一致
132
+ - 预期结果中的每一条都必须有对应断言
133
+
134
+ ### 1.3 设计原则
135
+
136
+ ```
137
+ → 用例设计必须以用户操作链路为核心,保持步骤连贯性和上下文完整,模拟真实业务场景
138
+ → 不需要单独创建权限测试用例,需要放到具体模块里面就行
139
+ ```
140
+
141
+ ### 1.4 编码规范
142
+
143
+ ```
144
+ → 必须使用 serial 模式,测试连续执行共享页面状态,前序测试的操作结果后续测试直接复用
145
+ → 有选择操作的必须使用随机原则
146
+ → 有列表记录的功能必须使用随机原则
147
+ → 表单创建/编辑时,遇到字段联动必须先操作联动触发字段,等待联动完成后再提交
148
+ → 优先使用 tests/e2e/utils/ 目录下的工具函数,禁止重复造轮子
149
+ ```
150
+
151
+ ### 1.5 态度要求
152
+
153
+ ```
154
+ → ❌ 不要跑偏,不乱说,不迎合,结果要实事求是
155
+ → 没有 -test-plan.md 文档时直接拒绝,不迎合不补全
156
+ ```
157
+
158
+ ## 二、输出规范
159
+
160
+ ### 2.1 输出路径规则
161
+
162
+ ## 三、职责边界
163
+
164
+ - ✅ 编写 Playwright E2E 测试代码(`tests/e2e/*.spec.ts`)
165
+ - ✅ 编写测试工具/配置/全局设置文件
166
+ - ❌ 不修改业务代码(`src/` 目录下的任何文件)
167
+ - ❌ 不编写测试用例文档(由 `jfb-do-test-docs` 技能负责)
168
+
169
+ ## 四、代码规范
170
+
171
+ ### 1. Serial 模式
172
+
173
+ - 必须使用 `test.describe.configure({ mode: 'serial' })` 串行模式
174
+ - beforeAll 中创建共享 page,afterAll 中关闭
175
+ - 每个测试必须独立可运行,任何 test 都能通过 `-g` 单独运行
176
+ - **共享窗口,不要来回启动**:serial 下全程只有一个浏览器窗口,测试之间页面状态和操作结果直接接力。不要在测试内部或测试之间反复启动新窗口、反复跳转页面。已在目标页就跳过导航,直接操作
177
+ - 禁止用注释替代实际调用(注释无法保证状态)
178
+ - 3 处以上重复逻辑必须提取为统一方法
179
+
180
+ ### 2. 条件式导航
181
+
182
+ - 每个测试用条件式导航:已在目标页则跳过 goto,不在则 goto + waitForResponse + WAIT_API_RENDER
183
+ - 定义常量 `PAGE_PATH` 存储当前模块路由
184
+ - 兼容两种运行模式:全量串行(首个 goto 后续跳过)、单用例调试(当前 goto)
185
+
186
+ ### 3. 前置条件分离(列表进入模式)
187
+
188
+ 前置条件(导航/登录/数据准备)不属于测试用例,必须放在 `beforeAll` 中,用标志位控制用例执行。适用于**从列表页随机选择一条记录进入详情页**的模式。
189
+
190
+ 详见知识库:`./docs/testing-strategy.md` §前置条件与测试用例分离
191
+
192
+ ### 4. 测试模式
193
+
194
+ - **使用有头模式**(`headless: false`),默认弹出浏览器窗口便于观察测试流程
195
+ - 运行命令:`npx playwright test xxx.spec.ts`
196
+ - **调试阶段** 请使用“单用例”模式调试,禁止跑全用例
197
+
198
+ ### 5. describe 嵌套框架
199
+
200
+ `test.describe` 嵌套层级必须与 test-plan.md §六的模块分类一一对应,按以下顺序排列(有则按此序,无则跳过):
201
+
202
+ - 一级:`模块名称 E2E 测试`
203
+ - 二级(平铺):页面基础 → 搜索区域 → 主表格与子表格 → 操作功能 → 分页 → 边界情况 → 权限控制
204
+ - 操作功能下三级:每个操作按钮独立子 describe
205
+ - 无子表格、无分页、无多角色的层次可省略
206
+
207
+ ### 5. 多账号认证
208
+
209
+ - 账号信息统一在 `tests/e2e/global-setup.ts` 的 `PROJECT_ACCOUNTS` 对象中管理,通过 `project.name` 自动匹配
210
+ - 提供两个登录工具函数(位于 `tests/e2e/utils/auth.ts`):
211
+ - `loginAs(username?, password?)` — 独立登录,自行启动浏览器,保存认证状态后关闭,适用于 globalSetup
212
+ - `loginAsPage(browser)` — 使用 fixture 提供的 browser 直接登录,不额外弹窗,返回认证后的 Page 对象,适用于测试用例
213
+ - 测试文件通过 `beforeAll` + `loginAsPage(browser)` 完成登录,避免多次弹窗
214
+ - 通过 `test.info().project.name` 区分角色,用 `test.skip()` 条件跳过不适用的 project
215
+
216
+ ### 6. 权限验证
217
+
218
+ 权限控制的前端实现方式决定了断言方式:
219
+
220
+ | 实现方式 | 无权限时 | 断言方式 |
221
+ |---------|---------|---------|
222
+ | `v-if` | 从 DOM 移除 | `count() === 0` |
223
+ | `v-show` | 隐藏保留在 DOM | `toBeHidden()` |
224
+
225
+ - 项目中大部分权限用 `v-if`,少量用 `v-show`
226
+ - 不确定时先 `isVisible()` 判断,再用 `count()` 区分"隐藏"还是"不存在"
227
+
228
+ ### 7. API 等待与常量
229
+
230
+ - 禁止使用 `waitForLoadState('networkidle')`,必须用 `waitForResponse`
231
+ - 匹配条件必须精确到 API 路径,禁止用域名模糊匹配
232
+ - 必须先声明 waitForResponse,再执行触发操作(click),再 await 响应
233
+ - 响应后追加 `waitForTimeout(WAIT_API_RENDER)` 等待 Vue 渲染
234
+ - API 路径统一定义为常量对象(如 `const API = { LIST: '/partner/v1/category/list' }`)
235
+ - 所有 timeout 参数必须使用 `E2E_CONFIG` 常量,禁止硬编码数字
236
+ - 常量集中管理在 `tests/e2e/utils/constants.ts`(WAIT_API_RENDER、WAIT_DEBOUNCE、WAIT_SUBMIT、WAIT_CLEAR、WAIT_API_TIMEOUT)
237
+ - **禁止修改常量值**:常量一经设定即为固定值,编写测试时不得以"优化速度"等理由修改 constants.ts 中的常量值
238
+ - **表单提交后落库校验**:操作阶段走完整 UI 链路。提交后可调用只读接口验证数据,工具函数见 `tests/e2e/utils/verify.ts`
239
+
240
+ ### 8. 表单操作
241
+
242
+ **背景**:项目使用动态生成表单(inForm() 重建机制),字段绑定有 debounce(约 300ms)。连续快速输入会导致表单重建覆盖前值、DOM 元素被替换导致定位器失效。
243
+
244
+ **通用规则**:
245
+ - 每个字段操作流程:click → fill('') → WAIT_CLEAR → fill(value) → WAIT_DEBOUNCE
246
+ - 提交前等待 WAIT_API_RENDER
247
+ - 填完后必须验证写入值
248
+ - 联动触发字段必须最先操作,等联动完成后再填其他字段
249
+ - 日期字段禁止直接 fill 值,必须通过 UI 选择(快捷选项或日历面板随机选日期 + 点确认按钮)
250
+
251
+ 各类表单组件的具体操作方式,查阅 [./docs/component-patterns.md](./docs/component-patterns.md)
252
+
253
+ ### 9. 随机性原则(数据操作核心)
254
+
255
+ 所有测试数据操作遵循三大随机维度,严禁硬编码:
256
+
257
+ | 维度 | 含义 | 规则 |
258
+ |------|------|------|
259
+ | **目标随机** | 「选谁」— 在多个可选目标中随机选取 | N > 1 个同类目标时,必须随机选,不固定第一个/最后一个 |
260
+ | **数量随机** | 「选多少」— 随机决定操作几个 | 在 [1, 可用总数] 区间内随机,避免边界值(1 或全选) |
261
+ | **数据随机** | 「填什么」— 随机生成填充值 | 优先从页面真实数据读取,其次随机生成,严禁写死 |
262
+
263
+ **三维组合**:每个测试用例 = 目标随机 × 数量随机 × 数据随机
264
+
265
+ **通用工具**:`randomInt(min, max)` / `randomPick(arr)`
266
+
267
+ #### 数据来源优先级
268
+ 1. 优先从页面已有数据读取(如表格第一行)
269
+ 2. 其次用随机生成器
270
+ 3. 最后用预定义池随机取
271
+
272
+ #### 数据格式规范
273
+ - 格式真实感:`${name}(已修改-${id})` 而非 `name_edited_1234`
274
+ - 编辑测试随机选字段:从可编辑字段列表随机选 1~3 个修改
275
+
276
+ #### 定位策略
277
+ - 优先语义定位:`getByPlaceholder` / `getByRole` / `getByText`
278
+ - 同 placeholder 重复用 `.nth(1)` 区分
279
+ - 定义 locator 变量复用,避免重复查找
280
+ - 翻页随机选:从 DOM 读 `.el-pager li.number` 实际页码再随机选
281
+
282
+ #### 调试输出
283
+ - 关键操作后 `console.log` 输出随机值和测试结果
284
+
285
+ ### 10. 测试覆盖与清理
286
+
287
+ 测试代码的 describe 结构必须对齐 §六用例的模块分层,每层根据实际功能点确定覆盖范围:
288
+
289
+ | 层次 | 覆盖要点 |
290
+ |------|---------|
291
+ | 页面基础 | 权限访问、标题渲染 |
292
+ | 搜索区域 | 每个搜索字段至少覆盖 1 个场景;**每个搜索用例执行完必须清空条件**(优先点「重置」,清空后等待 WAIT_API_RENDER) |
293
+ | 主表格与子表格 | 有子表格时覆盖展开和收起 |
294
+ | 操作功能 | 按钮存在性 + 完整操作链路 |
295
+ | 分页 | 有分页时覆盖翻页和页码跳转 |
296
+ | 边界情况 | 无数据、网络异常(用 `page.route()` + `route.abort()` 模拟,测试完必须 `unroute()` 恢复) |
297
+ | 权限控制 | 多角色时覆盖有权限和无权限 |
298
+
299
+ ### 11. 搜索区域测试精髓(必读)
300
+
301
+ 搜索测试的核心原则(详细实现与代码示例见 [docs/testing-strategy.md](./docs/testing-strategy.md) §搜索区域测试精髓):
302
+
303
+ 1. **15 秒定律**:任何两个操作之间超过 15 秒都是问题,`waitForResponse` 超时说明代码或监听链接有缺陷。
304
+ 2. **数据随机性**:遵循 §9 三维原则。输入框从表格第一行读取,下拉框随机选,远程搜索随机关键词,数值随机生成。
305
+ 3. **组合搜索三要素**:字段数量随机(>5 个条件时随机选「条件个数 − 2」个填写,≤5 个条件时全部填写)、字段选择随机、字段值随机。
306
+ 4. **时间组件**:禁止直接 fill 日期值,必须通过 UI 操作。快捷选项和日历面板随机选日期各覆盖一个用例,选完后必须点确认按钮完成操作。
307
+ 5. **联动字段策略**:先读源码确认联动类型(API / v-if / v-show),不同类型等待和断言方式不同。联动触发字段必须最先操作。
308
+
309
+ ### 12. 表格操作随机性(必读)
310
+
311
+ 表格上的任何操作遵循 §9「目标随机」原则:
312
+ - 表格上的任何操作(展开、勾选、编辑、删除等)都必须随机选择行
313
+ - 严禁固定操作第 1 行,模拟真实用户行为
314
+ - 操作前自动滚动到目标行(`scrollIntoViewIfNeeded()`),确保可见
315
+
316
+ **适用场景**:
317
+ - 展开/收起子表格:随机选一行展开
318
+ - 行选择/勾选:随机选 N 行勾选
319
+ - 单行操作按钮:随机选一行点击操作
320
+ - 批量操作:随机选择多行后执行批量操作
321
+
322
+ **输出日志**:关键操作后输出随机选择的行索引,便于调试
323
+
324
+ ### 13. 知识库
325
+
326
+ **编码前必读(核心文档,逐步阅读,不可跳过)**:
327
+ - 组件交互索引:[./docs/component-patterns.md](./docs/component-patterns.md)
328
+ - 等待策略原理:[./docs/wait-strategy.md](./docs/wait-strategy.md)
329
+ - 踩坑案例:[./docs/pitfalls.md](./docs/pitfalls.md)
330
+ - 测试策略经验:[./docs/testing-strategy.md](./docs/testing-strategy.md)
331
+ - 工具函数目录:`tests/e2e/utils/`(目录下所有 .ts 文件必须完整读取,不可跳过)
332
+ - `constants.ts` — 超时配置常量
333
+ - `auth.ts` — 登录认证(loginAs / loginAsPage,支持自动账号识别)
334
+ - `verify.ts` — 通用校验
335
+ - `tools.ts` — 公共工具函数(随机数、页面导航、表单填充、搜索、下拉选择、日期选择)
336
+
337
+ **编码阶段(按场景匹配,遇到再读)**:
338
+ - 具体组件交互细节:[./docs/patterns/{component-name}.md](./docs/patterns/)(见 component-patterns.md 索引)
339
+
340
+ **规则**:
341
+ - 编码前必须完整阅读 4 个核心文档和 `tests/e2e/utils/` 目录下所有文件,每个文件逐行读取
342
+ - 编码时遇到具体组件(如 xd-remote-input、date-picker),再读取对应的 patterns 文件
343
+ - 一个测试用例可能命中多个场景,逐条读取
344
+ - **知识库中所有代码示例仅作参考,说明操作思路,不得直接复制作为正式测试用例**
345
+
346
+ **知识库维护流程**(踩坑后执行):
347
+
348
+ 1. **追加知识**:按类型写入对应位置,格式固定为:
349
+ ```
350
+ # 组件/场景名称
351
+ - 现象:出了什么问题
352
+ - 原因:为什么会出
353
+ - 做法:正确写法 + 代码示例
354
+ ```
355
+
356
+ 2. **存放位置**:
357
+ - 组件交互 → 新建 `./docs/patterns/{component-name}.md`,并在 `./docs/component-patterns.md` 索引中添加一行
358
+ - 等待时机 → 追加到 `./docs/wait-strategy.md`
359
+ - 排查失败 → 追加到 `./docs/pitfalls.md`
360
+ - 测试设计 → 追加到 `./docs/testing-strategy.md`
361
+
362
+ 3. **通知用户**:知识库更新后,必须明确提示用户:
363
+ ```
364
+ ⚠️ 知识库已更新:
365
+ - 新增:./docs/patterns/{component-name}.md
366
+ - 原因:{踩坑场景简述}
367
+ 请确认是否提交到技能仓库。
368
+ ```
369
+
370
+ ### 14. 文件结构
371
+
372
+ ```
373
+ tests/e2e/
374
+ ├── utils/
375
+ │ ├── constants.ts # 超时配置
376
+ │ ├── auth.ts # 登录工具
377
+ │ ├── verify.ts # 通用校验(调只读接口比对数据)
378
+ │ └── tools.ts # 公共工具函数(导航、表单、搜索、下拉、日期、随机数)
379
+ ├── .auth/ # 认证缓存(gitignore)
380
+ ├── global-setup.ts # 预生成认证
381
+ ├── playwright.config.ts
382
+ └── {module}.spec.ts # 测试文件
383
+
384
+ .claude/skills/jfb-do-test-code/docs/ # 知识库(技能内置)
385
+ ├── plan-format.md # aimax-plan 输出格式
386
+ ├── component-patterns.md # 组件交互索引 → patterns/
387
+ ├── patterns/ # 每个组件一个文件(26+)
388
+ │ ├── el-select.md
389
+ │ ├── el-cascader.md
390
+ │ ├── date-picker.md
391
+ │ ├── xd-search.md
392
+ │ ├── xd-remote-input.md
393
+ │ ├── xd-base-field.md
394
+ │ └── ... # 详见 component-patterns.md 索引
395
+ ├── wait-strategy.md # 等待策略原理
396
+ ├── pitfalls.md # 踩坑案例
397
+ └── testing-strategy.md # 测试策略经验
398
+ ```
@@ -0,0 +1,38 @@
1
+ # 组件交互经验索引
2
+
3
+ 按组件名查找对应交互模式文件:
4
+
5
+ ## el-* 组件
6
+
7
+ | 组件 | 文件 |
8
+ |------|------|
9
+ | el-select | [patterns/el-select.md](./patterns/el-select.md) |
10
+ | el-cascader | [patterns/el-cascader.md](./patterns/el-cascader.md) |
11
+ | 日期选择器 | [patterns/date-picker.md](./patterns/date-picker.md) |
12
+ | el-message-box | [patterns/el-message-box.md](./patterns/el-message-box.md) |
13
+ | el-dialog / xd-dialog | [patterns/el-dialog.md](./patterns/el-dialog.md) |
14
+ | 抽屉关闭 | [patterns/drawer-close.md](./patterns/drawer-close.md) |
15
+
16
+ ## xd-* 组件
17
+
18
+ | 组件 | 文件 |
19
+ |------|------|
20
+ | 分类速查 | [patterns/xd-component-map.md](./patterns/xd-component-map.md) |
21
+ | xd-select-list | [patterns/xd-select-list.md](./patterns/xd-select-list.md) |
22
+ | xd-cascade-select | [patterns/xd-cascade-select.md](./patterns/xd-cascade-select.md) |
23
+ | xd-remote-cascader | [patterns/xd-remote-cascader.md](./patterns/xd-remote-cascader.md) |
24
+ | xd-remote-input | [patterns/xd-remote-input.md](./patterns/xd-remote-input.md) |
25
+ | xd-remote-select-list | [patterns/xd-remote-select-list.md](./patterns/xd-remote-select-list.md) |
26
+ | xd-textarea-tag | [patterns/xd-textarea-tag.md](./patterns/xd-textarea-tag.md) |
27
+ | xd-tinymce | [patterns/xd-tinymce.md](./patterns/xd-tinymce.md) |
28
+ | xd-wang-editor | [patterns/xd-wang-editor.md](./patterns/xd-wang-editor.md) |
29
+ | xd-radio-custom | [patterns/xd-radio-custom.md](./patterns/xd-radio-custom.md) |
30
+ | xd-upload | [patterns/xd-upload.md](./patterns/xd-upload.md) |
31
+ | xd-switch | [patterns/xd-switch.md](./patterns/xd-switch.md) |
32
+ | xd-group-input | [patterns/xd-group-input.md](./patterns/xd-group-input.md) |
33
+ | xd-region-path | [patterns/xd-region-path.md](./patterns/xd-region-path.md) |
34
+ | xd-input-range | [patterns/xd-input-range.md](./patterns/xd-input-range.md) |
35
+ | xd-table-sort / xd-table-witch | [patterns/xd-table-sort.md](./patterns/xd-table-sort.md) |
36
+ | xd-search | [patterns/xd-search.md](./patterns/xd-search.md) |
37
+ | xd-pagination-jfb | [patterns/xd-pagination-jfb.md](./patterns/xd-pagination-jfb.md) |
38
+ | xd-base-field | [patterns/xd-base-field.md](./patterns/xd-base-field.md) |
@@ -0,0 +1,91 @@
1
+ # 二级联动选择(异步加载)
2
+
3
+ - 现象:一级选项选择后,二级选项通过异步 API 加载,使用固定等待时间导致竞态条件
4
+ - 原因:选择一级选项后,接口返回和 DOM 重建需要时间,固定 `waitForTimeout` 无法准确等待
5
+ - 做法:使用 `waitForResponse` 监听异步接口返回,再等待固定时间确保 DOM 重建完成
6
+
7
+ ## 核心原则
8
+
9
+ 1. **Promise 注册时机**:必须在点击操作之前注册 `waitForResponse`,确保不会错过请求
10
+ 2. **循环内注册**:如果需要在循环中重试,promise 注册必须在循环内部
11
+ 3. **等待时间**:接口返回后额外等待 1s 确保 DOM 重建完成
12
+
13
+ ## 代码示例
14
+
15
+ ```typescript
16
+ async function selectSite(page: Page): Promise<boolean> {
17
+ // ... 前置检查 ...
18
+
19
+ // 遍历站点选项,选择有业务线的站点
20
+ for (let i = 0; i < count && i < 5; i++) {
21
+ // 关键:在点击之前注册 promise(确保不漏请求)
22
+ const businessLinePromise = page.waitForResponse(
23
+ r => r.url().includes(API.BUSINESS_LINE) && r.status() === 200,
24
+ { timeout: WAIT_API_TIMEOUT }
25
+ ).catch(() => null)
26
+
27
+ // 点击一级选项
28
+ await options.nth(i).click()
29
+ await page.locator('body').click({ position: { x: 10, y: 10 } })
30
+
31
+ // 等待异步接口返回
32
+ await businessLinePromise
33
+ // 等待 DOM 重建完成(1s)
34
+ await page.waitForTimeout(1000)
35
+
36
+ // 检查二级选项是否有数据
37
+ const secondLevelSelect = page.locator('.el-select').filter({
38
+ has: page.locator('input[placeholder="请选择业务线"]')
39
+ }).first()
40
+
41
+ if (await secondLevelSelect.count() > 0) {
42
+ await secondLevelSelect.click()
43
+ await page.waitForTimeout(WAIT_DEBOUNCE)
44
+ const secondLevelOptions = page.locator('.el-select-dropdown__item:visible')
45
+ const nsCount = await secondLevelOptions.count()
46
+ await page.keyboard.press('Escape')
47
+
48
+ if (nsCount > 0) {
49
+ console.log(`选择一级索引 ${i}(有 ${nsCount} 个二级选项)`)
50
+ return true
51
+ }
52
+ }
53
+
54
+ // 重新打开一级选择
55
+ await select.click()
56
+ await page.waitForTimeout(WAIT_DEBOUNCE)
57
+ }
58
+
59
+ return false
60
+ }
61
+ ```
62
+
63
+ ## 常见错误
64
+
65
+ ```typescript
66
+ // 错误:promise 在循环外注册,会错过后续请求
67
+ const businessLinePromise = page.waitForResponse(...)
68
+ for (let i = 0; i < count; i++) {
69
+ await options.nth(i).click() // 第二次循环时 promise 已经失效
70
+ await businessLinePromise
71
+ }
72
+
73
+ // 错误:使用固定等待时间,无法准确等待异步接口
74
+ await options.nth(i).click()
75
+ await page.waitForTimeout(WAIT_DEBOUNCE) // 500ms 可能不够
76
+
77
+ // 正确:循环内注册 + waitForResponse + 固定延迟
78
+ for (let i = 0; i < count; i++) {
79
+ const promise = page.waitForResponse(...)
80
+ await options.nth(i).click()
81
+ await promise
82
+ await page.waitForTimeout(1000) // DOM 重建
83
+ }
84
+ ```
85
+
86
+ ## 适用场景
87
+
88
+ - 站点 → 业务线
89
+ - 省份 → 城市
90
+ - 一级分类 → 二级分类
91
+ - 任何需要根据一级选择异步加载二级选项的场景
@@ -0,0 +1,86 @@
1
+ # 日期选择器
2
+
3
+ 所有日期选择器(`el-date-picker`、`xd-date`)统一操作规则。
4
+
5
+ ## 核心规则
6
+
7
+ | 规则 | 说明 |
8
+ |------|------|
9
+ | 禁止 fill | 所有类型禁止直接 `fill()` 日期值(date/datetime 不稳定,daterange 完全无效) |
10
+ | 必须 UI 操作 | 打开面板 → 选择日期 → 点确认 → 等面板关闭 |
11
+ | 日期随机选 | 日历面板上随机选日期,不固定第 1 个或最后 1 个 |
12
+ | 必须点确认 | 点「确定」关闭面板,不能用 Escape(面板不关闭会遮挡后续操作) |
13
+
14
+ ## 快捷选项(优先)
15
+
16
+ 搜索测试中优先使用快捷选项,更简单可靠:
17
+
18
+ ```typescript
19
+ const shortcuts = page.locator('.el-picker-panel__shortcut')
20
+ const shortcutCount = await shortcuts.count()
21
+ if (shortcutCount > 0) {
22
+ const idx = randomInt(0, shortcutCount - 1)
23
+ await shortcuts.nth(idx).click()
24
+ // 快捷选项通常自动确认
25
+ }
26
+ ```
27
+
28
+ ## 日历面板随机选
29
+
30
+ ### 单日期(date / datetime)
31
+
32
+ ```typescript
33
+ const dateInput = page.getByPlaceholder('选择日期').first()
34
+ await dateInput.click()
35
+ await page.waitForTimeout(WAIT_API_RENDER)
36
+
37
+ const pickerPanel = page.locator('.el-picker-panel').last()
38
+ await pickerPanel.waitFor({ state: 'visible', timeout: 5000 })
39
+
40
+ // 随机选一个可用日期
41
+ const availableDates = pickerPanel.locator('.el-date-table td.available')
42
+ const dateCount = await availableDates.count()
43
+ const randomIdx = randomInt(0, dateCount - 1)
44
+ console.log(`随机选择第 ${randomIdx + 1} 个日期(共 ${dateCount} 个)`)
45
+ await availableDates.nth(randomIdx).click()
46
+ await page.waitForTimeout(WAIT_DEBOUNCE)
47
+
48
+ // 点确认
49
+ await pickerPanel.getByRole('button', { name: '确定' }).click()
50
+ await page.waitForTimeout(WAIT_API_RENDER)
51
+ await pickerPanel.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => null)
52
+ ```
53
+
54
+ ### 日期区间(daterange / datetimerange)
55
+
56
+ ```typescript
57
+ const dateInput = page.getByPlaceholder('开始时间').first()
58
+ await dateInput.click()
59
+ await page.waitForTimeout(WAIT_API_RENDER)
60
+
61
+ const pickerPanel = page.locator('.el-picker-panel').last()
62
+ await pickerPanel.waitFor({ state: 'visible', timeout: 5000 })
63
+
64
+ // 随机选开始日期(前半段)和结束日期(后半段),确保区间有效
65
+ const availableDates = pickerPanel.locator('.el-date-table td.available')
66
+ const dateCount = await availableDates.count()
67
+ const startIdx = randomInt(0, Math.floor(dateCount / 2) - 1)
68
+ const endIdx = randomInt(Math.floor(dateCount / 2), dateCount - 1)
69
+ console.log(`随机区间:start=${startIdx + 1}, end=${endIdx + 1}(共 ${dateCount} 个日期)`)
70
+
71
+ await availableDates.nth(startIdx).click()
72
+ await page.waitForTimeout(WAIT_DEBOUNCE)
73
+ await availableDates.nth(endIdx).click()
74
+ await page.waitForTimeout(WAIT_DEBOUNCE)
75
+
76
+ // 点确认
77
+ await pickerPanel.getByRole('button', { name: '确定' }).click()
78
+ await page.waitForTimeout(WAIT_API_RENDER)
79
+ await pickerPanel.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => null)
80
+ ```
81
+
82
+ ## 踩坑
83
+
84
+ - `fill()` 对 daterange 无效,输入框显示为空,搜索不生效
85
+ - 选完日期必须点「确定」,直接 Escape 面板不会关闭,会遮挡搜索按钮
86
+ - 操作搜索按钮前,确保日期面板完全关闭(`waitFor({ state: 'hidden' })`)
@@ -0,0 +1,14 @@
1
+ # 抽屉关闭用 Escape
2
+
3
+ - 现象:点击抽屉遮罩层关闭后,偶尔触发意外页面跳转
4
+ - 原因:遮罩层点击可能穿透到底层元素,触发路由跳转或其他事件
5
+ - 做法:用 `page.keyboard.press('Escape')` 关闭抽屉/弹窗,更安全
6
+
7
+ ```typescript
8
+ // 关闭抽屉
9
+ await page.keyboard.press('Escape')
10
+ await page.waitForTimeout(WAIT_API_RENDER)
11
+
12
+ // 验证没有发生页面跳转
13
+ expect(page.url()).toContain(PAGE_PATH)
14
+ ```
@@ -0,0 +1,31 @@
1
+ # el-cascader 级联选择器
2
+
3
+ - 现象:checkStrictly 模式下点击节点文本不触发选中
4
+ - 原因:节点左侧有单选按钮(.el-radio__input),点击文本不会触发选中事件
5
+ - 做法:先检查是否有单选按钮,有则点击单选按钮,无则点击节点;懒加载子菜单需 waitFor 等待渲染。节点选择遵循 SKILL.md §9「目标随机」原则
6
+
7
+ ```typescript
8
+ const cascader = page.locator('.el-cascader').first()
9
+ await cascader.click()
10
+
11
+ const firstMenu = page.locator('.el-cascader-menu').first()
12
+ await firstMenu.waitFor({ state: 'visible', timeout: WAIT_API_TIMEOUT })
13
+
14
+ const firstItems = firstMenu.locator('.el-cascader-node')
15
+ const firstItem = firstItems.nth(randomIndex)
16
+
17
+ // 检查是否有单选按钮
18
+ const firstRadio = firstItem.locator('.el-radio__input, .el-cascader-node__prefix')
19
+ if (await firstRadio.count() > 0) {
20
+ await firstRadio.first().click()
21
+ } else {
22
+ await firstItem.click()
23
+ }
24
+
25
+ // 等待懒加载二级菜单
26
+ const secondMenu = page.locator('.el-cascader-menu').nth(1)
27
+ await secondMenu.waitFor({ state: 'visible', timeout: 3000 }).catch(() => null)
28
+
29
+ // 关闭级联弹出层
30
+ await page.keyboard.press('Escape')
31
+ ```