@epoint-testtech/ep-stage-skill 0.0.10 → 0.5.0-alpha.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/SKILL.md +27 -27
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -223
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -275
- package/codex-skill/ep-stage/glue-generate-testcase/examples/observable-testcase.json +40 -40
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +95 -95
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +273 -296
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -30
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -132
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -44
- package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
- package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +127 -166
- package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +95 -98
- package/dist/src/cli/generate-testcase.d.ts +0 -1
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +1 -6
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +14 -38
- package/dist/src/cli/run.d.ts +0 -2
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +21 -70
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +0 -9
- package/dist/src/contracts/glue-contract-v1.d.ts +0 -35
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +1 -34
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.js +0 -1
- package/dist/src/extractors/html-page.d.ts +0 -9
- package/dist/src/extractors/html-page.d.ts.map +1 -1
- package/dist/src/extractors/html-page.js +2 -26
- package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
- package/dist/src/generators/stage-skeleton-script.d.ts +0 -50
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +116 -449
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -1
- package/dist/src/runtime/ai-runtime-trace.js +0 -8
- package/dist/src/testcase/contract-testcase-assembler.d.ts +0 -28
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
- package/dist/src/testcase/contract-testcase-assembler.js +75 -329
- package/dist/src/validation/contract.d.ts.map +1 -1
- package/dist/src/validation/contract.js +0 -29
- package/dist/test/cli/generate-testcase-json5.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-json5.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-json5.test.js +154 -0
- package/dist/test/cli/index.test.d.ts +2 -0
- package/dist/test/cli/index.test.d.ts.map +1 -0
- package/dist/test/cli/index.test.js +114 -0
- package/dist/test/cli/run.test.d.ts +2 -0
- package/dist/test/cli/run.test.d.ts.map +1 -0
- package/dist/test/cli/run.test.js +429 -0
- package/dist/test/cli/spec.test.d.ts +2 -0
- package/dist/test/cli/spec.test.d.ts.map +1 -0
- package/dist/test/cli/spec.test.js +112 -0
- package/dist/test/code-list-json5.test.d.ts +2 -0
- package/dist/test/code-list-json5.test.d.ts.map +1 -0
- package/dist/test/code-list-json5.test.js +84 -0
- package/dist/test/code-list.test.d.ts +2 -0
- package/dist/test/code-list.test.d.ts.map +1 -0
- package/dist/test/code-list.test.js +39 -0
- package/dist/test/gap-inference.test.d.ts +2 -0
- package/dist/test/gap-inference.test.d.ts.map +1 -0
- package/dist/test/gap-inference.test.js +597 -0
- package/dist/test/generalization.test.d.ts +2 -0
- package/dist/test/generalization.test.d.ts.map +1 -0
- package/dist/test/generalization.test.js +73 -0
- package/dist/test/material-inventory.test.d.ts +2 -0
- package/dist/test/material-inventory.test.d.ts.map +1 -0
- package/dist/test/material-inventory.test.js +155 -0
- package/dist/test/observable-chain.test.d.ts +2 -0
- package/dist/test/observable-chain.test.d.ts.map +1 -0
- package/dist/test/observable-chain.test.js +123 -0
- package/dist/test/page-structure.test.d.ts +2 -0
- package/dist/test/page-structure.test.d.ts.map +1 -0
- package/dist/test/page-structure.test.js +45 -0
- package/dist/test/runtime/ai-runtime-trace.test.d.ts +2 -0
- package/dist/test/runtime/ai-runtime-trace.test.d.ts.map +1 -0
- package/dist/test/runtime/ai-runtime-trace.test.js +78 -0
- package/dist/test/scenario-inference.test.d.ts +2 -0
- package/dist/test/scenario-inference.test.d.ts.map +1 -0
- package/dist/test/scenario-inference.test.js +73 -0
- package/dist/test/stage-context.test.d.ts +2 -0
- package/dist/test/stage-context.test.d.ts.map +1 -0
- package/dist/test/stage-context.test.js +395 -0
- package/dist/test/testcase/testcase-markdown.test.d.ts +2 -0
- package/dist/test/testcase/testcase-markdown.test.d.ts.map +1 -0
- package/dist/test/testcase/testcase-markdown.test.js +48 -0
- package/dist/test/testcase-generator.test.d.ts +2 -0
- package/dist/test/testcase-generator.test.d.ts.map +1 -0
- package/dist/test/testcase-generator.test.js +276 -0
- package/dist/test/testcase-skeleton.test.d.ts +2 -0
- package/dist/test/testcase-skeleton.test.d.ts.map +1 -0
- package/dist/test/testcase-skeleton.test.js +185 -0
- package/dist/test/testcase-spec-assembly.test.d.ts +2 -0
- package/dist/test/testcase-spec-assembly.test.d.ts.map +1 -0
- package/dist/test/testcase-spec-assembly.test.js +105 -0
- package/dist/test/util/credentials.test.d.ts +2 -0
- package/dist/test/util/credentials.test.d.ts.map +1 -0
- package/dist/test/util/credentials.test.js +64 -0
- package/dist/test/util/i18n-testcase.test.d.ts +2 -0
- package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
- package/dist/test/util/i18n-testcase.test.js +119 -0
- package/dist/test/util/softlink.test.d.ts +2 -0
- package/dist/test/util/softlink.test.d.ts.map +1 -0
- package/dist/test/util/softlink.test.js +82 -0
- package/dist/test/validation/contract.test.d.ts +2 -0
- package/dist/test/validation/contract.test.d.ts.map +1 -0
- package/dist/test/validation/contract.test.js +116 -0
- package/dist/test/validation/credentials.test.d.ts +2 -0
- package/dist/test/validation/credentials.test.d.ts.map +1 -0
- package/dist/test/validation/credentials.test.js +74 -0
- package/dist/test/validation/projects-index.test.d.ts +2 -0
- package/dist/test/validation/projects-index.test.d.ts.map +1 -0
- package/dist/test/validation/projects-index.test.js +48 -0
- package/dist/test/validation/testcase-v1-draft.test.d.ts +2 -0
- package/dist/test/validation/testcase-v1-draft.test.d.ts.map +1 -0
- package/dist/test/validation/testcase-v1-draft.test.js +173 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +7 -0
- package/docs/README.md +124 -124
- package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
- package/examples/schemeresource.module-hints.json +38 -38
- package/examples/schemeresource.observable.code_list.md +37 -37
- package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
- package/examples/zwplace-placecategory-validation.json +29 -29
- package/examples/zwplace.module-hints.json +69 -69
- package/examples/zwplace.observable.code_list.md +37 -37
- package/package.json +19 -13
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +0 -72
- package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +0 -233
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +0 -91
- package/codex-skill/ep-stage/page-element-scan/evals/evals.json +0 -37
- package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +0 -85
- package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +0 -153
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +0 -233
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +0 -298
- package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +0 -749
- package/dist/src/cli/probe.d.ts +0 -46
- package/dist/src/cli/probe.d.ts.map +0 -1
- package/dist/src/cli/probe.js +0 -447
- package/dist/src/cli/run-report.d.ts +0 -46
- package/dist/src/cli/run-report.d.ts.map +0 -1
- package/dist/src/cli/run-report.js +0 -331
- package/dist/src/extractors/prd-extractor.d.ts +0 -70
- package/dist/src/extractors/prd-extractor.d.ts.map +0 -1
- package/dist/src/extractors/prd-extractor.js +0 -248
- package/dist/src/testcase/button-interaction-inference.d.ts +0 -37
- package/dist/src/testcase/button-interaction-inference.d.ts.map +0 -1
- package/dist/src/testcase/button-interaction-inference.js +0 -385
- package/dist/src/testcase/prd-button-mapper.d.ts +0 -72
- package/dist/src/testcase/prd-button-mapper.d.ts.map +0 -1
- package/dist/src/testcase/prd-button-mapper.js +0 -148
- package/dist/src/testcase/testcase-v2.d.ts +0 -50
- package/dist/src/testcase/testcase-v2.d.ts.map +0 -1
- package/dist/src/testcase/testcase-v2.js +0 -1
- package/docs/mvp-usage-guide.md +0 -298
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateGlueTestcases } from '../src/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* 构造用例生成器测试所需的最小 CRUD 契约。
|
|
5
|
+
*
|
|
6
|
+
* @returns 包含完整 CRUD 流程槽位的测试契约。
|
|
7
|
+
*/
|
|
8
|
+
function createCrudContract() {
|
|
9
|
+
return {
|
|
10
|
+
contractVersion: 'crud-business-module/v1',
|
|
11
|
+
module: {
|
|
12
|
+
id: 'zwplace',
|
|
13
|
+
label: '场所窗口信息管理',
|
|
14
|
+
},
|
|
15
|
+
pages: {
|
|
16
|
+
list: {
|
|
17
|
+
role: 'list',
|
|
18
|
+
pageId: 'gxhzwplacelist',
|
|
19
|
+
title: '场所窗口信息列表',
|
|
20
|
+
iframeSrcKeyword: 'gxhzwplacelist',
|
|
21
|
+
fields: [],
|
|
22
|
+
buttons: [],
|
|
23
|
+
grids: [],
|
|
24
|
+
dialogs: [],
|
|
25
|
+
sources: [],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
dataKey: {
|
|
29
|
+
field: 'windowname',
|
|
30
|
+
label: '窗口名称',
|
|
31
|
+
availability: 'derivable',
|
|
32
|
+
generationStrategy: 'timestamp_prefix',
|
|
33
|
+
sources: [],
|
|
34
|
+
},
|
|
35
|
+
searchConditions: [
|
|
36
|
+
{
|
|
37
|
+
field: 'windowname',
|
|
38
|
+
label: '窗口名称',
|
|
39
|
+
component: 'input',
|
|
40
|
+
seedValue: '自动化测试窗口',
|
|
41
|
+
generationStrategy: 'timestamp_prefix',
|
|
42
|
+
sources: [],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
flows: {
|
|
46
|
+
create: {
|
|
47
|
+
entryButton: {
|
|
48
|
+
pageRole: 'list',
|
|
49
|
+
label: '新增场所窗口',
|
|
50
|
+
sources: [],
|
|
51
|
+
},
|
|
52
|
+
targetPageId: 'gxhzwplaceadd',
|
|
53
|
+
saveButton: {
|
|
54
|
+
pageRole: 'add',
|
|
55
|
+
label: '保存并关闭',
|
|
56
|
+
sources: [],
|
|
57
|
+
},
|
|
58
|
+
overrideFields: [],
|
|
59
|
+
},
|
|
60
|
+
search: {
|
|
61
|
+
searchField: {
|
|
62
|
+
pageRole: 'list',
|
|
63
|
+
field: 'windowname',
|
|
64
|
+
label: '窗口名称',
|
|
65
|
+
sources: [],
|
|
66
|
+
},
|
|
67
|
+
submitControl: {
|
|
68
|
+
pageRole: 'list',
|
|
69
|
+
label: '查询',
|
|
70
|
+
sources: [],
|
|
71
|
+
},
|
|
72
|
+
resultGrid: 'datagrid',
|
|
73
|
+
},
|
|
74
|
+
update: {
|
|
75
|
+
entryAction: {
|
|
76
|
+
pageRole: 'list',
|
|
77
|
+
label: '修改',
|
|
78
|
+
sources: [],
|
|
79
|
+
},
|
|
80
|
+
targetPageId: 'gxhzwplaceedit',
|
|
81
|
+
updateField: {
|
|
82
|
+
pageRole: 'edit',
|
|
83
|
+
field: 'windowname',
|
|
84
|
+
label: '窗口名称',
|
|
85
|
+
sources: [],
|
|
86
|
+
},
|
|
87
|
+
saveButton: {
|
|
88
|
+
pageRole: 'edit',
|
|
89
|
+
label: '保存并关闭',
|
|
90
|
+
sources: [],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
delete: {
|
|
94
|
+
entryButton: {
|
|
95
|
+
pageRole: 'list',
|
|
96
|
+
label: '删除选定',
|
|
97
|
+
sources: [],
|
|
98
|
+
},
|
|
99
|
+
expectedOutcome: 'success_delete',
|
|
100
|
+
confirmControl: {
|
|
101
|
+
pageRole: 'list',
|
|
102
|
+
label: '确定',
|
|
103
|
+
sources: [],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
assertions: [],
|
|
108
|
+
businessRules: [],
|
|
109
|
+
unresolvedSlots: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 构造用例生成器测试所需的 coverage diff trace。
|
|
114
|
+
*
|
|
115
|
+
* @returns 默认无未确认项的 coverage diff。
|
|
116
|
+
*/
|
|
117
|
+
function createCoverageDiff() {
|
|
118
|
+
return {
|
|
119
|
+
moduleId: 'zwplace',
|
|
120
|
+
skeletonCoverageId: 'crud-plus-export/v1',
|
|
121
|
+
covered: [
|
|
122
|
+
{
|
|
123
|
+
actionId: 'zwplace.list.create',
|
|
124
|
+
label: '新增场所信息管理',
|
|
125
|
+
pageId: 'gxhzwplacelist',
|
|
126
|
+
status: 'covered',
|
|
127
|
+
reason: '动作标签已被当前骨架能力覆盖。',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
partiallyCovered: [],
|
|
131
|
+
gapCandidates: [],
|
|
132
|
+
unresolved: [],
|
|
133
|
+
nestedCrudCandidates: [],
|
|
134
|
+
baseline: { plannedWorkflows: [], candidateWorkflows: [], unresolvedWorkflows: [] },
|
|
135
|
+
comparison: { both: [], onlyBaseline: [], onlyGeneralized: [] },
|
|
136
|
+
reasoningSummary: {
|
|
137
|
+
conclusion: 'CRUD 单页能力已覆盖。',
|
|
138
|
+
evidenceChain: [],
|
|
139
|
+
alternatives: ['crud.single-page'],
|
|
140
|
+
confidence: 'medium',
|
|
141
|
+
risks: [],
|
|
142
|
+
needsHumanReview: false,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 胶水测试用例生成器测试。
|
|
148
|
+
*
|
|
149
|
+
* 验证 generateGlueTestcases 能从同一份 CRUD contract、已确认场景与 coverage diff
|
|
150
|
+
* 生成可审阅 JSON + Markdown。
|
|
151
|
+
*/
|
|
152
|
+
describe('glue testcase generator', () => {
|
|
153
|
+
it('renders reviewable markdown and json cases from a confirmed scenario and contract', () => {
|
|
154
|
+
const output = generateGlueTestcases({
|
|
155
|
+
contract: createCrudContract(),
|
|
156
|
+
menu: '场所窗口信息管理>场所窗口信息列表',
|
|
157
|
+
scenarios: ['crud.single-page', 'crud.nested'],
|
|
158
|
+
confirmedScenarios: ['crud.single-page'],
|
|
159
|
+
coverageDiff: createCoverageDiff(),
|
|
160
|
+
});
|
|
161
|
+
expect(output.json.cases).toHaveLength(4);
|
|
162
|
+
expect(output.json.cases.map((item) => item.caseId)).toEqual([
|
|
163
|
+
'zwplace.create',
|
|
164
|
+
'zwplace.read',
|
|
165
|
+
'zwplace.update',
|
|
166
|
+
'zwplace.delete',
|
|
167
|
+
]);
|
|
168
|
+
expect(output.json.cases[0]).toMatchObject({
|
|
169
|
+
caseId: 'zwplace.create',
|
|
170
|
+
reviewStatus: 'confirmed',
|
|
171
|
+
});
|
|
172
|
+
expect(output.json.cases[0]?.evidence).toContain('skeleton=crud.skeleton-testcase/v1');
|
|
173
|
+
expect(output.markdown).toContain('| TC-CRUD-001 |');
|
|
174
|
+
expect(output.markdown).toContain('查询字段:窗口名称');
|
|
175
|
+
expect(output.markdown).not.toContain('{{');
|
|
176
|
+
});
|
|
177
|
+
it('requires contract and menu instead of free-form module fields', () => {
|
|
178
|
+
expect(() => generateGlueTestcases({
|
|
179
|
+
moduleId: 'zwplace',
|
|
180
|
+
moduleName: '场所窗口信息管理',
|
|
181
|
+
scenarioStatus: 'confirmed',
|
|
182
|
+
scenarios: ['crud.single-page'],
|
|
183
|
+
confirmedScenarios: ['crud.single-page'],
|
|
184
|
+
coverageDiff: createCoverageDiff(),
|
|
185
|
+
})).toThrow('生成胶水测试用例必须提供 contract 和 menu');
|
|
186
|
+
});
|
|
187
|
+
it('keeps testcase gate in needs_review when coverage diff has unresolved items', () => {
|
|
188
|
+
const coverageDiff = createCoverageDiff();
|
|
189
|
+
coverageDiff.unresolved = [
|
|
190
|
+
{
|
|
191
|
+
actionId: 'zwplace.list.export',
|
|
192
|
+
label: '导出',
|
|
193
|
+
pageId: 'gxhzwplacelist',
|
|
194
|
+
status: 'unresolved',
|
|
195
|
+
reason: '导出动作是否纳入本轮 CRUD 用例需要人工确认。',
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
const output = generateGlueTestcases({
|
|
199
|
+
contract: createCrudContract(),
|
|
200
|
+
menu: '场所窗口信息管理>场所窗口信息列表',
|
|
201
|
+
scenarios: ['crud.single-page'],
|
|
202
|
+
confirmedScenarios: ['crud.single-page'],
|
|
203
|
+
coverageDiff,
|
|
204
|
+
});
|
|
205
|
+
expect(output.json.cases.every((item) => item.reviewStatus === 'needs_review')).toBe(true);
|
|
206
|
+
expect(output.json.cases[0]?.unresolved).toEqual([
|
|
207
|
+
'coverageDiff.unresolved: 导出动作是否纳入本轮 CRUD 用例需要人工确认。',
|
|
208
|
+
]);
|
|
209
|
+
expect(output.json.reasoningSummary.risks).toContain('coverageDiff.unresolved: 导出动作是否纳入本轮 CRUD 用例需要人工确认。');
|
|
210
|
+
expect(output.json.reasoningSummary.evidenceChain).toContainEqual({
|
|
211
|
+
source: 'human',
|
|
212
|
+
path: 'testcase-generation.json',
|
|
213
|
+
text: 'unresolved=coverageDiff.unresolved: 导出动作是否纳入本轮 CRUD 用例需要人工确认。',
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
it('renders nested CRUD candidates with the CRUD testcase skeleton and unresolved gate', () => {
|
|
217
|
+
const coverageDiff = createCoverageDiff();
|
|
218
|
+
coverageDiff.nestedCrudCandidates = [
|
|
219
|
+
{
|
|
220
|
+
pageId: 'gxhzwplaceadd',
|
|
221
|
+
evidence: ['弹窗页面中存在 mini-datagrid', '按钮 添加窗口'],
|
|
222
|
+
supportedActions: ['create', 'delete'],
|
|
223
|
+
unresolvedReason: '子表保存后断言需要人工确认',
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
const output = generateGlueTestcases({
|
|
227
|
+
contract: createCrudContract(),
|
|
228
|
+
menu: '场所窗口信息管理>场所窗口信息列表',
|
|
229
|
+
scenarios: ['crud.nested'],
|
|
230
|
+
confirmedScenarios: ['crud.nested'],
|
|
231
|
+
coverageDiff,
|
|
232
|
+
});
|
|
233
|
+
expect(output.json.cases[0]).toMatchObject({
|
|
234
|
+
scenario: 'crud.nested',
|
|
235
|
+
reviewStatus: 'needs_review',
|
|
236
|
+
unresolved: ['coverageDiff.nestedCrudCandidates[gxhzwplaceadd]: 子表保存后断言需要人工确认'],
|
|
237
|
+
});
|
|
238
|
+
expect(output.json.cases[0]?.evidence).toContain('skeleton=crud.skeleton-testcase/v1');
|
|
239
|
+
expect(output.markdown).toContain('| TC-CRUD-001 |');
|
|
240
|
+
expect(output.markdown).not.toContain('{{');
|
|
241
|
+
});
|
|
242
|
+
it('does not let nested CRUD candidates block a confirmed crud.single-page scenario', () => {
|
|
243
|
+
const coverageDiff = createCoverageDiff();
|
|
244
|
+
coverageDiff.nestedCrudCandidates = [
|
|
245
|
+
{
|
|
246
|
+
pageId: 'gxhzwplaceadd',
|
|
247
|
+
evidence: ['弹窗页面中存在 mini-datagrid', '按钮 添加窗口'],
|
|
248
|
+
supportedActions: ['create', 'delete'],
|
|
249
|
+
unresolvedReason: '子表保存后断言需要人工确认',
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
const output = generateGlueTestcases({
|
|
253
|
+
contract: createCrudContract(),
|
|
254
|
+
menu: '场所窗口信息管理>场所窗口信息列表',
|
|
255
|
+
scenarios: ['crud.single-page', 'crud.nested'],
|
|
256
|
+
confirmedScenarios: ['crud.single-page'],
|
|
257
|
+
coverageDiff,
|
|
258
|
+
});
|
|
259
|
+
expect(output.json.cases.every((item) => item.reviewStatus === 'confirmed')).toBe(true);
|
|
260
|
+
expect(output.json.cases[0]?.unresolved).toEqual([]);
|
|
261
|
+
expect(output.json.reasoningSummary.needsHumanReview).toBe(false);
|
|
262
|
+
expect(output.json.reasoningSummary.risks).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
it('surfaces inferred menu candidates in testcase unresolved output for human review', () => {
|
|
265
|
+
const output = generateGlueTestcases({
|
|
266
|
+
contract: createCrudContract(),
|
|
267
|
+
menu: '未确认菜单路径',
|
|
268
|
+
menuReviewHint: 'menu.candidate: 场所窗口信息管理>场所窗口信息列表 | 场所窗口信息列表',
|
|
269
|
+
scenarios: ['crud.single-page'],
|
|
270
|
+
confirmedScenarios: ['crud.single-page'],
|
|
271
|
+
coverageDiff: createCoverageDiff(),
|
|
272
|
+
});
|
|
273
|
+
expect(output.json.cases[0]?.unresolved).toContain('menu.candidate: 场所窗口信息管理>场所窗口信息列表 | 场所窗口信息列表');
|
|
274
|
+
expect(output.markdown).toContain('menu.candidate: 场所窗口信息管理>场所窗口信息列表 | 场所窗口信息列表');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testcase-skeleton.test.d.ts","sourceRoot":"","sources":["../../test/testcase-skeleton.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mapContractToCrudTestcaseSlots, renderGlueTestcaseSkeleton, selectTestcaseSkeletonByScenario, } from '../src/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* 构造用例骨架映射测试所需的最小 CRUD 契约。
|
|
5
|
+
*
|
|
6
|
+
* @returns 包含完整 CRUD 槽位来源的测试契约。
|
|
7
|
+
*/
|
|
8
|
+
function createCrudContract() {
|
|
9
|
+
return {
|
|
10
|
+
contractVersion: 'crud-business-module/v1',
|
|
11
|
+
module: {
|
|
12
|
+
id: 'zwplace',
|
|
13
|
+
label: '场所窗口信息管理',
|
|
14
|
+
},
|
|
15
|
+
pages: {
|
|
16
|
+
list: {
|
|
17
|
+
role: 'list',
|
|
18
|
+
pageId: 'gxhzwplacelist',
|
|
19
|
+
title: '场所窗口信息列表',
|
|
20
|
+
iframeSrcKeyword: 'gxhzwplacelist',
|
|
21
|
+
fields: [],
|
|
22
|
+
buttons: [],
|
|
23
|
+
grids: [],
|
|
24
|
+
dialogs: [],
|
|
25
|
+
sources: [],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
dataKey: {
|
|
29
|
+
field: 'windowname',
|
|
30
|
+
label: '窗口名称',
|
|
31
|
+
availability: 'derivable',
|
|
32
|
+
generationStrategy: 'timestamp_prefix',
|
|
33
|
+
sources: [],
|
|
34
|
+
},
|
|
35
|
+
searchConditions: [
|
|
36
|
+
{
|
|
37
|
+
field: 'windowname',
|
|
38
|
+
label: '窗口名称',
|
|
39
|
+
component: 'input',
|
|
40
|
+
seedValue: '自动化测试窗口',
|
|
41
|
+
generationStrategy: 'timestamp_prefix',
|
|
42
|
+
sources: [],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
flows: {
|
|
46
|
+
create: {
|
|
47
|
+
entryButton: {
|
|
48
|
+
pageRole: 'list',
|
|
49
|
+
label: '新增场所窗口',
|
|
50
|
+
sources: [],
|
|
51
|
+
},
|
|
52
|
+
targetPageId: 'gxhzwplaceadd',
|
|
53
|
+
saveButton: {
|
|
54
|
+
pageRole: 'add',
|
|
55
|
+
label: '保存并关闭',
|
|
56
|
+
sources: [],
|
|
57
|
+
},
|
|
58
|
+
overrideFields: [],
|
|
59
|
+
},
|
|
60
|
+
search: {
|
|
61
|
+
searchField: {
|
|
62
|
+
pageRole: 'list',
|
|
63
|
+
field: 'windowname',
|
|
64
|
+
label: '窗口名称',
|
|
65
|
+
sources: [],
|
|
66
|
+
},
|
|
67
|
+
submitControl: {
|
|
68
|
+
pageRole: 'list',
|
|
69
|
+
label: '查询',
|
|
70
|
+
sources: [],
|
|
71
|
+
},
|
|
72
|
+
resultGrid: 'datagrid',
|
|
73
|
+
},
|
|
74
|
+
update: {
|
|
75
|
+
entryAction: {
|
|
76
|
+
pageRole: 'list',
|
|
77
|
+
label: '修改',
|
|
78
|
+
sources: [],
|
|
79
|
+
},
|
|
80
|
+
targetPageId: 'gxhzwplaceedit',
|
|
81
|
+
updateField: {
|
|
82
|
+
pageRole: 'edit',
|
|
83
|
+
field: 'windowname',
|
|
84
|
+
label: '窗口名称',
|
|
85
|
+
sources: [],
|
|
86
|
+
},
|
|
87
|
+
saveButton: {
|
|
88
|
+
pageRole: 'edit',
|
|
89
|
+
label: '保存并关闭',
|
|
90
|
+
sources: [],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
delete: {
|
|
94
|
+
entryButton: {
|
|
95
|
+
pageRole: 'list',
|
|
96
|
+
label: '删除选定',
|
|
97
|
+
sources: [],
|
|
98
|
+
},
|
|
99
|
+
expectedOutcome: 'success_delete',
|
|
100
|
+
confirmControl: {
|
|
101
|
+
pageRole: 'list',
|
|
102
|
+
label: '确定',
|
|
103
|
+
sources: [],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
assertions: [],
|
|
108
|
+
businessRules: [],
|
|
109
|
+
unresolvedSlots: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
describe('glue testcase skeleton', () => {
|
|
113
|
+
it('selects the CRUD testcase skeleton for a single-page CRUD scenario', () => {
|
|
114
|
+
expect(selectTestcaseSkeletonByScenario('crud.single-page')).toMatchObject({
|
|
115
|
+
skeletonId: 'crud.skeleton-testcase/v1',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
it('selects the same CRUD testcase skeleton for a nested CRUD pilot scenario', () => {
|
|
119
|
+
expect(selectTestcaseSkeletonByScenario('crud.nested')).toMatchObject({
|
|
120
|
+
skeletonId: 'crud.skeleton-testcase/v1',
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('maps a CRUD contract to testcase slots without guessing unresolved business values', () => {
|
|
124
|
+
const slots = mapContractToCrudTestcaseSlots({
|
|
125
|
+
contract: createCrudContract(),
|
|
126
|
+
scenario: 'crud.single-page',
|
|
127
|
+
menu: '场所窗口信息管理>场所窗口信息列表',
|
|
128
|
+
});
|
|
129
|
+
expect(slots).toMatchObject({
|
|
130
|
+
moduleId: 'zwplace',
|
|
131
|
+
moduleName: '场所窗口信息管理',
|
|
132
|
+
listPageTitle: '场所窗口信息列表',
|
|
133
|
+
createActionLabel: '新增场所窗口',
|
|
134
|
+
searchFieldLabel: '窗口名称',
|
|
135
|
+
envBaseUrl: '.env: LOGIN_SYSTEM_URL',
|
|
136
|
+
envUsername: '.env: LOGIN_USERNAME',
|
|
137
|
+
envPassword: '.env: LOGIN_PASSWORD',
|
|
138
|
+
unresolved: [],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
it('treats unresolved menu placeholders as unresolved instead of resolved slot sources', () => {
|
|
142
|
+
const slots = mapContractToCrudTestcaseSlots({
|
|
143
|
+
contract: createCrudContract(),
|
|
144
|
+
scenario: 'crud.single-page',
|
|
145
|
+
menu: '未确认菜单路径',
|
|
146
|
+
});
|
|
147
|
+
expect(slots.reviewStatus).toBe('needs_review');
|
|
148
|
+
expect(slots.menu).toBe('待人工确认');
|
|
149
|
+
expect(slots.unresolved).toContain('menu: 缺少 input.menu');
|
|
150
|
+
expect(slots.slotSources).not.toContain('menu=input.menu');
|
|
151
|
+
});
|
|
152
|
+
it('records missing contract slots as unresolved without guessing business values', () => {
|
|
153
|
+
const contract = createCrudContract();
|
|
154
|
+
delete contract.flows.create;
|
|
155
|
+
const slots = mapContractToCrudTestcaseSlots({
|
|
156
|
+
contract,
|
|
157
|
+
scenario: 'crud.single-page',
|
|
158
|
+
menu: '场所窗口信息管理>场所窗口信息列表',
|
|
159
|
+
});
|
|
160
|
+
expect(slots.reviewStatus).toBe('needs_review');
|
|
161
|
+
expect(slots.createActionLabel).toBe('待人工确认');
|
|
162
|
+
expect(slots.unresolved).toContain('createActionLabel: 缺少 contract.flows.create.entryButton.label');
|
|
163
|
+
});
|
|
164
|
+
it('renders the testcase skeleton template from slots', () => {
|
|
165
|
+
const template = [
|
|
166
|
+
'# {{moduleName}}',
|
|
167
|
+
'',
|
|
168
|
+
'## {{createCaseTitle}}',
|
|
169
|
+
'',
|
|
170
|
+
'- 列表页:{{listPageTitle}}',
|
|
171
|
+
'- 步骤:{{createSteps}}',
|
|
172
|
+
'- 查询字段:{{searchFieldLabel}}',
|
|
173
|
+
].join('\n');
|
|
174
|
+
const markdown = renderGlueTestcaseSkeleton(template, mapContractToCrudTestcaseSlots({
|
|
175
|
+
contract: createCrudContract(),
|
|
176
|
+
scenario: 'crud.single-page',
|
|
177
|
+
menu: '场所窗口信息管理>场所窗口信息列表',
|
|
178
|
+
}));
|
|
179
|
+
expect(markdown).toContain('# 场所窗口信息管理');
|
|
180
|
+
expect(markdown).toContain('## 新增场所窗口信息管理');
|
|
181
|
+
expect(markdown).toContain('列表页:场所窗口信息列表');
|
|
182
|
+
expect(markdown).toContain('查询字段:窗口名称');
|
|
183
|
+
expect(markdown).not.toContain('{{');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testcase-spec-assembly.test.d.ts","sourceRoot":"","sources":["../../test/testcase-spec-assembly.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateGlueTestcaseDocumentForSpecAssembly } from '../src/testcase/testcase-spec-assembly.js';
|
|
3
|
+
/**
|
|
4
|
+
* 构造 spec assembly 校验所需的最小契约。
|
|
5
|
+
*
|
|
6
|
+
* @returns CRUD 业务模块契约的模块标识部分。
|
|
7
|
+
*/
|
|
8
|
+
function createContract() {
|
|
9
|
+
return {
|
|
10
|
+
contractVersion: 'crud-business-module/v1',
|
|
11
|
+
module: {
|
|
12
|
+
id: 'zwplace',
|
|
13
|
+
label: '场所窗口信息管理',
|
|
14
|
+
},
|
|
15
|
+
pages: {
|
|
16
|
+
list: {
|
|
17
|
+
role: 'list',
|
|
18
|
+
pageId: 'gxhzwplacelist',
|
|
19
|
+
title: '场所窗口信息列表',
|
|
20
|
+
iframeSrcKeyword: 'gxhzwplacelist',
|
|
21
|
+
fields: [],
|
|
22
|
+
buttons: [],
|
|
23
|
+
grids: [],
|
|
24
|
+
dialogs: [],
|
|
25
|
+
sources: [],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
dataKey: {
|
|
29
|
+
field: 'windowname',
|
|
30
|
+
label: '窗口名称',
|
|
31
|
+
availability: 'derivable',
|
|
32
|
+
generationStrategy: 'timestamp_prefix',
|
|
33
|
+
sources: [],
|
|
34
|
+
},
|
|
35
|
+
flows: {},
|
|
36
|
+
assertions: [],
|
|
37
|
+
businessRules: [],
|
|
38
|
+
unresolvedSlots: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 构造已确认的 CRUD 四工作流用例文档。
|
|
43
|
+
*
|
|
44
|
+
* @returns 胶水测试用例文档。
|
|
45
|
+
*/
|
|
46
|
+
function createConfirmedTestcaseDocument() {
|
|
47
|
+
return {
|
|
48
|
+
moduleId: 'zwplace',
|
|
49
|
+
moduleName: '场所窗口信息管理',
|
|
50
|
+
cases: [
|
|
51
|
+
createCase('zwplace.create', 'Create:新增场所窗口信息管理'),
|
|
52
|
+
createCase('zwplace.read', 'Read:搜索场所窗口信息管理'),
|
|
53
|
+
createCase('zwplace.update', 'Update:修改场所窗口信息管理'),
|
|
54
|
+
createCase('zwplace.delete', 'Delete:删除场所窗口信息管理'),
|
|
55
|
+
],
|
|
56
|
+
reasoningSummary: {
|
|
57
|
+
conclusion: '已确认 CRUD 用例。',
|
|
58
|
+
evidenceChain: [],
|
|
59
|
+
alternatives: [],
|
|
60
|
+
confidence: 'high',
|
|
61
|
+
risks: [],
|
|
62
|
+
needsHumanReview: false,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 构造单条已确认用例。
|
|
68
|
+
*
|
|
69
|
+
* @param caseId 用例 ID。
|
|
70
|
+
* @param title 用例标题。
|
|
71
|
+
* @returns 胶水测试用例。
|
|
72
|
+
*/
|
|
73
|
+
function createCase(caseId, title) {
|
|
74
|
+
return {
|
|
75
|
+
caseId,
|
|
76
|
+
title,
|
|
77
|
+
scenario: 'crud.single-page',
|
|
78
|
+
reviewStatus: 'confirmed',
|
|
79
|
+
evidence: [],
|
|
80
|
+
steps: ['执行动作'],
|
|
81
|
+
assertions: ['验证结果'],
|
|
82
|
+
unresolved: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
describe('testcase spec assembly contract', () => {
|
|
86
|
+
it('accepts confirmed testcase documents that match the contract CRUD workflow granularity', () => {
|
|
87
|
+
expect(() => validateGlueTestcaseDocumentForSpecAssembly(createConfirmedTestcaseDocument(), createContract())).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
it('rejects testcase documents that still need human review', () => {
|
|
90
|
+
const document = createConfirmedTestcaseDocument();
|
|
91
|
+
document.cases[1].reviewStatus = 'needs_review';
|
|
92
|
+
document.cases[1].unresolved = ['menu: 缺少 input.menu'];
|
|
93
|
+
expect(() => validateGlueTestcaseDocumentForSpecAssembly(document, createContract())).toThrow('glue testcase 仍有未确认用例');
|
|
94
|
+
});
|
|
95
|
+
it('rejects testcase documents that do not match the contract module', () => {
|
|
96
|
+
const document = createConfirmedTestcaseDocument();
|
|
97
|
+
document.moduleId = 'other-module';
|
|
98
|
+
expect(() => validateGlueTestcaseDocumentForSpecAssembly(document, createContract())).toThrow('glue testcase moduleId 与 contract 不一致');
|
|
99
|
+
});
|
|
100
|
+
it('rejects testcase documents missing CRUD workflow cases required by the generated spec', () => {
|
|
101
|
+
const document = createConfirmedTestcaseDocument();
|
|
102
|
+
document.cases = document.cases.filter((item) => item.caseId !== 'zwplace.update');
|
|
103
|
+
expect(() => validateGlueTestcaseDocumentForSpecAssembly(document, createContract())).toThrow('glue testcase 缺少 spec assembly 需要的用例');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.test.d.ts","sourceRoot":"","sources":["../../../test/util/credentials.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { loadCredentials } from '../../src/util/credentials.js';
|
|
6
|
+
/**
|
|
7
|
+
* REQ-ENV-02 / REQ-ENV-03:从 <需求名>/credentials.json5 加载数组形态凭据。
|
|
8
|
+
*
|
|
9
|
+
* 不重复实现 schema 校验(已由 validateCredentials 覆盖),本测试关注 IO 行为:
|
|
10
|
+
* 文件读取、JSON5 解析、对校验器的组合调用,以及缺失场景的友好错误。
|
|
11
|
+
*/
|
|
12
|
+
describe('loadCredentials', () => {
|
|
13
|
+
let tmpDir;
|
|
14
|
+
let credPath;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = mkdtempSync(path.join(os.tmpdir(), 'credentials-test-'));
|
|
17
|
+
credPath = path.join(tmpDir, 'credentials.json5');
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
it('读合法数组形态凭据返回 Credential[]', () => {
|
|
23
|
+
writeFileSync(credPath, `[
|
|
24
|
+
// 普通账号
|
|
25
|
+
{ url: 'https://demo.example.com', username: 'alice', password: 'p1', role: 'admin', systemName: '后台' },
|
|
26
|
+
{ url: 'https://demo.example.com', username: 'bob', password: 'p2', role: 'user' },
|
|
27
|
+
]`);
|
|
28
|
+
const list = loadCredentials(credPath);
|
|
29
|
+
expect(list).toHaveLength(2);
|
|
30
|
+
expect(list[0]).toMatchObject({
|
|
31
|
+
url: 'https://demo.example.com',
|
|
32
|
+
username: 'alice',
|
|
33
|
+
password: 'p1',
|
|
34
|
+
role: 'admin',
|
|
35
|
+
systemName: '后台',
|
|
36
|
+
});
|
|
37
|
+
expect(list[1]).toMatchObject({ username: 'bob', role: 'user' });
|
|
38
|
+
expect(list[1].systemName).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
it('单条凭据无可选字段也能解析', () => {
|
|
41
|
+
writeFileSync(credPath, `[{ url: 'https://x.test', username: 'u', password: 'p' }]`);
|
|
42
|
+
const list = loadCredentials(credPath);
|
|
43
|
+
expect(list).toHaveLength(1);
|
|
44
|
+
expect(list[0].role).toBeUndefined();
|
|
45
|
+
expect(list[0].systemName).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
it('缺必填字段 loginUrl/url 时抛 schema 错误(来自 validateCredentials)', () => {
|
|
48
|
+
writeFileSync(credPath, `[{ username: 'u', password: 'p' }]`);
|
|
49
|
+
expect(() => loadCredentials(credPath)).toThrow(/缺必填字段 loginUrl/);
|
|
50
|
+
});
|
|
51
|
+
it('顶层不是数组时抛 schema 错误', () => {
|
|
52
|
+
writeFileSync(credPath, `{ url: 'x', username: 'u', password: 'p' }`);
|
|
53
|
+
expect(() => loadCredentials(credPath)).toThrow(/顶层必须是数组/);
|
|
54
|
+
});
|
|
55
|
+
it('文件不存在时抛带路径的友好错误', () => {
|
|
56
|
+
const missing = path.join(tmpDir, 'not-here.json5');
|
|
57
|
+
expect(() => loadCredentials(missing)).toThrow(/credentials\.json5 不存在/);
|
|
58
|
+
expect(() => loadCredentials(missing)).toThrow(new RegExp(missing.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
59
|
+
});
|
|
60
|
+
it('JSON5 语法错误时抛解析错误', () => {
|
|
61
|
+
writeFileSync(credPath, `[ { url: 'x', username: 'u', password: 'p' `); // 缺右括号
|
|
62
|
+
expect(() => loadCredentials(credPath)).toThrow();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n-testcase.test.d.ts","sourceRoot":"","sources":["../../../test/util/i18n-testcase.test.ts"],"names":[],"mappings":""}
|