@kkeve/suite 0.0.1 → 0.0.2
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/README.md +53 -0
- package/dist/bin/keve.d.ts +13 -0
- package/dist/bin/keve.js +164 -0
- package/dist/commands/auth-update.d.ts +16 -0
- package/dist/commands/auth-update.js +88 -0
- package/dist/commands/clean.d.ts +10 -0
- package/dist/commands/clean.js +78 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +292 -0
- package/dist/commands/report.d.ts +12 -0
- package/dist/commands/report.js +159 -0
- package/dist/commands/run.d.ts +29 -0
- package/dist/commands/run.js +417 -0
- package/dist/commands/test.d.ts +12 -0
- package/dist/commands/test.js +102 -0
- package/dist/commands/vitest.d.ts +14 -0
- package/dist/commands/vitest.js +166 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +291 -0
- package/dist/core/action-log-writer.d.ts +47 -0
- package/dist/{tools/write-file.js → core/action-log-writer.js} +42 -26
- package/dist/core/auth.d.ts +17 -0
- package/dist/core/auth.js +224 -0
- package/dist/core/cdp-resolver.d.ts +5 -0
- package/dist/core/cdp-resolver.js +93 -0
- package/dist/core/generateReport/report.d.ts +19 -0
- package/dist/core/generateReport/report.js +91 -0
- package/dist/core/generateReport/reportData.d.ts +215 -0
- package/dist/core/generateReport/reportData.js +735 -0
- package/dist/core/global-setup.d.ts +6 -0
- package/dist/core/global-setup.js +113 -0
- package/dist/core/keve-aspect.d.ts +54 -0
- package/dist/core/keve-aspect.js +52 -0
- package/dist/core/keve-decorators.d.ts +51 -0
- package/dist/core/keve-decorators.js +95 -0
- package/dist/core/keve-registry.d.ts +38 -0
- package/dist/core/keve-registry.js +25 -0
- package/dist/core/keve-report.d.ts +27 -0
- package/dist/core/keve-report.js +357 -0
- package/dist/core/keve-test.d.ts +36 -0
- package/dist/core/keve-test.js +364 -0
- package/dist/core/learned-actions.d.ts +24 -0
- package/dist/core/learned-actions.js +65 -0
- package/dist/core/pw-config.d.ts +19 -0
- package/dist/core/pw-config.js +180 -0
- package/dist/index.d.ts +8 -23
- package/dist/index.js +36 -43
- package/dist/page-agent/agent.d.ts +129 -0
- package/dist/page-agent/agent.js +585 -0
- package/dist/page-agent/hooks.d.ts +13 -0
- package/dist/page-agent/hooks.js +218 -0
- package/dist/page-agent/tools.d.ts +29 -0
- package/dist/page-agent/tools.js +297 -0
- package/doc/README.md +124 -0
- package/doc/agents/keve-dev-test-agent.md +140 -0
- package/doc/agents/keve-test-agent.md +178 -0
- package/doc/asserts//346/265/213/350/257/225/345/220/204/351/230/266/346/256/265/350/257/264/346/230/216.png +0 -0
- package/doc/keve-agent-demo.html +370 -0
- package/doc/skills/keve-bddcase-gen/SKILL.md +332 -0
- package/doc/skills/keve-bddcase-gen/references/test-case-rules.md +91 -0
- package/doc/skills/keve-bddcase-gen/references/test-cases-example.md +103 -0
- package/doc/skills/keve-component-gen/SKILL.md +255 -0
- package/doc/skills/keve-component-gen/references/component-mock-strategy.md +74 -0
- package/doc/skills/keve-component-gen/references/component-model-template.md +98 -0
- package/doc/skills/keve-component-gen/references/component-test-template.md +100 -0
- package/doc/skills/keve-component-gen/references/vue-test-utils-guide.md +91 -0
- package/doc/skills/keve-e2e-gen/SKILL.md +179 -0
- package/doc/skills/keve-e2e-gen/references/code-analysis-guide.md +208 -0
- package/doc/skills/keve-e2e-gen/references/decisions.md +36 -0
- package/doc/skills/keve-e2e-gen/references/pom-generation-guide.md +140 -0
- package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +236 -0
- package/doc/skills/keve-e2e-gen/references/test-auditor-guide.md +189 -0
- package/doc/skills/keve-e2e-gen/scripts/extract-components.sh +85 -0
- package/doc/skills/keve-e2e-gen/scripts/inject-testids.sh +95 -0
- package/doc/skills/keve-static-analysis/SKILL.md +362 -0
- package/doc/skills/keve-static-analysis/references/codegraph-analysis-guide.md +108 -0
- package/doc/skills/keve-static-analysis/references/git-impact-analysis.md +81 -0
- package/doc/skills/keve-static-analysis/references/lint-fix-guide.md +69 -0
- package/doc/skills/keve-static-analysis/references/runtime-risk-patterns.md +92 -0
- package/doc/skills/keve-static-analysis/references/security-scan-guide.md +32 -0
- package/doc/skills/keve-static-analysis/references/ts-error-patterns.md +98 -0
- package/doc/skills/keve-static-analysis/references/vue-security-guide.md +72 -0
- package/doc/skills/keve-test-eval/SKILL.md +78 -0
- package/doc/skills/keve-test-eval/references/common-fix-guide.md +166 -0
- package/doc/skills/keve-test-eval/references/confidence-data-guide.md +78 -0
- package/doc/skills/keve-test-eval/references/fix-loop-guide.md +67 -0
- package/doc/skills/keve-unit-gen/SKILL.md +203 -0
- package/doc/skills/keve-unit-gen/references/boundary-conditions.md +78 -0
- package/doc/skills/keve-unit-gen/references/unit-mock-strategy.md +71 -0
- package/doc/skills/keve-unit-gen/references/unit-test-template.md +105 -0
- package/package.json +47 -25
- package/dist/agent/factory.d.ts +0 -25
- package/dist/agent/factory.js +0 -99
- package/dist/agent/index.d.ts +0 -3
- package/dist/agent/index.js +0 -7
- package/dist/agent/loader.d.ts +0 -22
- package/dist/agent/loader.js +0 -117
- package/dist/agent/types.d.ts +0 -9
- package/dist/agent/types.js +0 -2
- package/dist/agent-loop/complete-task.d.ts +0 -28
- package/dist/agent-loop/complete-task.js +0 -70
- package/dist/agent-loop/index.d.ts +0 -16
- package/dist/agent-loop/index.js +0 -26
- package/dist/agent-loop/loop.d.ts +0 -42
- package/dist/agent-loop/loop.js +0 -282
- package/dist/agent-loop/runtime.d.ts +0 -40
- package/dist/agent-loop/runtime.js +0 -163
- package/dist/agent-loop/types.d.ts +0 -161
- package/dist/agent-loop/types.js +0 -105
- package/dist/context/index.d.ts +0 -8
- package/dist/context/index.js +0 -29
- package/dist/context/tokenCalculation.d.ts +0 -55
- package/dist/context/tokenCalculation.js +0 -95
- package/dist/context/truncation.d.ts +0 -67
- package/dist/context/truncation.js +0 -142
- package/dist/context/types.d.ts +0 -7
- package/dist/context/types.js +0 -27
- package/dist/llm/OpenAIClient.d.ts +0 -31
- package/dist/llm/OpenAIClient.js +0 -455
- package/dist/llm/constants.d.ts +0 -2
- package/dist/llm/constants.js +0 -6
- package/dist/llm/errors.d.ts +0 -25
- package/dist/llm/errors.js +0 -51
- package/dist/llm/index.d.ts +0 -49
- package/dist/llm/index.js +0 -158
- package/dist/llm/types.d.ts +0 -124
- package/dist/llm/types.js +0 -2
- package/dist/llm/utils.d.ts +0 -29
- package/dist/llm/utils.js +0 -284
- package/dist/skill/frontmatter-parser.d.ts +0 -50
- package/dist/skill/frontmatter-parser.js +0 -152
- package/dist/skill/index.d.ts +0 -12
- package/dist/skill/index.js +0 -17
- package/dist/skill/loader.d.ts +0 -96
- package/dist/skill/loader.js +0 -256
- package/dist/tools/ask-user.d.ts +0 -8
- package/dist/tools/ask-user.js +0 -47
- package/dist/tools/bash.d.ts +0 -47
- package/dist/tools/bash.js +0 -213
- package/dist/tools/edit-file.d.ts +0 -9
- package/dist/tools/edit-file.js +0 -76
- package/dist/tools/glob.d.ts +0 -8
- package/dist/tools/glob.js +0 -101
- package/dist/tools/grep.d.ts +0 -10
- package/dist/tools/grep.js +0 -76
- package/dist/tools/index.d.ts +0 -24
- package/dist/tools/index.js +0 -47
- package/dist/tools/read-files.d.ts +0 -8
- package/dist/tools/read-files.js +0 -81
- package/dist/tools/write-file.d.ts +0 -8
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keve-component-gen
|
|
3
|
+
description: 从 Vue 组件源码自动生成组件行为模型和 Vitest 组件测试。分析组件 Props/emit/Slots/条件渲染,生成 Component Model(场景 + 断言)和测试脚本,执行验证后输出覆盖索引。
|
|
4
|
+
allowed-tools: Bash(shell:*) Bash(npx:*) Bash(npm:*) Bash(node:*) Bash(git:*) Bash(pnpm:*) Bash(yarn:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# keve-component-gen
|
|
8
|
+
|
|
9
|
+
## 定位
|
|
10
|
+
|
|
11
|
+
**本技能负责**:从 Vue 组件源码自动生成组件行为模型和组件测试(不负责单元测试、E2E 测试)。
|
|
12
|
+
|
|
13
|
+
组件测试覆盖测试金字塔中层:Vue 组件的 Props、事件、条件渲染、Slots。
|
|
14
|
+
运行环境为 Node.js + jsdom,不需要真实浏览器,秒级执行。
|
|
15
|
+
|
|
16
|
+
***
|
|
17
|
+
|
|
18
|
+
## 触发条件
|
|
19
|
+
|
|
20
|
+
| 触发方式 | 场景 | 说明 |
|
|
21
|
+
|----------|------|------|
|
|
22
|
+
| `use_skill("keve-component-gen")` | 开发阶段测试流水线 | 由 keve-dev-test-agent 调度 |
|
|
23
|
+
| `/keve-component-gen` 指令 | 用户主动要求生成组件测试 | 手动触发 |
|
|
24
|
+
|
|
25
|
+
***
|
|
26
|
+
|
|
27
|
+
## 输入
|
|
28
|
+
|
|
29
|
+
- 项目组件源码(由 Step 1 自动扫描识别,`.vue` SFC 文件)
|
|
30
|
+
- 可选:已有的 `keve_test_spec/component/registry.json`
|
|
31
|
+
|
|
32
|
+
**排除**:
|
|
33
|
+
- 页面级组件(pages/、views/、layouts/ 目录下的组件,由 E2E 覆盖)
|
|
34
|
+
- 纯布局组件(无交互、无 Props、无逻辑)
|
|
35
|
+
- 第三方组件库组件
|
|
36
|
+
- 已在 `coverage.yaml` 中标记 100% 覆盖的组件
|
|
37
|
+
|
|
38
|
+
***
|
|
39
|
+
|
|
40
|
+
## 流程
|
|
41
|
+
|
|
42
|
+
### Step 1: 扫描组件文件
|
|
43
|
+
|
|
44
|
+
**分析项目结构**,自动识别包含 `.vue` SFC 文件的目录,不硬编码路径:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
扫描策略:
|
|
48
|
+
1. 读取项目 src/ 目录结构
|
|
49
|
+
2. 识别包含 .vue 文件的目录
|
|
50
|
+
3. 过滤条件:
|
|
51
|
+
- **只扫描 components/ 目录下的组件**(页面级组件由 E2E 覆盖)
|
|
52
|
+
- 排除: pages/、views/、layouts/ 目录(页面级,依赖路由+API,组件测试 ROI 低)
|
|
53
|
+
- 排除: 纯布局组件(无 script 或 script 只有 name)
|
|
54
|
+
- 排除: 第三方组件库组件(node_modules 下的)
|
|
55
|
+
- 排除: 已在 coverage.yaml 中 coverage=100% 的组件
|
|
56
|
+
|
|
57
|
+
判断标准:
|
|
58
|
+
- 组件能独立 mount(只依赖 Props + stub 子组件)→ 适合组件测试
|
|
59
|
+
- 组件依赖路由/API/页面级生命周期 → 不适合,由 E2E 覆盖
|
|
60
|
+
|
|
61
|
+
常见组件目录(仅供参考,不硬编码):
|
|
62
|
+
- src/components/
|
|
63
|
+
- 项目可能有自定义目录名,需逐一分析
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Step 2: 分析组件 API
|
|
67
|
+
|
|
68
|
+
对每个组件分析:
|
|
69
|
+
|
|
70
|
+
| 分析维度 | 提取来源 | 用途 |
|
|
71
|
+
|---------|---------|------|
|
|
72
|
+
| Props 定义 | `defineProps` / `props` 选项 | 场景 Props + 默认值 |
|
|
73
|
+
| emit 事件 | `defineEmits` / `emits` 选项 | 断言 emitted |
|
|
74
|
+
| Slots | `$slots` / `<slot>` 标签 | 断言插槽渲染 |
|
|
75
|
+
| 条件渲染 | `v-if` / `v-show` | true/false 分支各 1 test |
|
|
76
|
+
| 计算属性 | `computed` | 断言派生值 |
|
|
77
|
+
| 事件处理器 | `@click` / `@input` 等 | trigger + 断言 |
|
|
78
|
+
|
|
79
|
+
### Step 3: 生成 Component Model
|
|
80
|
+
|
|
81
|
+
**生成路径**:`keve_test_spec/component/models/{ComponentName}.model.ts`
|
|
82
|
+
|
|
83
|
+
**model.ts 结构**(纯数据,不依赖 Storybook,D14):
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// keve_test_spec/component/models/SearchFilter.model.ts
|
|
87
|
+
|
|
88
|
+
import SearchFilter from '@/components/SearchFilter.vue';
|
|
89
|
+
|
|
90
|
+
export const SearchFilterModel = {
|
|
91
|
+
// ─── 组件标识 ───
|
|
92
|
+
name: 'SearchFilter',
|
|
93
|
+
source: 'src/components/SearchFilter.vue',
|
|
94
|
+
component: SearchFilter,
|
|
95
|
+
|
|
96
|
+
// ─── 组件 API ───
|
|
97
|
+
events: ['update:modelValue'],
|
|
98
|
+
slots: [],
|
|
99
|
+
|
|
100
|
+
// ─── 组件场景 ───
|
|
101
|
+
scenarios: [
|
|
102
|
+
{
|
|
103
|
+
id: 'SF-DEFAULT',
|
|
104
|
+
name: '默认状态',
|
|
105
|
+
description: '显示三个选项,当前选中"全部"',
|
|
106
|
+
props: { modelValue: 'all', disabled: false, options: ['全部', '我创建的', '可编辑的'] },
|
|
107
|
+
mountConfig: { globalStubs: ['RouterLink'] },
|
|
108
|
+
assertions: [
|
|
109
|
+
{ type: 'visible', target: '三个筛选按钮' },
|
|
110
|
+
{ type: 'checked', target: '"全部"按钮' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'SF-DISABLED',
|
|
115
|
+
name: '禁用状态',
|
|
116
|
+
description: '所有按钮 disabled,不响应点击',
|
|
117
|
+
props: { modelValue: 'all', disabled: true, options: ['全部', '我创建的', '可编辑的'] },
|
|
118
|
+
mountConfig: { globalStubs: ['RouterLink'] },
|
|
119
|
+
assertions: [
|
|
120
|
+
{ type: 'disabled', target: '所有按钮' },
|
|
121
|
+
{ type: 'no-emit', event: 'update:modelValue' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**场景 ID 命名规则**:`{组件缩写}-{场景描述}`(如 `SF-DEFAULT`)
|
|
129
|
+
|
|
130
|
+
### Step 4: 生成组件测试脚本
|
|
131
|
+
|
|
132
|
+
**生成路径**:`keve_test_spec/component/__test__/{ComponentName}.test.ts`
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// keve_test_spec/component/__test__/SearchFilter.test.ts
|
|
136
|
+
|
|
137
|
+
import { describe, it, expect } from 'vitest';
|
|
138
|
+
import { mount } from '@vue/test-utils';
|
|
139
|
+
import SearchFilter from '@/components/SearchFilter.vue';
|
|
140
|
+
import { SearchFilterModel } from '@/keve_test_spec/component/models/SearchFilter.model';
|
|
141
|
+
|
|
142
|
+
function mountFromScenario(scenarioId: string, overrides: Record<string, unknown> = {}) {
|
|
143
|
+
const scenario = SearchFilterModel.scenarios.find(s => s.id === scenarioId)!;
|
|
144
|
+
return mount(SearchFilter, {
|
|
145
|
+
props: { ...scenario.props, ...overrides },
|
|
146
|
+
global: {
|
|
147
|
+
stubs: Object.fromEntries(scenario.mountConfig.globalStubs.map(n => [n, true])),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
describe('SearchFilter', () => {
|
|
153
|
+
it('SF-DEFAULT: 默认状态', async () => {
|
|
154
|
+
const wrapper = mountFromScenario('SF-DEFAULT');
|
|
155
|
+
expect(wrapper.findAll('button')).toHaveLength(3);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('SF-DISABLED: 禁用状态', async () => {
|
|
159
|
+
const wrapper = mountFromScenario('SF-DISABLED');
|
|
160
|
+
await wrapper.find('button').trigger('click');
|
|
161
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Step 5: Mock 策略
|
|
167
|
+
|
|
168
|
+
| 场景 | Mock 方式 |
|
|
169
|
+
|------|----------|
|
|
170
|
+
| 子组件 | `global.stubs: { ChildComponent: true }` |
|
|
171
|
+
| RouterLink | `global.stubs: { RouterLink: true }` |
|
|
172
|
+
| Pinia Store | `global.plugins: [createTestingPinia()]` |
|
|
173
|
+
| API 调用 | `vi.mock('@/api/xxx')` |
|
|
174
|
+
|
|
175
|
+
**原则**:使用 `stubChildComponent`(通过 `global.stubs`)避免级联渲染,
|
|
176
|
+
只在 model 的 `mountConfig.globalStubs` 中声明需要 stub 的组件名。
|
|
177
|
+
|
|
178
|
+
### Step 6: 执行验证
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npx vitest run keve_test_spec/component/__test__/SearchFilter.test.ts
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- 如果通过 → 继续
|
|
185
|
+
- 如果失败 → AI 分析错误,修复测试(≤3 轮)
|
|
186
|
+
- 超过 3 轮仍失败 → 用 `it.todo` 标记
|
|
187
|
+
|
|
188
|
+
### Step 7: 生成覆盖索引
|
|
189
|
+
|
|
190
|
+
生成 `keve_test_spec/component/coverage.yaml`:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
generated_at: 2026-06-16T16:08:00
|
|
194
|
+
total_components: 20
|
|
195
|
+
tested_components: 12
|
|
196
|
+
untested_components: 8
|
|
197
|
+
|
|
198
|
+
components:
|
|
199
|
+
- source: src/components/SearchFilter.vue
|
|
200
|
+
model: component/models/SearchFilter.model.ts
|
|
201
|
+
test: component/__test__/SearchFilter.test.ts
|
|
202
|
+
scenarios_total: 2
|
|
203
|
+
scenarios_tested: 2
|
|
204
|
+
coverage: 100%
|
|
205
|
+
missing: []
|
|
206
|
+
|
|
207
|
+
- source: src/components/ToolManagement.vue
|
|
208
|
+
model: null
|
|
209
|
+
test: null
|
|
210
|
+
missing: [全部场景]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
***
|
|
214
|
+
|
|
215
|
+
## 产出
|
|
216
|
+
|
|
217
|
+
| 产出 | 路径 |
|
|
218
|
+
|------|------|
|
|
219
|
+
| 组件行为模型 | `keve_test_spec/component/models/*.model.ts` |
|
|
220
|
+
| 组件测试脚本 | `keve_test_spec/component/__test__/*.test.ts` |
|
|
221
|
+
| 覆盖索引 | `keve_test_spec/component/coverage.yaml` |
|
|
222
|
+
|
|
223
|
+
***
|
|
224
|
+
|
|
225
|
+
## 硬性约束
|
|
226
|
+
|
|
227
|
+
- **不打标**(不需要 `data-e2e-name`,D07)
|
|
228
|
+
- **不生成 POM**(组件测试不需要页面操作模型,D07)
|
|
229
|
+
- **不生成 .stories.ts**(延后,D14)
|
|
230
|
+
- **model.ts 保持纯数据**(不依赖 Storybook,D14)
|
|
231
|
+
- **test.ts 通过 model 的 scenarios 获取 mount 配置**
|
|
232
|
+
- **model 和 test 放在 `keve_test_spec/component/`**(长期资产,D12)
|
|
233
|
+
- **每轮修复后重新执行验证**
|
|
234
|
+
- **失败用例用 `it.todo` 标记**
|
|
235
|
+
|
|
236
|
+
***
|
|
237
|
+
|
|
238
|
+
## Key Decisions
|
|
239
|
+
|
|
240
|
+
| ID | 决策 | 说明 |
|
|
241
|
+
|----|------|------|
|
|
242
|
+
| CG01 | 先生成 model 再生成 test | model 是唯一数据源,test 引用 model |
|
|
243
|
+
| CG02 | model.ts 保持纯数据 | 不依赖 Storybook,后续可按需扩展(D14) |
|
|
244
|
+
| CG03 | 不打标、不 POM | jsdom 中直接 mount,不需要浏览器定位(D07) |
|
|
245
|
+
| CG04 | 最多 3 轮修复 | 组件测试比单测复杂,允许更多轮次 |
|
|
246
|
+
| CG05 | 覆盖索引从 model 生成 | 场景信息已含在 model 中,直接提取 |
|
|
247
|
+
|
|
248
|
+
***
|
|
249
|
+
|
|
250
|
+
## References
|
|
251
|
+
|
|
252
|
+
- [component-model-template.md](references/component-model-template.md) — model.ts 结构和字段说明
|
|
253
|
+
- [component-test-template.md](references/component-test-template.md) — .test.ts 生成模板
|
|
254
|
+
- [component-mock-strategy.md](references/component-mock-strategy.md) — stub + vi.mock 策略
|
|
255
|
+
- [vue-test-utils-guide.md](references/vue-test-utils-guide.md) — @vue/test-utils API 速查
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 组件测试 Mock 策略
|
|
2
|
+
|
|
3
|
+
> AI 生成组件测试时的 stub/mock 指南
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 优先级
|
|
8
|
+
|
|
9
|
+
| 优先级 | 方式 | 适用场景 |
|
|
10
|
+
|--------|------|---------|
|
|
11
|
+
| 1 | `global.stubs` | 子组件、RouterLink、RouterView |
|
|
12
|
+
| 2 | `createTestingPinia` | Pinia Store |
|
|
13
|
+
| 3 | `vi.mock` | API 调用、工具函数 |
|
|
14
|
+
| 4 | 直接传 Props | 不需要 mock |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 子组件 Stub
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// 方式1:通过 mountConfig.globalStubs(在 model 中声明)
|
|
22
|
+
mount(Component, {
|
|
23
|
+
global: {
|
|
24
|
+
stubs: { RouterLink: true, ChildComponent: true },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 方式2:stub 所有子组件(粗暴,不推荐)
|
|
29
|
+
mount(Component, {
|
|
30
|
+
global: {
|
|
31
|
+
stubs: { stubChildComponent: true },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**原则**:只 stub 有副作用的子组件(API 调用、路由跳转),不 stub 纯展示组件。
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Pinia Store Mock
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { createTestingPinia } from '@pinia/testing';
|
|
44
|
+
|
|
45
|
+
mount(Component, {
|
|
46
|
+
global: {
|
|
47
|
+
plugins: [createTestingPinia({ stubActions: false })],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**原则**:默认 `stubActions: false`,保留 Store 逻辑;只在 Store 有 API 调用时 `stubActions: true`。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## API Mock
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
vi.mock('@/api/agent', () => ({
|
|
60
|
+
getAgentList: vi.fn().mockResolvedValue({ list: [], total: 0 }),
|
|
61
|
+
getAgentDetail: vi.fn().mockResolvedValue({ id: 1, name: 'Test Agent' }),
|
|
62
|
+
}));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 禁止做法
|
|
68
|
+
|
|
69
|
+
| 禁止 | 原因 |
|
|
70
|
+
|------|------|
|
|
71
|
+
| Stub 被测组件本身 | 失去测试意义 |
|
|
72
|
+
| Mock Vue 内置组件 | `Transition`、`Teleport` 不需要 mock |
|
|
73
|
+
| `shallowMount` 替代 `mount` | 会跳过子组件渲染,断言不完整 |
|
|
74
|
+
| 在 `global.stubs` 中 stub 所有 | 过度 stub 导致测试无意义 |
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Component Model 模板
|
|
2
|
+
|
|
3
|
+
> AI 生成 component model 时的参考
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本结构
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import ComponentName from '@/components/ComponentName.vue';
|
|
11
|
+
|
|
12
|
+
export const ComponentNameModel = {
|
|
13
|
+
name: 'ComponentName',
|
|
14
|
+
source: 'src/components/ComponentName.vue',
|
|
15
|
+
component: ComponentName,
|
|
16
|
+
|
|
17
|
+
events: ['update:modelValue', 'confirm', 'cancel'],
|
|
18
|
+
slots: ['default', 'footer'],
|
|
19
|
+
|
|
20
|
+
scenarios: [
|
|
21
|
+
{
|
|
22
|
+
id: 'CN-DEFAULT',
|
|
23
|
+
name: '默认状态',
|
|
24
|
+
description: '组件在默认 Props 下的渲染',
|
|
25
|
+
props: { /* default prop values */ },
|
|
26
|
+
mountConfig: {
|
|
27
|
+
globalStubs: ['RouterLink'],
|
|
28
|
+
},
|
|
29
|
+
assertions: [
|
|
30
|
+
{ type: 'visible', target: '组件根元素' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 场景设计规则
|
|
40
|
+
|
|
41
|
+
### Props 变化场景
|
|
42
|
+
|
|
43
|
+
每个有意义的 Props 变化 → 独立场景:
|
|
44
|
+
|
|
45
|
+
| Props | 场景 ID | 场景名 |
|
|
46
|
+
|-------|---------|--------|
|
|
47
|
+
| 默认值 | `CN-DEFAULT` | 默认状态 |
|
|
48
|
+
| disabled: true | `CN-DISABLED` | 禁用状态 |
|
|
49
|
+
| loading: true | `CN-LOADING` | 加载中 |
|
|
50
|
+
| empty: [] | `CN-EMPTY` | 空数据 |
|
|
51
|
+
|
|
52
|
+
### 条件渲染场景
|
|
53
|
+
|
|
54
|
+
每个 `v-if` → true/false 两个场景:
|
|
55
|
+
|
|
56
|
+
| 条件 | 场景 ID | 场景名 |
|
|
57
|
+
|------|---------|--------|
|
|
58
|
+
| `v-if="detail"` 为 true | `CN-WITH-DETAIL` | 有详情 |
|
|
59
|
+
| `v-if="detail"` 为 false | `CN-NO-DETAIL` | 无详情 |
|
|
60
|
+
|
|
61
|
+
### 交互场景
|
|
62
|
+
|
|
63
|
+
每个用户交互 → 独立场景:
|
|
64
|
+
|
|
65
|
+
| 交互 | 场景 ID | 场景名 |
|
|
66
|
+
|------|---------|--------|
|
|
67
|
+
| 点击确认按钮 | `CN-CONFIRM` | 确认操作 |
|
|
68
|
+
| 点击取消按钮 | `CN-CANCEL` | 取消操作 |
|
|
69
|
+
| 输入文本 | `CN-INPUT` | 输入内容 |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 场景 ID 命名规则
|
|
74
|
+
|
|
75
|
+
格式:`{组件缩写}-{场景描述}`(大写英文 + 连字符)
|
|
76
|
+
|
|
77
|
+
示例:
|
|
78
|
+
- `SF-DEFAULT` — SearchFilter 默认
|
|
79
|
+
- `SF-DISABLED` — SearchFilter 禁用
|
|
80
|
+
- `SF-EMPTY` — SearchFilter 空选项
|
|
81
|
+
- `SF-FILTER-MINE` — SearchFilter 切换到我创建的
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 断言类型速查
|
|
86
|
+
|
|
87
|
+
| type | 用途 | 必填字段 |
|
|
88
|
+
|------|------|---------|
|
|
89
|
+
| `visible` | 元素可见 | target |
|
|
90
|
+
| `hidden` | 元素隐藏 | target |
|
|
91
|
+
| `disabled` | 元素禁用 | target |
|
|
92
|
+
| `enabled` | 元素可用 | target |
|
|
93
|
+
| `emitted` | 事件触发 | event |
|
|
94
|
+
| `no-emit` | 事件未触发 | event |
|
|
95
|
+
| `text` | 文本内容 | target, value |
|
|
96
|
+
| `value` | 输入值 | target, value |
|
|
97
|
+
| `class` | CSS 类 | target, value |
|
|
98
|
+
| `attribute` | HTML 属性 | target, value |
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# 组件测试生成模板
|
|
2
|
+
|
|
3
|
+
> AI 生成组件测试脚本时的参考
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本结构
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
+
import { mount } from '@vue/test-utils';
|
|
12
|
+
import ComponentName from '@/components/ComponentName.vue';
|
|
13
|
+
import { ComponentNameModel } from '@/keve_test_spec/component/models/ComponentName.model';
|
|
14
|
+
|
|
15
|
+
function mountFromScenario(scenarioId: string, overrides: Record<string, unknown> = {}) {
|
|
16
|
+
const scenario = ComponentNameModel.scenarios.find(s => s.id === scenarioId)!;
|
|
17
|
+
return mount(ComponentName, {
|
|
18
|
+
props: { ...scenario.props, ...overrides },
|
|
19
|
+
global: {
|
|
20
|
+
stubs: Object.fromEntries(
|
|
21
|
+
scenario.mountConfig.globalStubs.map(n => [n, true])
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('ComponentName', () => {
|
|
28
|
+
it('CN-DEFAULT: 默认状态', async () => {
|
|
29
|
+
const wrapper = mountFromScenario('CN-DEFAULT');
|
|
30
|
+
// 断言...
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 常见测试模式
|
|
38
|
+
|
|
39
|
+
### 默认渲染
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
it('CN-DEFAULT: 默认状态', async () => {
|
|
43
|
+
const wrapper = mountFromScenario('CN-DEFAULT');
|
|
44
|
+
expect(wrapper.find('.component-root').exists()).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Props 变更
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
it('CN-DISABLED: 禁用状态', async () => {
|
|
52
|
+
const wrapper = mountFromScenario('CN-DISABLED');
|
|
53
|
+
expect(wrapper.find('button').attributes('disabled')).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 事件触发
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
it('CN-CONFIRM: 点击确认', async () => {
|
|
61
|
+
const wrapper = mountFromScenario('CN-DEFAULT');
|
|
62
|
+
await wrapper.find('[data-test="confirm-btn"]').trigger('click');
|
|
63
|
+
expect(wrapper.emitted('confirm')).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 条件渲染
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
it('CN-WITH-DETAIL: 有详情', async () => {
|
|
71
|
+
const wrapper = mountFromScenario('CN-WITH-DETAIL');
|
|
72
|
+
expect(wrapper.find('.detail-section').exists()).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('CN-NO-DETAIL: 无详情', async () => {
|
|
76
|
+
const wrapper = mountFromScenario('CN-NO-DETAIL');
|
|
77
|
+
expect(wrapper.find('.detail-section').exists()).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Slots
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
it('CN-SLOT-FOOTER: 底部插槽', async () => {
|
|
85
|
+
const wrapper = mount(ComponentName, {
|
|
86
|
+
props: ComponentNameModel.scenarios[0].props,
|
|
87
|
+
slots: { footer: '<div class="custom-footer">Footer</div>' },
|
|
88
|
+
global: { stubs: { RouterLink: true } },
|
|
89
|
+
});
|
|
90
|
+
expect(wrapper.find('.custom-footer').exists()).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 失败用例处理
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
it.todo('CN-LOADING: 加载中状态');
|
|
100
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @vue/test-utils API 速查
|
|
2
|
+
|
|
3
|
+
> AI 生成组件测试时的 API 参考
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## mount / shallowMount
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { mount } from '@vue/test-utils';
|
|
11
|
+
|
|
12
|
+
const wrapper = mount(Component, {
|
|
13
|
+
props: { modelValue: 'test' },
|
|
14
|
+
attrs: { id: 'my-component' },
|
|
15
|
+
slots: { default: '<div>Slot content</div>' },
|
|
16
|
+
global: {
|
|
17
|
+
plugins: [router, pinia],
|
|
18
|
+
stubs: { RouterLink: true },
|
|
19
|
+
mocks: { $t: (key: string) => key },
|
|
20
|
+
provide: { theme: 'dark' },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Wrapper 方法
|
|
28
|
+
|
|
29
|
+
### 查询
|
|
30
|
+
|
|
31
|
+
| 方法 | 说明 |
|
|
32
|
+
|------|------|
|
|
33
|
+
| `wrapper.find(selector)` | 查找单个元素 |
|
|
34
|
+
| `wrapper.findAll(selector)` | 查找所有元素 |
|
|
35
|
+
| `wrapper.findComponent(Component)` | 查找子组件 |
|
|
36
|
+
| `wrapper.get(selector)` | 查找元素(不存在则抛错) |
|
|
37
|
+
| `wrapper.vm` | 组件实例 |
|
|
38
|
+
|
|
39
|
+
### 交互
|
|
40
|
+
|
|
41
|
+
| 方法 | 说明 |
|
|
42
|
+
|------|------|
|
|
43
|
+
| `wrapper.trigger('click')` | 触发 DOM 事件 |
|
|
44
|
+
| `wrapper.trigger('click', { button: 0 })` | 带参数的事件 |
|
|
45
|
+
| `wrapper.setValue('new value')` | 设置表单值 |
|
|
46
|
+
| `wrapper.setChecked(true)` | 设置 checkbox |
|
|
47
|
+
| `wrapper.setSelected('option1')` | 设置 select |
|
|
48
|
+
|
|
49
|
+
### 断言
|
|
50
|
+
|
|
51
|
+
| 方法 | 说明 |
|
|
52
|
+
|------|------|
|
|
53
|
+
| `wrapper.exists()` | 元素是否存在 |
|
|
54
|
+
| `wrapper.isVisible()` | 元素是否可见(v-show) |
|
|
55
|
+
| `wrapper.text()` | 元素文本内容 |
|
|
56
|
+
| `wrapper.html()` | 元素 HTML |
|
|
57
|
+
| `wrapper.classes()` | 元素 CSS 类列表 |
|
|
58
|
+
| `wrapper.attributes('href')` | 元素属性值 |
|
|
59
|
+
| `wrapper.emitted()` | 所有触发的事件 |
|
|
60
|
+
| `wrapper.emitted('update')` | 特定事件的参数列表 |
|
|
61
|
+
|
|
62
|
+
### 状态操作
|
|
63
|
+
|
|
64
|
+
| 方法 | 说明 |
|
|
65
|
+
|------|------|
|
|
66
|
+
| `wrapper.setProps({ key: value })` | 更新 Props |
|
|
67
|
+
| `wrapper.setData({ key: value })` | 更新 data(Options API) |
|
|
68
|
+
| `await wrapper.vm.$nextTick()` | 等待下次渲染 |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 常见模式
|
|
73
|
+
|
|
74
|
+
### 等待异步渲染
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
await wrapper.setProps({ loading: false });
|
|
78
|
+
await wrapper.vm.$nextTick();
|
|
79
|
+
// 或
|
|
80
|
+
await flushPromises();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 测试 v-model
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const wrapper = mount(Component, {
|
|
87
|
+
props: { modelValue: 'initial', 'onUpdate:modelValue': (e: any) => wrapper.setProps({ modelValue: e }) },
|
|
88
|
+
});
|
|
89
|
+
await wrapper.find('input').setValue('new value');
|
|
90
|
+
expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
|
|
91
|
+
```
|