@epoint-testtech/ep-stage-skill 0.0.3-alpha.1
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 -0
- package/codex-skill/ep-stage/create-project/SKILL.md +59 -0
- package/codex-skill/ep-stage/glue-test/SKILL.md +258 -0
- package/codex-skill/ep-stage/glue-test/references/crud-pipeline.md +139 -0
- package/codex-skill/ep-stage/glue-test/references/gap-review-protocol.md +43 -0
- package/codex-skill/ep-stage/glue-test/references/harness-principles.md +46 -0
- package/codex-skill/ep-stage/glue-test/scripts/generate-crud-spec.mjs +149 -0
- package/codex-skill/ep-stage/glue-testcase/SKILL.md +31 -0
- package/codex-skill/ep-stage/glue-testcase/examples/observable-testcase.json +40 -0
- package/codex-skill/ep-stage/glue-testcase/references/testcase-schema.md +67 -0
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +27 -0
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +73 -0
- package/dist/src/capability/coverage-diff.d.ts +34 -0
- package/dist/src/capability/coverage-diff.d.ts.map +1 -0
- package/dist/src/capability/coverage-diff.js +91 -0
- package/dist/src/capability/page-structure.d.ts +31 -0
- package/dist/src/capability/page-structure.d.ts.map +1 -0
- package/dist/src/capability/page-structure.js +50 -0
- package/dist/src/capability/scenario-inference.d.ts +36 -0
- package/dist/src/capability/scenario-inference.d.ts.map +1 -0
- package/dist/src/capability/scenario-inference.js +114 -0
- package/dist/src/cli/generate-crud-contract.d.ts +2 -0
- package/dist/src/cli/generate-crud-contract.d.ts.map +1 -0
- package/dist/src/cli/generate-crud-contract.js +77 -0
- package/dist/src/cli/generate-playwright-tests.d.ts +30 -0
- package/dist/src/cli/generate-playwright-tests.d.ts.map +1 -0
- package/dist/src/cli/generate-playwright-tests.js +81 -0
- package/dist/src/cli/run-gap-pipeline.d.ts +256 -0
- package/dist/src/cli/run-gap-pipeline.d.ts.map +1 -0
- package/dist/src/cli/run-gap-pipeline.js +1468 -0
- package/dist/src/context/stage-context.d.ts +63 -0
- package/dist/src/context/stage-context.d.ts.map +1 -0
- package/dist/src/context/stage-context.js +297 -0
- package/dist/src/contracts/crud-business-module.d.ts +645 -0
- package/dist/src/contracts/crud-business-module.d.ts.map +1 -0
- package/dist/src/contracts/crud-business-module.js +1 -0
- package/dist/src/contracts/gap-inference.d.ts +213 -0
- package/dist/src/contracts/gap-inference.d.ts.map +1 -0
- package/dist/src/contracts/gap-inference.js +11 -0
- package/dist/src/contracts/observable-chain.d.ts +250 -0
- package/dist/src/contracts/observable-chain.d.ts.map +1 -0
- package/dist/src/contracts/observable-chain.js +1 -0
- package/dist/src/extractors/code-list.d.ts +40 -0
- package/dist/src/extractors/code-list.d.ts.map +1 -0
- package/dist/src/extractors/code-list.js +225 -0
- package/dist/src/extractors/html-page.d.ts +67 -0
- package/dist/src/extractors/html-page.d.ts.map +1 -0
- package/dist/src/extractors/html-page.js +195 -0
- package/dist/src/extractors/java-action.d.ts +8 -0
- package/dist/src/extractors/java-action.d.ts.map +1 -0
- package/dist/src/extractors/java-action.js +53 -0
- package/dist/src/extractors/spec-yaml.d.ts +28 -0
- package/dist/src/extractors/spec-yaml.d.ts.map +1 -0
- package/dist/src/extractors/spec-yaml.js +29 -0
- package/dist/src/gap-planner/action-candidates.d.ts +9 -0
- package/dist/src/gap-planner/action-candidates.d.ts.map +1 -0
- package/dist/src/gap-planner/action-candidates.js +66 -0
- package/dist/src/gap-planner/list-gap-workflows.d.ts +17 -0
- package/dist/src/gap-planner/list-gap-workflows.d.ts.map +1 -0
- package/dist/src/gap-planner/list-gap-workflows.js +47 -0
- package/dist/src/gap-planner/plan-agent-workflows.d.ts +26 -0
- package/dist/src/gap-planner/plan-agent-workflows.d.ts.map +1 -0
- package/dist/src/gap-planner/plan-agent-workflows.js +116 -0
- package/dist/src/gap-planner/skeleton-coverage.d.ts +9 -0
- package/dist/src/gap-planner/skeleton-coverage.d.ts.map +1 -0
- package/dist/src/gap-planner/skeleton-coverage.js +41 -0
- package/dist/src/gap-planner/stable-id.d.ts +16 -0
- package/dist/src/gap-planner/stable-id.d.ts.map +1 -0
- package/dist/src/gap-planner/stable-id.js +19 -0
- package/dist/src/generalization/generalization-eval.d.ts +71 -0
- package/dist/src/generalization/generalization-eval.d.ts.map +1 -0
- package/dist/src/generalization/generalization-eval.js +53 -0
- package/dist/src/generators/agent-inferred-workflow-script.d.ts +22 -0
- package/dist/src/generators/agent-inferred-workflow-script.d.ts.map +1 -0
- package/dist/src/generators/agent-inferred-workflow-script.js +230 -0
- package/dist/src/generators/stage-skeleton-script.d.ts +107 -0
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -0
- package/dist/src/generators/stage-skeleton-script.js +607 -0
- package/dist/src/index.d.ts +52 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/material/material-inventory.d.ts +92 -0
- package/dist/src/material/material-inventory.d.ts.map +1 -0
- package/dist/src/material/material-inventory.js +191 -0
- package/dist/src/normalizers/crud-contract.d.ts +107 -0
- package/dist/src/normalizers/crud-contract.d.ts.map +1 -0
- package/dist/src/normalizers/crud-contract.js +1068 -0
- package/dist/src/testcase/testcase-generator.d.ts +43 -0
- package/dist/src/testcase/testcase-generator.d.ts.map +1 -0
- package/dist/src/testcase/testcase-generator.js +152 -0
- package/dist/src/testcase/testcase-skeleton.d.ts +91 -0
- package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -0
- package/dist/src/testcase/testcase-skeleton.js +121 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts +11 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -0
- package/dist/src/testcase/testcase-spec-assembly.js +24 -0
- package/dist/src/trace/review-summary.d.ts +17 -0
- package/dist/src/trace/review-summary.d.ts.map +1 -0
- package/dist/src/trace/review-summary.js +34 -0
- package/dist/src/trace/trace-writer.d.ts +17 -0
- package/dist/src/trace/trace-writer.d.ts.map +1 -0
- package/dist/src/trace/trace-writer.js +81 -0
- package/dist/test/crud-contract.test.d.ts +2 -0
- package/dist/test/crud-contract.test.d.ts.map +1 -0
- package/dist/test/crud-contract.test.js +819 -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 +141 -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/observable-pipeline.test.d.ts +2 -0
- package/dist/test/observable-pipeline.test.d.ts.map +1 -0
- package/dist/test/observable-pipeline.test.js +461 -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/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 +263 -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/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 +134 -0
- package/docs/mvp-usage-guide.md +298 -0
- package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -0
- package/examples/schemeresource.module-hints.json +38 -0
- package/examples/schemeresource.observable.code_list.md +37 -0
- package/examples/zwplace-observable-docs/zwplace.context.md +16 -0
- package/examples/zwplace-placecategory-validation.json +29 -0
- package/examples/zwplace.module-hints.json +69 -0
- package/examples/zwplace.observable.code_list.md +37 -0
- package/package.json +38 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import { cpSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { buildCrudBusinessModuleContract, extractCodeListSummary, extractHtmlPage, extractJavaAction, generateStageSkeletonCrudSpec } from '../src/index.js';
|
|
8
|
+
const packageRoot = process.cwd();
|
|
9
|
+
const srcIndexPath = join(packageRoot, 'src', 'index.ts');
|
|
10
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
11
|
+
const zwplaceDocs = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/_docs');
|
|
12
|
+
const zwplaceWebapp = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/src/main/webapp/perpage/zwplace');
|
|
13
|
+
const zwplaceActions = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/src/main/java/com/epoint/zwplace/action');
|
|
14
|
+
const codeListPath = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/_docs/code_list.md');
|
|
15
|
+
function readZwplaceSpecFixture() {
|
|
16
|
+
return {
|
|
17
|
+
path: join(zwplaceDocs, 'spec.yaml'),
|
|
18
|
+
module: {
|
|
19
|
+
id: 'zwplace',
|
|
20
|
+
label: '场所窗口信息管理',
|
|
21
|
+
description: '场所窗口信息管理模块'
|
|
22
|
+
},
|
|
23
|
+
fields: [
|
|
24
|
+
{ name: 'placename', label: '场所名称', component: 'input', required: true, searchable: true, listVisible: true, formVisible: true, length: 50 },
|
|
25
|
+
{ name: 'placecategory', label: '场所分类', component: 'listbox', required: true, searchable: true, listVisible: true, formVisible: true },
|
|
26
|
+
{ name: 'status', label: '状态', component: 'listbox', required: false, searchable: true, listVisible: true, formVisible: false }
|
|
27
|
+
],
|
|
28
|
+
businessRules: []
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
let stageCoreBuilt = false;
|
|
32
|
+
/**
|
|
33
|
+
* 对单个 TypeScript 夹具做无配置编译,校验导出类型形状。
|
|
34
|
+
*
|
|
35
|
+
* @param source 需要编译的 TypeScript 源码。
|
|
36
|
+
* @returns tsc 执行状态与输出。
|
|
37
|
+
*/
|
|
38
|
+
function compileFixture(source) {
|
|
39
|
+
const fixtureDir = mkdtempSync(join(tmpdir(), 'crud-contract-'));
|
|
40
|
+
const fixturePath = join(fixtureDir, 'fixture.ts');
|
|
41
|
+
const importPath = relative(fixtureDir, srcIndexPath)
|
|
42
|
+
.replaceAll('\\', '/')
|
|
43
|
+
.replace(/\.ts$/, '.js');
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(fixturePath, source.replaceAll('__INDEX_PATH__', importPath), 'utf8');
|
|
46
|
+
const result = spawnSync('pnpm', [
|
|
47
|
+
'exec',
|
|
48
|
+
'tsc',
|
|
49
|
+
'--noEmit',
|
|
50
|
+
'--ignoreConfig',
|
|
51
|
+
'--pretty',
|
|
52
|
+
'false',
|
|
53
|
+
'--target',
|
|
54
|
+
'ES2022',
|
|
55
|
+
'--module',
|
|
56
|
+
'NodeNext',
|
|
57
|
+
'--moduleResolution',
|
|
58
|
+
'NodeNext',
|
|
59
|
+
'--strict',
|
|
60
|
+
'--esModuleInterop',
|
|
61
|
+
'--skipLibCheck',
|
|
62
|
+
fixturePath
|
|
63
|
+
], {
|
|
64
|
+
cwd: packageRoot,
|
|
65
|
+
encoding: 'utf8'
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
status: result.status,
|
|
69
|
+
stdout: result.stdout,
|
|
70
|
+
stderr: result.stderr
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
rmSync(fixtureDir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 确保本地 stage-core 已完成构建,便于后续夹具通过真实包结构做类型检查。
|
|
79
|
+
*
|
|
80
|
+
* @returns 无返回值。
|
|
81
|
+
*/
|
|
82
|
+
function ensureStageCoreBuilt() {
|
|
83
|
+
if (stageCoreBuilt) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
execFileSync('pnpm', ['--filter', '@epoint-testtech/stage-core', 'build'], {
|
|
87
|
+
cwd: repoRoot,
|
|
88
|
+
stdio: 'pipe'
|
|
89
|
+
});
|
|
90
|
+
stageCoreBuilt = true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 基于 stage-create 的 glue 模板骨架编译生成脚本,验证契约输出能在真实模板里通过类型检查。
|
|
94
|
+
*
|
|
95
|
+
* @param source 生成的 Playwright spec 源码。
|
|
96
|
+
* @returns tsc 执行状态与输出。
|
|
97
|
+
*/
|
|
98
|
+
function compileGlueTemplateSpec(source) {
|
|
99
|
+
ensureStageCoreBuilt();
|
|
100
|
+
const fixtureRoot = mkdtempSync(join(tmpdir(), 'stage-create-glue-'));
|
|
101
|
+
const fixtureSrcRoot = join(fixtureRoot, 'src');
|
|
102
|
+
const fixtureTestsRoot = join(fixtureSrcRoot, 'tests');
|
|
103
|
+
const fixtureSkeletonsRoot = join(fixtureSrcRoot, 'skeletons');
|
|
104
|
+
const fixtureWebRoot = join(fixtureSrcRoot, 'web');
|
|
105
|
+
const fixtureNodeModulesRoot = join(fixtureRoot, 'node_modules');
|
|
106
|
+
const fixtureTypesRoot = join(fixtureNodeModulesRoot, '@types');
|
|
107
|
+
const fixturePath = join(fixtureTestsRoot, '__generated-test-glue-fixture.spec.ts');
|
|
108
|
+
const glueTemplateRoot = join(repoRoot, 'packages/stage-create/templates/glue');
|
|
109
|
+
const glueTemplateSkeletonsRoot = join(glueTemplateRoot, 'src/skeletons');
|
|
110
|
+
const glueTemplateWebRoot = join(glueTemplateRoot, 'src/web');
|
|
111
|
+
const glueTemplateReportRoot = join(glueTemplateRoot, 'src/report');
|
|
112
|
+
const fixtureReportRoot = join(fixtureSrcRoot, 'report');
|
|
113
|
+
const stageCorePackageRoot = join(repoRoot, 'packages/stage-core');
|
|
114
|
+
const playwrightPackageRoot = join(stageCorePackageRoot, 'node_modules/@playwright/test');
|
|
115
|
+
const nodeTypesPackageRoot = join(packageRoot, 'node_modules/@types/node');
|
|
116
|
+
try {
|
|
117
|
+
mkdirSync(fixtureTestsRoot, { recursive: true });
|
|
118
|
+
mkdirSync(join(fixtureNodeModulesRoot, '@epoint-testtech'), { recursive: true });
|
|
119
|
+
mkdirSync(join(fixtureNodeModulesRoot, '@playwright'), { recursive: true });
|
|
120
|
+
mkdirSync(fixtureTypesRoot, { recursive: true });
|
|
121
|
+
cpSync(glueTemplateSkeletonsRoot, fixtureSkeletonsRoot, { recursive: true });
|
|
122
|
+
cpSync(glueTemplateWebRoot, fixtureWebRoot, { recursive: true });
|
|
123
|
+
cpSync(glueTemplateReportRoot, fixtureReportRoot, { recursive: true });
|
|
124
|
+
writeFileSync(fixturePath, source, 'utf8');
|
|
125
|
+
writeFileSync(join(fixtureRoot, 'package.json'), JSON.stringify({ name: 'stage-create-glue-fixture', private: true, type: 'module' }, null, 2), 'utf8');
|
|
126
|
+
writeFileSync(join(fixtureRoot, 'tsconfig.json'), JSON.stringify({
|
|
127
|
+
compilerOptions: {
|
|
128
|
+
target: 'ES2022',
|
|
129
|
+
module: 'ESNext',
|
|
130
|
+
moduleResolution: 'bundler',
|
|
131
|
+
strict: true,
|
|
132
|
+
esModuleInterop: true,
|
|
133
|
+
skipLibCheck: true,
|
|
134
|
+
types: ['node']
|
|
135
|
+
},
|
|
136
|
+
include: ['src/**/*.ts']
|
|
137
|
+
}, null, 2), 'utf8');
|
|
138
|
+
symlinkSync(stageCorePackageRoot, join(fixtureNodeModulesRoot, '@epoint-testtech/stage-core'), 'dir');
|
|
139
|
+
symlinkSync(playwrightPackageRoot, join(fixtureNodeModulesRoot, '@playwright/test'), 'dir');
|
|
140
|
+
symlinkSync(nodeTypesPackageRoot, join(fixtureTypesRoot, 'node'), 'dir');
|
|
141
|
+
const result = spawnSync(join(packageRoot, 'node_modules/.bin/tsc'), [
|
|
142
|
+
'--noEmit',
|
|
143
|
+
'--pretty',
|
|
144
|
+
'false',
|
|
145
|
+
'--project',
|
|
146
|
+
join(fixtureRoot, 'tsconfig.json')
|
|
147
|
+
], {
|
|
148
|
+
cwd: packageRoot,
|
|
149
|
+
encoding: 'utf8'
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
status: result.status,
|
|
153
|
+
stdout: result.stdout,
|
|
154
|
+
stderr: result.stderr
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
rmSync(fixtureRoot, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 使用 zwplace 真实物料与 hints 组装一份完整 CRUD 契约,作为回归样本。
|
|
163
|
+
*
|
|
164
|
+
* @returns CRUD 业务模块契约对象。
|
|
165
|
+
*/
|
|
166
|
+
function buildZwplaceContractWithHints() {
|
|
167
|
+
const spec = readZwplaceSpecFixture();
|
|
168
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
169
|
+
const pages = ['gxhzwplacelist.html', 'gxhzwplaceadd.html', 'gxhzwplaceedit.html', 'gxhzwplacedetail.html']
|
|
170
|
+
.map((fileName) => extractHtmlPage(join(zwplaceWebapp, fileName)));
|
|
171
|
+
const actions = ['GxhZwPlaceListAction.java', 'GxhZwPlaceAddAction.java', 'GxhZwPlaceEditAction.java']
|
|
172
|
+
.map((fileName) => extractJavaAction(join(zwplaceActions, fileName)));
|
|
173
|
+
return buildCrudBusinessModuleContract({
|
|
174
|
+
moduleId: 'zwplace',
|
|
175
|
+
spec,
|
|
176
|
+
codeList,
|
|
177
|
+
pages,
|
|
178
|
+
actions,
|
|
179
|
+
hints: {
|
|
180
|
+
dataKey: {
|
|
181
|
+
selectedField: 'placename',
|
|
182
|
+
generationStrategy: 'timestamp_prefix'
|
|
183
|
+
},
|
|
184
|
+
searchConditions: [
|
|
185
|
+
{
|
|
186
|
+
field: 'placename',
|
|
187
|
+
component: 'input',
|
|
188
|
+
seedValue: '自动化测试名称',
|
|
189
|
+
strategy: 'timestamp_prefix',
|
|
190
|
+
updatePrefix: '修改'
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
field: 'placecategory',
|
|
194
|
+
component: 'listbox',
|
|
195
|
+
value: '条线大厅',
|
|
196
|
+
createSelections: [
|
|
197
|
+
{ field: 'placecategory', value: '条线大厅' }
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
field: 'status',
|
|
202
|
+
component: 'listbox',
|
|
203
|
+
value: '在用',
|
|
204
|
+
createSelections: [
|
|
205
|
+
{ field: 'placecategory', value: '条线大厅' }
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
buttonAliases: {
|
|
210
|
+
create: ['新增场所信息管理', '新增'],
|
|
211
|
+
saveAndClose: ['保存并关闭'],
|
|
212
|
+
search: ['搜索', '查询']
|
|
213
|
+
},
|
|
214
|
+
autofill: {
|
|
215
|
+
overrideFields: [
|
|
216
|
+
{ field: 'placename', strategy: 'timestamp_prefix', value: '自动化测试场所名称' }
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
deletePolicy: 'blocked_by_business_rule'
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
describe('CrudBusinessModuleContract', () => {
|
|
224
|
+
it('supports the v1 contract shape', () => {
|
|
225
|
+
const contract = {
|
|
226
|
+
contractVersion: 'crud-business-module/v1',
|
|
227
|
+
module: {
|
|
228
|
+
id: 'zwplace',
|
|
229
|
+
label: '场所窗口信息管理'
|
|
230
|
+
},
|
|
231
|
+
pages: {
|
|
232
|
+
list: {
|
|
233
|
+
role: 'list',
|
|
234
|
+
pageId: 'gxhzwplacelist',
|
|
235
|
+
title: '场所信息管理列表',
|
|
236
|
+
iframeSrcKeyword: 'gxhzwplacelist',
|
|
237
|
+
fields: [],
|
|
238
|
+
buttons: [],
|
|
239
|
+
grids: [],
|
|
240
|
+
dialogs: [],
|
|
241
|
+
sources: []
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
dataKey: {
|
|
245
|
+
field: 'placename',
|
|
246
|
+
label: '场所名称',
|
|
247
|
+
availability: 'derivable',
|
|
248
|
+
generationStrategy: 'timestamp_prefix',
|
|
249
|
+
sources: []
|
|
250
|
+
},
|
|
251
|
+
flows: {},
|
|
252
|
+
assertions: [],
|
|
253
|
+
businessRules: [],
|
|
254
|
+
unresolvedSlots: []
|
|
255
|
+
};
|
|
256
|
+
expect(contract.contractVersion).toBe('crud-business-module/v1');
|
|
257
|
+
expect(contract.module.id).toBe('zwplace');
|
|
258
|
+
expect(contract.pages.list.role).toBe('list');
|
|
259
|
+
expect(contract.dataKey.field).toBe('placename');
|
|
260
|
+
expect(contract.dataKey.generationStrategy).toBe('timestamp_prefix');
|
|
261
|
+
expect(Array.isArray(contract.assertions)).toBe(true);
|
|
262
|
+
expect(Array.isArray(contract.unresolvedSlots)).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
it('accepts a valid contract fixture in tsc', () => {
|
|
265
|
+
const result = compileFixture(`
|
|
266
|
+
import type { CrudBusinessModuleContract } from '__INDEX_PATH__';
|
|
267
|
+
|
|
268
|
+
const contract: CrudBusinessModuleContract = {
|
|
269
|
+
contractVersion: 'crud-business-module/v1',
|
|
270
|
+
module: {
|
|
271
|
+
id: 'zwplace',
|
|
272
|
+
label: '场所窗口信息管理'
|
|
273
|
+
},
|
|
274
|
+
pages: {
|
|
275
|
+
list: {
|
|
276
|
+
role: 'list',
|
|
277
|
+
pageId: 'gxhzwplacelist',
|
|
278
|
+
title: '场所信息管理列表',
|
|
279
|
+
iframeSrcKeyword: 'gxhzwplacelist',
|
|
280
|
+
fields: [],
|
|
281
|
+
buttons: [],
|
|
282
|
+
grids: [],
|
|
283
|
+
dialogs: [],
|
|
284
|
+
sources: []
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
dataKey: {
|
|
288
|
+
field: 'placename',
|
|
289
|
+
label: '场所名称',
|
|
290
|
+
availability: 'derivable',
|
|
291
|
+
generationStrategy: 'timestamp_prefix',
|
|
292
|
+
sources: []
|
|
293
|
+
},
|
|
294
|
+
flows: {},
|
|
295
|
+
assertions: [
|
|
296
|
+
{
|
|
297
|
+
id: 'exists-check',
|
|
298
|
+
type: 'record_exists',
|
|
299
|
+
field: 'placename',
|
|
300
|
+
expectedCount: 1,
|
|
301
|
+
availability: 'derivable',
|
|
302
|
+
sources: []
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'toast-check',
|
|
306
|
+
type: 'toast_message',
|
|
307
|
+
expectedMessage: '保存成功',
|
|
308
|
+
availability: 'derivable',
|
|
309
|
+
sources: []
|
|
310
|
+
}
|
|
311
|
+
],
|
|
312
|
+
businessRules: [],
|
|
313
|
+
unresolvedSlots: []
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
void contract;
|
|
317
|
+
`);
|
|
318
|
+
expect(result.status).toBe(0);
|
|
319
|
+
expect(result.stderr).toBe('');
|
|
320
|
+
});
|
|
321
|
+
it('rejects inconsistent page role bindings in tsc', () => {
|
|
322
|
+
const result = compileFixture(`
|
|
323
|
+
import type { CrudBusinessModuleContract } from '__INDEX_PATH__';
|
|
324
|
+
|
|
325
|
+
const contract: CrudBusinessModuleContract = {
|
|
326
|
+
contractVersion: 'crud-business-module/v1',
|
|
327
|
+
module: {
|
|
328
|
+
id: 'zwplace',
|
|
329
|
+
label: '场所窗口信息管理'
|
|
330
|
+
},
|
|
331
|
+
pages: {
|
|
332
|
+
list: {
|
|
333
|
+
role: 'edit',
|
|
334
|
+
pageId: 'gxhzwplacelist',
|
|
335
|
+
title: '场所信息管理列表',
|
|
336
|
+
iframeSrcKeyword: 'gxhzwplacelist',
|
|
337
|
+
fields: [],
|
|
338
|
+
buttons: [],
|
|
339
|
+
grids: [],
|
|
340
|
+
dialogs: [],
|
|
341
|
+
sources: []
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
dataKey: {
|
|
345
|
+
field: 'placename',
|
|
346
|
+
label: '场所名称',
|
|
347
|
+
availability: 'derivable',
|
|
348
|
+
generationStrategy: 'timestamp_prefix',
|
|
349
|
+
sources: []
|
|
350
|
+
},
|
|
351
|
+
flows: {},
|
|
352
|
+
assertions: [],
|
|
353
|
+
businessRules: [],
|
|
354
|
+
unresolvedSlots: []
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
void contract;
|
|
358
|
+
`);
|
|
359
|
+
expect(result.status).not.toBe(0);
|
|
360
|
+
expect(result.stdout).toContain(`Type '"edit"' is not assignable to type '"list"'`);
|
|
361
|
+
});
|
|
362
|
+
it('rejects invalid assertion variants in tsc', () => {
|
|
363
|
+
const result = compileFixture(`
|
|
364
|
+
import type { AssertionContract } from '__INDEX_PATH__';
|
|
365
|
+
|
|
366
|
+
const invalidToast: AssertionContract = {
|
|
367
|
+
id: 'toast-check',
|
|
368
|
+
type: 'toast_message',
|
|
369
|
+
availability: 'derivable',
|
|
370
|
+
sources: []
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const invalidExists: AssertionContract = {
|
|
374
|
+
id: 'exists-check',
|
|
375
|
+
type: 'record_exists',
|
|
376
|
+
availability: 'derivable',
|
|
377
|
+
sources: []
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
void invalidToast;
|
|
381
|
+
void invalidExists;
|
|
382
|
+
`);
|
|
383
|
+
expect(result.status).not.toBe(0);
|
|
384
|
+
expect(result.stdout).toContain(`Property 'expectedMessage' is missing`);
|
|
385
|
+
expect(result.stdout).toContain(`missing the following properties`);
|
|
386
|
+
expect(result.stdout).toContain(`field, expectedCount`);
|
|
387
|
+
});
|
|
388
|
+
it('accepts module hints and slot resolutions in tsc', () => {
|
|
389
|
+
const result = compileFixture(`
|
|
390
|
+
import type { EvidenceRef, ModuleHints, SlotResolution } from '__INDEX_PATH__';
|
|
391
|
+
|
|
392
|
+
const evidence: EvidenceRef = {
|
|
393
|
+
kind: 'test_policy_hint',
|
|
394
|
+
path: 'examples/zwplace.module-hints.json',
|
|
395
|
+
evidenceText: 'dataKey.selectedField=placename',
|
|
396
|
+
confidence: 'high'
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const hints: ModuleHints = {
|
|
400
|
+
dataKey: {
|
|
401
|
+
selectedField: 'placename',
|
|
402
|
+
generationStrategy: 'timestamp_prefix'
|
|
403
|
+
},
|
|
404
|
+
deletePolicy: 'blocked_by_business_rule',
|
|
405
|
+
buttonAliases: {
|
|
406
|
+
search: ['搜索', '查询']
|
|
407
|
+
},
|
|
408
|
+
autofill: {
|
|
409
|
+
overrideFields: [
|
|
410
|
+
{ field: 'placename', strategy: 'timestamp_prefix', value: '自动化测试场所名称' }
|
|
411
|
+
]
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const slot: SlotResolution<string> = {
|
|
416
|
+
status: 'resolved',
|
|
417
|
+
value: 'placename',
|
|
418
|
+
evidence: [evidence]
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
void hints;
|
|
422
|
+
void slot;
|
|
423
|
+
`);
|
|
424
|
+
expect(result.status).toBe(0);
|
|
425
|
+
expect(result.stderr).toBe('');
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
describe('crud contract normalizer', () => {
|
|
429
|
+
it('builds a zwplace CRUD contract with unresolved strategy slots', () => {
|
|
430
|
+
const spec = readZwplaceSpecFixture();
|
|
431
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
432
|
+
const pages = ['gxhzwplacelist.html', 'gxhzwplaceadd.html', 'gxhzwplaceedit.html', 'gxhzwplacedetail.html']
|
|
433
|
+
.map((fileName) => extractHtmlPage(join(zwplaceWebapp, fileName)));
|
|
434
|
+
const actions = ['GxhZwPlaceListAction.java', 'GxhZwPlaceAddAction.java', 'GxhZwPlaceEditAction.java']
|
|
435
|
+
.map((fileName) => extractJavaAction(join(zwplaceActions, fileName)));
|
|
436
|
+
const contract = buildCrudBusinessModuleContract({
|
|
437
|
+
moduleId: 'zwplace',
|
|
438
|
+
spec,
|
|
439
|
+
codeList,
|
|
440
|
+
pages,
|
|
441
|
+
actions
|
|
442
|
+
});
|
|
443
|
+
expect(contract.contractVersion).toBe('crud-business-module/v1');
|
|
444
|
+
expect(contract.pages.list.pageId).toBe('gxhzwplacelist');
|
|
445
|
+
expect(contract.pages.add?.pageId).toBe('gxhzwplaceadd');
|
|
446
|
+
expect(contract.dataKey).toMatchObject({
|
|
447
|
+
field: 'placename',
|
|
448
|
+
label: '场所名称',
|
|
449
|
+
availability: 'human_required'
|
|
450
|
+
});
|
|
451
|
+
expect(contract.flows.create?.entryButton.label).toBe('新增场所窗口信息管理');
|
|
452
|
+
expect(contract.flows.delete?.expectedOutcome).toBe('blocked_by_business_rule');
|
|
453
|
+
expect(contract.unresolvedSlots.map((slot) => slot.slotId)).toContain('dataKey.selectedField');
|
|
454
|
+
expect(contract.unresolvedSlots.map((slot) => slot.slotId)).toContain('autofill.overrideFields');
|
|
455
|
+
expect(contract.unresolvedSlots.map((slot) => slot.slotId)).toContain('dataKey.generationStrategy');
|
|
456
|
+
expect(contract.unresolvedSlots.map((slot) => slot.slotId)).toContain('delete.expectedOutcome');
|
|
457
|
+
expect(contract.assertions.some((a) => a.type === 'toast_message')).toBe(true);
|
|
458
|
+
expect(contract.assertions.some((a) => a.type === 'record_exists')).toBe(true);
|
|
459
|
+
});
|
|
460
|
+
it('supports custom buttonLabels and dataKeyField config', () => {
|
|
461
|
+
const spec = readZwplaceSpecFixture();
|
|
462
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
463
|
+
const pages = ['gxhzwplacelist.html', 'gxhzwplaceadd.html']
|
|
464
|
+
.map((fileName) => extractHtmlPage(join(zwplaceWebapp, fileName)));
|
|
465
|
+
const actions = [];
|
|
466
|
+
const contract = buildCrudBusinessModuleContract({
|
|
467
|
+
moduleId: 'zwplace',
|
|
468
|
+
spec,
|
|
469
|
+
codeList,
|
|
470
|
+
pages,
|
|
471
|
+
actions,
|
|
472
|
+
buttonLabels: {
|
|
473
|
+
create: '自定义新增',
|
|
474
|
+
saveAndClose: '自定义保存'
|
|
475
|
+
},
|
|
476
|
+
dataKeyField: 'placecategory'
|
|
477
|
+
});
|
|
478
|
+
expect(contract.dataKey.field).toBe('placecategory');
|
|
479
|
+
expect(contract.flows.create?.entryButton.label).toBe('自定义新增');
|
|
480
|
+
expect(contract.flows.create?.saveButton.label).toBe('自定义保存');
|
|
481
|
+
});
|
|
482
|
+
it('throws when list page is missing', () => {
|
|
483
|
+
const spec = readZwplaceSpecFixture();
|
|
484
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
485
|
+
const pages = [extractHtmlPage(join(zwplaceWebapp, 'gxhzwplaceadd.html'))];
|
|
486
|
+
const actions = [];
|
|
487
|
+
expect(() => buildCrudBusinessModuleContract({ moduleId: 'zwplace', spec, codeList, pages, actions })).toThrow('List page is required');
|
|
488
|
+
});
|
|
489
|
+
it('auto-resolves dataKeyField from spec searchable fields when not provided', () => {
|
|
490
|
+
const spec = readZwplaceSpecFixture();
|
|
491
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
492
|
+
const pages = [extractHtmlPage(join(zwplaceWebapp, 'gxhzwplacelist.html'))];
|
|
493
|
+
const actions = [];
|
|
494
|
+
const contract = buildCrudBusinessModuleContract({
|
|
495
|
+
moduleId: 'zwplace',
|
|
496
|
+
spec,
|
|
497
|
+
codeList,
|
|
498
|
+
pages,
|
|
499
|
+
actions
|
|
500
|
+
});
|
|
501
|
+
expect(contract.dataKey.field).toBe('placename');
|
|
502
|
+
});
|
|
503
|
+
it('uses ModuleHints for dataKey, button aliases, autofill overrides, and delete policy', () => {
|
|
504
|
+
const spec = readZwplaceSpecFixture();
|
|
505
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
506
|
+
const pages = ['gxhzwplacelist.html', 'gxhzwplaceadd.html', 'gxhzwplaceedit.html', 'gxhzwplacedetail.html']
|
|
507
|
+
.map((fileName) => extractHtmlPage(join(zwplaceWebapp, fileName)));
|
|
508
|
+
const actions = ['GxhZwPlaceListAction.java', 'GxhZwPlaceAddAction.java', 'GxhZwPlaceEditAction.java']
|
|
509
|
+
.map((fileName) => extractJavaAction(join(zwplaceActions, fileName)));
|
|
510
|
+
const contract = buildCrudBusinessModuleContract({
|
|
511
|
+
moduleId: 'zwplace',
|
|
512
|
+
spec,
|
|
513
|
+
codeList,
|
|
514
|
+
pages,
|
|
515
|
+
actions,
|
|
516
|
+
hints: {
|
|
517
|
+
dataKey: {
|
|
518
|
+
selectedField: 'placecategory',
|
|
519
|
+
generationStrategy: 'fixed_value'
|
|
520
|
+
},
|
|
521
|
+
buttonAliases: {
|
|
522
|
+
search: ['搜索', '查询']
|
|
523
|
+
},
|
|
524
|
+
autofill: {
|
|
525
|
+
overrideFields: [
|
|
526
|
+
{ field: 'placecategory', strategy: 'fixed_value', value: '大厅' }
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
deletePolicy: 'blocked_by_business_rule'
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
expect(contract.dataKey).toMatchObject({
|
|
533
|
+
field: 'placecategory',
|
|
534
|
+
label: '场所分类',
|
|
535
|
+
generationStrategy: 'fixed_value'
|
|
536
|
+
});
|
|
537
|
+
expect(contract.flows.create?.overrideFields).toEqual([
|
|
538
|
+
expect.objectContaining({ field: 'placecategory' })
|
|
539
|
+
]);
|
|
540
|
+
expect(contract.flows.delete?.expectedOutcome).toBe('blocked_by_business_rule');
|
|
541
|
+
expect(contract.unresolvedSlots.map((slot) => slot.slotId)).not.toContain('dataKey.selectedField');
|
|
542
|
+
});
|
|
543
|
+
it('marks policy slots unresolved when hints are missing and evidence is weak', () => {
|
|
544
|
+
const spec = readZwplaceSpecFixture();
|
|
545
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
546
|
+
const pages = ['gxhzwplacelist.html', 'gxhzwplaceadd.html']
|
|
547
|
+
.map((fileName) => extractHtmlPage(join(zwplaceWebapp, fileName)));
|
|
548
|
+
const contract = buildCrudBusinessModuleContract({
|
|
549
|
+
moduleId: 'zwplace',
|
|
550
|
+
spec,
|
|
551
|
+
codeList,
|
|
552
|
+
pages,
|
|
553
|
+
actions: []
|
|
554
|
+
});
|
|
555
|
+
expect(contract.unresolvedSlots.map((slot) => slot.slotId)).toContain('dataKey.selectedField');
|
|
556
|
+
expect(contract.unresolvedSlots.map((slot) => slot.slotId)).toContain('delete.expectedOutcome');
|
|
557
|
+
});
|
|
558
|
+
it('includes agent inferred workflows from ModuleHints customWorkflows', () => {
|
|
559
|
+
const spec = readZwplaceSpecFixture();
|
|
560
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
561
|
+
const pages = [extractHtmlPage(join(zwplaceWebapp, 'gxhzwplacelist.html'))];
|
|
562
|
+
const contract = buildCrudBusinessModuleContract({
|
|
563
|
+
moduleId: 'zwplace',
|
|
564
|
+
spec,
|
|
565
|
+
codeList,
|
|
566
|
+
pages,
|
|
567
|
+
actions: [],
|
|
568
|
+
hints: {
|
|
569
|
+
buttonAliases: {
|
|
570
|
+
create: ['新增场所信息管理'],
|
|
571
|
+
search: ['搜索'],
|
|
572
|
+
edit: ['修改'],
|
|
573
|
+
delete: ['删除选定'],
|
|
574
|
+
export: ['导出'],
|
|
575
|
+
},
|
|
576
|
+
dataKey: { selectedField: 'placename', generationStrategy: 'timestamp_prefix' },
|
|
577
|
+
deletePolicy: 'blocked_by_business_rule',
|
|
578
|
+
customWorkflows: [
|
|
579
|
+
{
|
|
580
|
+
label: '同步窗口管理系统',
|
|
581
|
+
expectedIntent: '同步窗口管理系统数据',
|
|
582
|
+
expectedAssertion: '出现同步完成提示',
|
|
583
|
+
include: true,
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
expect(contract.agentInferredWorkflows?.[0]).toMatchObject({
|
|
589
|
+
workflowId: 'zwplace.custom_da3d238e',
|
|
590
|
+
status: 'planned',
|
|
591
|
+
executionSource: 'agent_inferred',
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
describe('upstream extractors', () => {
|
|
596
|
+
it('extracts list page controls from HTML', () => {
|
|
597
|
+
const page = extractHtmlPage(join(zwplaceWebapp, 'gxhzwplacelist.html'));
|
|
598
|
+
expect(page.pageId).toBe('gxhzwplacelist');
|
|
599
|
+
expect(page.title).toBe('场所信息管理列表');
|
|
600
|
+
expect(page.buttons.map((button) => button.label)).toContain('新增场所信息管理');
|
|
601
|
+
expect(page.buttons.find((button) => button.label === '新增场所信息管理')?.locator)
|
|
602
|
+
.toBe("xpath=//a[contains(@class,'mini-button')][normalize-space()='新增场所信息管理']");
|
|
603
|
+
expect(page.dialogs.find((dialog) => dialog.title === '新增场所信息管理')).toMatchObject({
|
|
604
|
+
title: '新增场所信息管理',
|
|
605
|
+
pageId: 'perpage/zwplace/gxhzwplaceadd'
|
|
606
|
+
});
|
|
607
|
+
expect(page.grids[0]?.columns.map((column) => column.field)).toContain('placename');
|
|
608
|
+
});
|
|
609
|
+
it('extracts required and maxLength from generated zwplace form HTML', () => {
|
|
610
|
+
const page = extractHtmlPage(join(zwplaceWebapp, 'gxhzwplaceadd.html'));
|
|
611
|
+
expect(page.fields.find((field) => field.name === 'placename')).toMatchObject({
|
|
612
|
+
label: '场所名称',
|
|
613
|
+
required: true,
|
|
614
|
+
maxLength: 50,
|
|
615
|
+
locator: "//div[@label='场所名称']//input[@class='mini-textbox-input']"
|
|
616
|
+
});
|
|
617
|
+
expect(page.fields.find((field) => field.name === 'address')).toMatchObject({
|
|
618
|
+
label: '路/弄/号',
|
|
619
|
+
required: true,
|
|
620
|
+
maxLength: 50
|
|
621
|
+
});
|
|
622
|
+
expect(page.fields.find((field) => field.name === 'consultphone')).toMatchObject({
|
|
623
|
+
label: '咨询电话',
|
|
624
|
+
required: false,
|
|
625
|
+
maxLength: 20
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
it('separates grid data columns from selection, index, and row actions', () => {
|
|
629
|
+
const page = extractHtmlPage(join(zwplaceWebapp, 'gxhzwplacelist.html'));
|
|
630
|
+
const grid = page.grids[0];
|
|
631
|
+
expect(grid?.dataColumns.map((column) => column.field)).toEqual([
|
|
632
|
+
'placename',
|
|
633
|
+
'placecategory',
|
|
634
|
+
'address',
|
|
635
|
+
'servicetime',
|
|
636
|
+
'consultphone',
|
|
637
|
+
'complaintphone',
|
|
638
|
+
'status',
|
|
639
|
+
'version'
|
|
640
|
+
]);
|
|
641
|
+
expect(grid?.selectionColumn?.columnType).toBe('checkcolumn');
|
|
642
|
+
expect(grid?.indexColumn?.columnType).toBe('indexcolumn');
|
|
643
|
+
expect(grid?.rowActions.map((action) => action.label)).toEqual(['修改', '查看']);
|
|
644
|
+
expect(grid?.columns.some((column) => column.field === '')).toBe(false);
|
|
645
|
+
});
|
|
646
|
+
it('uses module and field metadata from the zwplace spec fixture', () => {
|
|
647
|
+
const spec = readZwplaceSpecFixture();
|
|
648
|
+
expect(spec.module.id).toBe('zwplace');
|
|
649
|
+
expect(spec.module.label).toBe('场所窗口信息管理');
|
|
650
|
+
expect(spec.fields.find((field) => field.name === 'placename')).toMatchObject({
|
|
651
|
+
label: '场所名称',
|
|
652
|
+
required: true,
|
|
653
|
+
searchable: true
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
it('extracts code list summaries', () => {
|
|
657
|
+
const summary = extractCodeListSummary(codeListPath);
|
|
658
|
+
expect(summary.moduleLabel).toBe('场所窗口信息管理');
|
|
659
|
+
expect(summary.pages).toContain('gxhzwplacelist.html');
|
|
660
|
+
expect(summary.methods).toContain('deleteSelect');
|
|
661
|
+
});
|
|
662
|
+
it('separates action methods from service and dao methods in code list', () => {
|
|
663
|
+
const summary = extractCodeListSummary(codeListPath);
|
|
664
|
+
expect(summary.actionMethods.GxhZwPlaceListAction).toContain('deleteSelect');
|
|
665
|
+
expect(summary.actionMethods.GxhZwPlaceAddAction).toContain('add');
|
|
666
|
+
expect(summary.serviceMethods).toContain('insert');
|
|
667
|
+
expect(summary.daoMethods).toContain('archivePlace');
|
|
668
|
+
expect(summary.methods).toContain('deleteSelect');
|
|
669
|
+
});
|
|
670
|
+
it('extracts Java Action methods and callback messages', () => {
|
|
671
|
+
const action = extractJavaAction(join(zwplaceActions, 'GxhZwPlaceListAction.java'));
|
|
672
|
+
expect(action.controllerName).toBe('gxhzwplacelistaction');
|
|
673
|
+
expect(action.methods).toContain('deleteSelect');
|
|
674
|
+
expect(action.messages).toContain('在用数据不可删除!');
|
|
675
|
+
});
|
|
676
|
+
it('extracts only callback and tip messages from Java Action', () => {
|
|
677
|
+
const action = extractJavaAction(join(zwplaceActions, 'GxhZwPlaceAddAction.java'));
|
|
678
|
+
expect(action.messages).toContain('场所名称已存在!');
|
|
679
|
+
expect(action.messages).toContain('保存成功!');
|
|
680
|
+
expect(action.messages).not.toContain('黄浦区');
|
|
681
|
+
expect(action.messages).not.toContain('下拉列表');
|
|
682
|
+
expect(action.messages.some((message) => message.includes('public class'))).toBe(false);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
describe('generate-crud-contract CLI', () => {
|
|
686
|
+
it('generates a contract from CLI with module hints', () => {
|
|
687
|
+
const outDir = mkdtempSync(join(tmpdir(), 'test-glue-'));
|
|
688
|
+
const outFile = join(outDir, 'zwplace.crud.contract.json');
|
|
689
|
+
execFileSync('pnpm', [
|
|
690
|
+
'generate:crud-contract',
|
|
691
|
+
'--',
|
|
692
|
+
'--module-id',
|
|
693
|
+
'zwplace',
|
|
694
|
+
'--docs',
|
|
695
|
+
zwplaceDocs,
|
|
696
|
+
'--code-list',
|
|
697
|
+
codeListPath,
|
|
698
|
+
'--webapp',
|
|
699
|
+
zwplaceWebapp,
|
|
700
|
+
'--java-actions',
|
|
701
|
+
zwplaceActions,
|
|
702
|
+
'--hints',
|
|
703
|
+
join(packageRoot, 'examples', 'zwplace.module-hints.json'),
|
|
704
|
+
'--out',
|
|
705
|
+
outFile
|
|
706
|
+
], { cwd: packageRoot, stdio: 'pipe' });
|
|
707
|
+
const json = JSON.parse(readFileSync(outFile, 'utf8'));
|
|
708
|
+
expect(json.contractVersion).toBe('crud-business-module/v1');
|
|
709
|
+
expect(json.module.id).toBe('zwplace');
|
|
710
|
+
expect(json.dataKey.field).toBe('placename');
|
|
711
|
+
expect(json.dataKey.generationStrategy).toBe('timestamp_prefix');
|
|
712
|
+
expect(json.searchConditions?.map((condition) => condition.field)).toEqual(['placename', 'placecategory', 'status']);
|
|
713
|
+
expect(json.unresolvedSlots.map((slot) => slot.slotId)).not.toContain('dataKey.selectedField');
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
describe('stage-skeleton Playwright generator', () => {
|
|
717
|
+
it('maps a zwplace CRUD contract to CrudFlowSlots and generated skeleton spec compiles', () => {
|
|
718
|
+
const contract = buildZwplaceContractWithHints();
|
|
719
|
+
const script = generateStageSkeletonCrudSpec({
|
|
720
|
+
contract,
|
|
721
|
+
menuNavigation: '场所窗口信息管理>场所窗口信息列表',
|
|
722
|
+
testName: 'zwplace CRUD 胶水生成链路'
|
|
723
|
+
});
|
|
724
|
+
expect(script).toContain("import { CrudPage, ExportPage, LoginPage, MenuPage, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../skeletons';");
|
|
725
|
+
expect(script).toContain("import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../report/glue-report';");
|
|
726
|
+
expect(script).toContain("test.describe('场所窗口信息管理 CRUD + Export'");
|
|
727
|
+
expect(script).toContain("test.describe.configure({ mode: 'serial' });");
|
|
728
|
+
expect(script).toContain("let workflowData: CrudWorkflowData | undefined;");
|
|
729
|
+
expect(script).toContain('const crudSlots: CrudFlowSlots =');
|
|
730
|
+
expect(script).toContain("moduleId: 'zwplace'");
|
|
731
|
+
expect(script).toContain("workflowId: 'zwplace.create'");
|
|
732
|
+
expect(script).toContain("workflowId: 'zwplace.read'");
|
|
733
|
+
expect(script).toContain("workflowId: 'zwplace.update'");
|
|
734
|
+
expect(script).toContain("workflowId: 'zwplace.delete'");
|
|
735
|
+
expect(script).toContain("workflowId: 'zwplace.export'");
|
|
736
|
+
expect(script).toContain('workflowData = crudPage.prepareWorkflowData();');
|
|
737
|
+
expect(script).toContain('await crudPage.runReadWorkflow(workflowData);');
|
|
738
|
+
expect(script).toContain('await crudPage.runUpdateWorkflow(workflowData);');
|
|
739
|
+
expect(script).toContain('await crudPage.runDeleteWorkflow(workflowData);');
|
|
740
|
+
expect(script).toContain('await exportPage.runExportWorkflow(testInfo);');
|
|
741
|
+
expect(script).not.toContain('runCrudFlow');
|
|
742
|
+
const compileResult = compileGlueTemplateSpec(script);
|
|
743
|
+
expect(compileResult.status).toBe(0);
|
|
744
|
+
expect(compileResult.stderr).toBe('');
|
|
745
|
+
});
|
|
746
|
+
it('generates a stage-skeleton spec from CLI', () => {
|
|
747
|
+
const outDir = mkdtempSync(join(tmpdir(), 'test-glue-playwright-'));
|
|
748
|
+
const contractFile = join(outDir, 'zwplace.crud.contract.json');
|
|
749
|
+
const specFile = join(outDir, 'zwplace.spec.ts');
|
|
750
|
+
try {
|
|
751
|
+
writeFileSync(contractFile, JSON.stringify(buildZwplaceContractWithHints(), null, 2), 'utf8');
|
|
752
|
+
execFileSync('pnpm', [
|
|
753
|
+
'generate:playwright-tests',
|
|
754
|
+
'--',
|
|
755
|
+
'--contract',
|
|
756
|
+
contractFile,
|
|
757
|
+
'--out',
|
|
758
|
+
specFile,
|
|
759
|
+
'--menu',
|
|
760
|
+
'场所窗口信息管理>场所窗口信息列表'
|
|
761
|
+
], { cwd: packageRoot, stdio: 'pipe' });
|
|
762
|
+
const script = readFileSync(specFile, 'utf8');
|
|
763
|
+
expect(script).toContain("import { CrudPage, ExportPage, LoginPage, MenuPage, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../skeletons';");
|
|
764
|
+
expect(script).toContain("import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../report/glue-report';");
|
|
765
|
+
expect(script).toContain('const crudSlots: CrudFlowSlots =');
|
|
766
|
+
expect(script).toContain("workflowId: 'zwplace.create'");
|
|
767
|
+
expect(script).toContain('workflowData = crudPage.prepareWorkflowData();');
|
|
768
|
+
expect(script).not.toContain('runCrudFlow');
|
|
769
|
+
const compileResult = compileGlueTemplateSpec(script);
|
|
770
|
+
expect(compileResult.status).toBe(0);
|
|
771
|
+
expect(compileResult.stderr).toBe('');
|
|
772
|
+
}
|
|
773
|
+
finally {
|
|
774
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
it('prefers contract locators and result grid ids over hard-coded zwplace fallbacks', () => {
|
|
778
|
+
const contract = buildZwplaceContractWithHints();
|
|
779
|
+
contract.flows.create.entryButton.locator = 'css=[data-create]';
|
|
780
|
+
contract.flows.create.saveButton.locator = 'css=[data-save-add]';
|
|
781
|
+
contract.pages.add.fields.find((field) => field.name === 'placename').locator = 'css=[data-add-field]';
|
|
782
|
+
contract.pages.list.fields.find((field) => field.name === 'placename').locator = 'css=[data-search-field]';
|
|
783
|
+
contract.pages.edit.fields.find((field) => field.name === 'placename').locator = 'css=[data-edit-field]';
|
|
784
|
+
contract.flows.search.submitControl.locator = 'css=[data-search-submit]';
|
|
785
|
+
contract.flows.search.resultGrid = 'custom-grid';
|
|
786
|
+
contract.flows.update.entryAction.locator = 'xpath=//button[@data-action="edit"]';
|
|
787
|
+
contract.flows.update.saveButton.locator = 'css=[data-save-edit]';
|
|
788
|
+
contract.flows.delete.entryButton.locator = 'css=[data-delete]';
|
|
789
|
+
contract.flows.delete.confirmControl = {
|
|
790
|
+
pageRole: 'list',
|
|
791
|
+
label: '确定',
|
|
792
|
+
locator: 'css=.confirm-delete',
|
|
793
|
+
sources: []
|
|
794
|
+
};
|
|
795
|
+
const script = generateStageSkeletonCrudSpec({
|
|
796
|
+
contract,
|
|
797
|
+
menuNavigation: '场所窗口信息管理>场所窗口信息列表'
|
|
798
|
+
});
|
|
799
|
+
expect(script).toContain('createButton: "css=[data-create]"');
|
|
800
|
+
expect(script).toContain('addLocator: "css=[data-add-field]"');
|
|
801
|
+
expect(script).toContain('editLocator: "css=[data-edit-field]"');
|
|
802
|
+
expect(script).toContain('listLocator: "css=[data-search-field]"');
|
|
803
|
+
expect(script).toContain('addSaveButton: "css=[data-save-add]"');
|
|
804
|
+
expect(script).toContain('listSearchSubmit: "css=[data-search-submit]"');
|
|
805
|
+
expect(script).toContain('listResultValueLocatorTemplate: "xpath=//*[@id=\'custom-grid\']//div[text()=\'{{value}}\']"');
|
|
806
|
+
expect(script).toContain('listEditActionLocatorTemplate: "xpath=//*[@id=\'custom-grid\']//tr[.//div[text()=\'{{value}}\']]//button[@data-action=\\"edit\\"]"');
|
|
807
|
+
expect(script).toContain('editSaveButton: "css=[data-save-edit]"');
|
|
808
|
+
expect(script).toContain('listDeleteButton: "css=[data-delete]"');
|
|
809
|
+
expect(script).toContain("deleteConfirmButton: 'css=.confirm-delete'");
|
|
810
|
+
});
|
|
811
|
+
it('fails fast when contract uses an unsupported data generation strategy', () => {
|
|
812
|
+
const contract = buildZwplaceContractWithHints();
|
|
813
|
+
contract.searchConditions[0].generationStrategy = 'external_data_pool';
|
|
814
|
+
expect(() => generateStageSkeletonCrudSpec({
|
|
815
|
+
contract,
|
|
816
|
+
menuNavigation: '场所窗口信息管理>场所窗口信息列表'
|
|
817
|
+
})).toThrow('当前数据生成策略无法自动映射到 CrudFlowSlots: external_data_pool');
|
|
818
|
+
});
|
|
819
|
+
});
|