@epoint-testtech/ep-stage-skill 0.0.15 → 0.0.17
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/codex-skill/ep-stage/code-list-generate/SKILL.md +1 -1
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +1 -1
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +7 -6
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +100 -38
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +15 -9
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +10 -3
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +11 -0
- package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +13 -2
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +92 -91
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +4 -1
- package/dist/src/cli/calibrate.d.ts +75 -0
- package/dist/src/cli/calibrate.d.ts.map +1 -0
- package/dist/src/cli/calibrate.js +318 -0
- package/dist/src/cli/demote.d.ts +53 -0
- package/dist/src/cli/demote.d.ts.map +1 -0
- package/dist/src/cli/demote.js +111 -0
- package/dist/src/cli/index.d.ts +5 -3
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +45 -6
- package/dist/src/cli/run-report.d.ts +6 -0
- package/dist/src/cli/run-report.d.ts.map +1 -1
- package/dist/src/cli/run-report.js +161 -147
- package/dist/src/cli/run.d.ts +51 -0
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +233 -31
- package/dist/src/contracts/glue-contract-v1.d.ts +37 -1
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +29 -0
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.d.ts +30 -1
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +74 -11
- package/dist/src/runtime/ai-runtime-trace.d.ts +6 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -1
- package/dist/src/testcase/contract-testcase-assembler.d.ts +23 -4
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
- package/dist/src/testcase/contract-testcase-assembler.js +204 -27
- package/dist/src/testcase/query-component-map.d.ts +51 -0
- package/dist/src/testcase/query-component-map.d.ts.map +1 -0
- package/dist/src/testcase/query-component-map.js +64 -0
- package/dist/src/validation/contract.d.ts.map +1 -1
- package/dist/src/validation/contract.js +45 -3
- package/package.json +1 -1
|
@@ -334,6 +334,13 @@ function renderOptionalStringProperty(key, value, level) {
|
|
|
334
334
|
return '';
|
|
335
335
|
return `${' '.repeat(level)}${key}: ${singleQuoted(value)},\n`;
|
|
336
336
|
}
|
|
337
|
+
/** 渲染可选字符串数组属性(如 gridColumns);缺省/空数组不渲染。 */
|
|
338
|
+
function renderStringArrayProperty(key, values, level) {
|
|
339
|
+
if (!values || values.length === 0)
|
|
340
|
+
return '';
|
|
341
|
+
const items = values.map((value) => singleQuoted(value)).join(', ');
|
|
342
|
+
return `${' '.repeat(level)}${key}: [${items}],\n`;
|
|
343
|
+
}
|
|
337
344
|
function renderListboxSelections(selections, level) {
|
|
338
345
|
if (!selections) {
|
|
339
346
|
return '';
|
|
@@ -367,6 +374,50 @@ function renderListboxSelections(selections, level) {
|
|
|
367
374
|
function renderSearchCondition(condition, level) {
|
|
368
375
|
const indent = ' '.repeat(level);
|
|
369
376
|
const innerIndent = ' '.repeat(level + 4);
|
|
377
|
+
if (condition.component === 'datepicker') {
|
|
378
|
+
const lines = [
|
|
379
|
+
`${innerIndent}component: 'datepicker',`,
|
|
380
|
+
`${innerIndent}field: ${singleQuoted(condition.field)},`,
|
|
381
|
+
`${innerIndent}label: ${singleQuoted(condition.label)},`,
|
|
382
|
+
];
|
|
383
|
+
if (condition.listLocator !== undefined) {
|
|
384
|
+
lines.push(`${innerIndent}listLocator: ${locatorLiteral(condition.listLocator)},`);
|
|
385
|
+
}
|
|
386
|
+
if (condition.searchText !== undefined) {
|
|
387
|
+
lines.push(`${innerIndent}searchText: ${singleQuoted(condition.searchText)},`);
|
|
388
|
+
}
|
|
389
|
+
if (condition.triggerSearch !== undefined) {
|
|
390
|
+
lines.push(`${innerIndent}triggerSearch: ${condition.triggerSearch},`);
|
|
391
|
+
}
|
|
392
|
+
if (condition.searchAssertion !== undefined) {
|
|
393
|
+
lines.push(`${innerIndent}searchAssertion: ${singleQuoted(condition.searchAssertion)},`);
|
|
394
|
+
}
|
|
395
|
+
return `${indent}{
|
|
396
|
+
${lines.join('\n')}
|
|
397
|
+
${indent}}`;
|
|
398
|
+
}
|
|
399
|
+
if (condition.component === 'tree') {
|
|
400
|
+
const lines = [
|
|
401
|
+
`${innerIndent}component: 'tree',`,
|
|
402
|
+
`${innerIndent}field: ${singleQuoted(condition.field)},`,
|
|
403
|
+
`${innerIndent}label: ${singleQuoted(condition.label)},`,
|
|
404
|
+
];
|
|
405
|
+
if (condition.treeLocator !== undefined) {
|
|
406
|
+
lines.push(`${innerIndent}treeLocator: ${locatorLiteral(condition.treeLocator)},`);
|
|
407
|
+
}
|
|
408
|
+
if (condition.nodeStrategy !== undefined) {
|
|
409
|
+
lines.push(`${innerIndent}nodeStrategy: ${singleQuoted(condition.nodeStrategy)},`);
|
|
410
|
+
}
|
|
411
|
+
if (condition.searchOnSelect !== undefined) {
|
|
412
|
+
lines.push(`${innerIndent}searchOnSelect: ${condition.searchOnSelect},`);
|
|
413
|
+
}
|
|
414
|
+
if (condition.searchAssertion !== undefined) {
|
|
415
|
+
lines.push(`${innerIndent}searchAssertion: ${singleQuoted(condition.searchAssertion)},`);
|
|
416
|
+
}
|
|
417
|
+
return `${indent}{
|
|
418
|
+
${lines.join('\n')}
|
|
419
|
+
${indent}}`;
|
|
420
|
+
}
|
|
370
421
|
if (condition.component === 'listbox') {
|
|
371
422
|
const lines = [
|
|
372
423
|
`${innerIndent}component: 'listbox',`,
|
|
@@ -433,8 +484,8 @@ ${renderOptionalStringProperty('deleteConfirmButton', slots.locators.deleteConfi
|
|
|
433
484
|
assertions: {
|
|
434
485
|
deletePolicy: ${singleQuoted(slots.assertions.deletePolicy)},
|
|
435
486
|
${renderOptionalStringProperty('blockedDeleteMessage', slots.assertions.blockedDeleteMessage, 12)}
|
|
436
|
-
${slots.assertions.conditionalDelete ? ' conditionalDelete: true,\n' : ''} },
|
|
437
|
-
}`;
|
|
487
|
+
${slots.assertions.conditionalDelete ? ' conditionalDelete: true,\n' : ''}${renderOptionalStringProperty('searchResponseUrlKeyword', slots.assertions.searchResponseUrlKeyword, 12)} },
|
|
488
|
+
${renderStringArrayProperty('gridColumns', slots.gridColumns, 8)} }`;
|
|
438
489
|
}
|
|
439
490
|
function renderChildGridColumns(columns, level) {
|
|
440
491
|
const indent = ' '.repeat(level);
|
|
@@ -793,13 +844,20 @@ function renderCrudCaseTests(cases, context) {
|
|
|
793
844
|
await crudPage.runCreateWorkflow(data);`;
|
|
794
845
|
break;
|
|
795
846
|
case 'read':
|
|
796
|
-
|
|
847
|
+
// 无 create 用例(只读模块,如纯统计报表)时 read 无 workflowData 可依赖,
|
|
848
|
+
// 退化为独立查询:操作查询控件并触发搜索,不断言特定数据命中;
|
|
849
|
+
// 骨架返回执行结论(passed / needs_review 需人工复核),由 body 回传给报告层。
|
|
850
|
+
body = dataKey === ''
|
|
851
|
+
? ` await openModule(page);
|
|
852
|
+
const crudPage = new CrudPage(page, crudSlots);
|
|
853
|
+
return crudPage.runStandaloneReadWorkflowByField(${singleQuoted(key)});`
|
|
854
|
+
: ` await openModule(page);
|
|
797
855
|
const data = workflowDataByKey[${singleQuoted(dataKey)}];
|
|
798
856
|
if (!data) {
|
|
799
857
|
throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
|
|
800
858
|
}
|
|
801
859
|
const crudPage = new CrudPage(page, crudSlots);
|
|
802
|
-
|
|
860
|
+
return crudPage.runReadWorkflowByField(data, ${singleQuoted(key)});`;
|
|
803
861
|
break;
|
|
804
862
|
case 'update':
|
|
805
863
|
body = ` await openModule(page);
|
|
@@ -890,8 +948,8 @@ function renderExportSlotsFromSlot(exportSlot, listIframeSrcKeyword) {
|
|
|
890
948
|
listIframeSrcKeyword: ${singleQuoted(listIframeSrcKeyword)},
|
|
891
949
|
},
|
|
892
950
|
locators: {
|
|
893
|
-
exportButton: '.mini-button.mini-dataexport',
|
|
894
|
-
},
|
|
951
|
+
exportButton: ${singleQuoted(exportSlot.locators?.exportButton ?? '.mini-button.mini-dataexport')},
|
|
952
|
+
${renderOptionalStringProperty('exportContainer', exportSlot.locators?.exportContainer, 12)} },
|
|
895
953
|
export: {
|
|
896
954
|
label: ${singleQuoted(exportSlot.label)},
|
|
897
955
|
mode: ${exportSlot.mode === 'all' ? "'all' as const" : singleQuoted(exportSlot.mode)},
|
|
@@ -943,9 +1001,13 @@ export function generateStageSkeletonCrudSpecFromSlots(options) {
|
|
|
943
1001
|
const exportCaseRef = (glueCases ?? []).find((testCase) => testCase.title.endsWith(' export'));
|
|
944
1002
|
return `import { test, expect, type Page } from '@playwright/test';
|
|
945
1003
|
import { loadCredentials, StagePath, type Credential } from '@epoint-testtech/stage-core';
|
|
946
|
-
import { CrudPage, ExportPage, LoginPage${childGridImport}, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../../skeletons';
|
|
1004
|
+
import { CrudPage, ExportPage, LoginPage${childGridImport}, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData, type LoginSlots } from '../../skeletons';
|
|
947
1005
|
import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../../report/glue-report';
|
|
948
1006
|
|
|
1007
|
+
// loginSlots 为 credentials.json5 的可选扩展字段:stage-core 的 loadCredentials 运行时整体透传,
|
|
1008
|
+
// 类型由 login 骨架本地定义(不依赖 stage-core 导出),此处做交叉类型补齐。
|
|
1009
|
+
type GlueCredential = Credential & { loginSlots?: LoginSlots };
|
|
1010
|
+
|
|
949
1011
|
test.describe(${singleQuoted(suiteTitle)}, () => {
|
|
950
1012
|
const directListUrl = ${singleQuoted(directUrl)};
|
|
951
1013
|
const credentialsPath = \`\${StagePath.projectRootDirPath}/src/tests/${requirementName}/credentials.json5\`;
|
|
@@ -973,7 +1035,7 @@ ${childGridSlots?.length ? ` const childGridSlots: ChildGridFlowSlots[] = ${ren
|
|
|
973
1035
|
recorder.writeReport();
|
|
974
1036
|
});
|
|
975
1037
|
|
|
976
|
-
function resolveCredential(role: string):
|
|
1038
|
+
function resolveCredential(role: string): GlueCredential {
|
|
977
1039
|
const [credential] = loadCredentials(credentialsPath, role);
|
|
978
1040
|
if (!credential) {
|
|
979
1041
|
throw new Error(\`credentials.json5 中未找到角色: \${role}\`);
|
|
@@ -981,15 +1043,16 @@ ${childGridSlots?.length ? ` const childGridSlots: ChildGridFlowSlots[] = ${ren
|
|
|
981
1043
|
return credential;
|
|
982
1044
|
}
|
|
983
1045
|
|
|
984
|
-
function applyLoginCredential(credential:
|
|
1046
|
+
function applyLoginCredential(credential: GlueCredential): void {
|
|
985
1047
|
process.env.LOGIN_SYSTEM_URL = credential.url;
|
|
986
1048
|
process.env.LOGIN_USERNAME = credential.username;
|
|
987
1049
|
process.env.LOGIN_PASSWORD = credential.password;
|
|
988
1050
|
}
|
|
989
1051
|
|
|
990
1052
|
async function openModule(page: Page): Promise<void> {
|
|
991
|
-
|
|
992
|
-
|
|
1053
|
+
const credential = resolveCredential(primaryRole);
|
|
1054
|
+
applyLoginCredential(credential);
|
|
1055
|
+
const loginPage = new LoginPage(page, credential.loginSlots);
|
|
993
1056
|
await loginPage.login();
|
|
994
1057
|
await loginPage.gotoDirectUrl(directListUrl);
|
|
995
1058
|
}
|
|
@@ -22,6 +22,12 @@ export type AiRuntimeResult = {
|
|
|
22
22
|
source: string;
|
|
23
23
|
note: string;
|
|
24
24
|
}>;
|
|
25
|
+
/**
|
|
26
|
+
* 人工复审原因(needs_review 专用):Agent O-D-A-V 的差异化证据结论
|
|
27
|
+
* (字段是否在列表列、默认值、尝试值及来源、搜索前后数量)。
|
|
28
|
+
* CLI 生成的占位 result 无此字段,报告层回退 observation.summary。
|
|
29
|
+
*/
|
|
30
|
+
reviewReason?: string;
|
|
25
31
|
};
|
|
26
32
|
export type RepairRound = {
|
|
27
33
|
round: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-runtime-trace.d.ts","sourceRoot":"","sources":["../../../src/runtime/ai-runtime-trace.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC;AAEjG,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,MAAM,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"ai-runtime-trace.d.ts","sourceRoot":"","sources":["../../../src/runtime/ai-runtime-trace.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC;AAEjG,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,MAAM,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,cAAc,CAAC;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,8BAA8B,CAAC;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB,GAAG,cAAc,CASjB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAGlF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,CAmCA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQvD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CAyBT"}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { ExtractedCodeListJson5 } from '../extractors/code-list-json5.js';
|
|
19
19
|
import type { EpStageContractV1Draft } from '../contracts/glue-contract-v1.js';
|
|
20
|
+
import { type QueryComponentKind } from './query-component-map.js';
|
|
20
21
|
import type { EpStageTestcaseV1Draft } from './testcase-v1-draft.js';
|
|
21
22
|
/** assembler 输入。 */
|
|
22
23
|
export type AssembleInput = {
|
|
@@ -42,23 +43,41 @@ type GlueActionKind = 'create' | 'read' | 'update' | 'delete' | 'export';
|
|
|
42
43
|
export type CrudCaseStage = {
|
|
43
44
|
actionKind: GlueActionKind;
|
|
44
45
|
queryKeyLabel: string;
|
|
45
|
-
component:
|
|
46
|
+
component: QueryComponentKind;
|
|
46
47
|
};
|
|
47
48
|
/**
|
|
48
49
|
* 按查询字段推导 CRUD 用例阶段序列。
|
|
49
50
|
*
|
|
50
|
-
*
|
|
51
|
+
* 规则:input/listbox 且在新增表单中出现的查询字段(可写)产出完整 create/read/update/delete 四条;
|
|
52
|
+
* datepicker/tree 不参与 create/update/delete(createSelections 机制只认 listbox),一律只产 read 一条;
|
|
51
53
|
* 不在表单中的字段(如系统管理的状态)只产出 read 一条。
|
|
52
54
|
* export 不在此计划内,由装配器单独追加。
|
|
53
55
|
*
|
|
54
|
-
* @param searchFields -
|
|
56
|
+
* @param searchFields - 列表页查询字段(有序,仅含白名单组件)。
|
|
55
57
|
* @param formFieldLabels - 新增表单字段标签集合。
|
|
56
58
|
* @returns 有序阶段计划,顺序与 buildGlueCases 产出的用例顺序一一对应。
|
|
57
59
|
*/
|
|
58
60
|
export declare function planCrudCaseStages(searchFields: Array<{
|
|
59
61
|
label: string;
|
|
60
|
-
component:
|
|
62
|
+
component: QueryComponentKind;
|
|
61
63
|
}>, formFieldLabels: string[]): CrudCaseStage[];
|
|
64
|
+
/**
|
|
65
|
+
* 生成 glue 用例执行字段。
|
|
66
|
+
*
|
|
67
|
+
* @param actionKind - CRUD / Export 动作类型。
|
|
68
|
+
* @param queryKey - 当前用例查询主键。
|
|
69
|
+
* @param searchFieldLabels - 列表页查询字段标签集合。
|
|
70
|
+
* @param dataKeyLabel - dataKey 对应的中文标签。
|
|
71
|
+
* @param deleteOutcome - 删除策略(与 skeletonSlots.assertions.deletePolicy 同源),
|
|
72
|
+
* 仅 delete 用例消费;缺省按 success_delete 出文案。
|
|
73
|
+
* @returns testcase 执行字段。
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildGlueExecutionFields(actionKind: GlueActionKind, queryKeyLabel: string, queryComponent: QueryComponentKind, queryKeyInForm: boolean, searchFieldLabels: string[], dataKeyLabel?: string, deleteOutcome?: {
|
|
76
|
+
deletePolicy: 'success_delete' | 'blocked_by_business_rule';
|
|
77
|
+
blockedDeleteMessage?: string;
|
|
78
|
+
/** 条件性删除(如"草稿可删、在用不可删"):删除断言写双预期,运行时按实际结果分流 */
|
|
79
|
+
conditional?: boolean;
|
|
80
|
+
}): Pick<EpStageTestcaseV1Draft['cases'][number], 'queryKey' | 'precondition' | 'steps' | 'assertion'>;
|
|
62
81
|
/**
|
|
63
82
|
* 装配 contract + testcase + inference-trace 账本。
|
|
64
83
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-testcase-assembler.d.ts","sourceRoot":"","sources":["../../../src/testcase/contract-testcase-assembler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAA2E,MAAM,kCAAkC,CAAC;AACxJ,OAAO,KAAK,EAAE,sBAAsB,
|
|
1
|
+
{"version":3,"file":"contract-testcase-assembler.d.ts","sourceRoot":"","sources":["../../../src/testcase/contract-testcase-assembler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAA2E,MAAM,kCAAkC,CAAC;AACxJ,OAAO,KAAK,EAAE,sBAAsB,EAA2C,MAAM,kCAAkC,CAAC;AACxH,OAAO,EAAyB,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE1F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AASrE,oBAAoB;AACpB,MAAM,MAAM,aAAa,GAAG;IAC1B,iCAAiC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;IACjC,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;IACjC,mEAAmE;IACnE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC,CAAC;AA2cF,mBAAmB;AACnB,KAAK,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,kBAAkB,CAAA;CAAE,CAAC,EACrE,eAAe,EAAE,MAAM,EAAE,GACxB,aAAa,EAAE,CAgBjB;AA2CD;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,cAAc,EAC1B,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,OAAO,EACvB,iBAAiB,EAAE,MAAM,EAAE,EAC3B,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE;IACd,YAAY,EAAE,gBAAgB,GAAG,0BAA0B,CAAC;IAC5D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,+CAA+C;IAC/C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,cAAc,GAAG,OAAO,GAAG,WAAW,CAAC,CA8GpG;AAiTD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CA6QrF"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - locators:用 014 草案已确认的框架默认(MiniUI 默认规则 + text=确定 等),不进 unresolvedSlots
|
|
16
16
|
* - assertions.deletePolicy='success_delete'(MiniUI 默认删除成功策略)
|
|
17
17
|
*/
|
|
18
|
+
import { resolveQueryComponent } from './query-component-map.js';
|
|
18
19
|
import { extractHtmlPage } from '../extractors/html-page.js';
|
|
19
20
|
import { inferButtonInteractions, } from './button-interaction-inference.js';
|
|
20
21
|
import { existsSync } from 'node:fs';
|
|
@@ -72,6 +73,33 @@ function buildCrudLocators(listPage) {
|
|
|
72
73
|
deleteConfirmButton: 'text=确定',
|
|
73
74
|
};
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* 从 list 页事实推导导出按钮定位器。
|
|
78
|
+
*
|
|
79
|
+
* 推导优先级(无来源不入契约的落地):
|
|
80
|
+
* 1. toolbarActions 中命中导出组件(backendMethod=getExportModel / component=mini-dataexport / label=导出)
|
|
81
|
+
* 且 elementHints 带 id → `xpath=//*[@id='<id>']`(页面级唯一锚点,最稳);
|
|
82
|
+
* 2. 命中导出组件但无 id 线索 → `a.mini-dataexport`(MiniUI 导出组件实际渲染类名,
|
|
83
|
+
* 历史默认 '.mini-button.mini-dataexport' 多假设了 mini-button 类,实测页面并无该类);
|
|
84
|
+
* 3. 页面无导出事实 → 回退历史框架默认 '.mini-button.mini-dataexport'。
|
|
85
|
+
*/
|
|
86
|
+
function buildExportButtonLocator(listPage) {
|
|
87
|
+
const exportToolbar = (listPage.toolbarActions ?? []).find((t) => {
|
|
88
|
+
return t.backendMethod === 'getExportModel' || t.component === 'mini-dataexport' || t.label === '导出';
|
|
89
|
+
});
|
|
90
|
+
const idHint = exportToolbar?.elementHints?.find((h) => h.attr === 'id' && h.value)?.value;
|
|
91
|
+
if (idHint) {
|
|
92
|
+
return `xpath=//*[@id='${idHint}']`;
|
|
93
|
+
}
|
|
94
|
+
if (exportToolbar?.component === 'mini-dataexport') {
|
|
95
|
+
return 'a.mini-dataexport';
|
|
96
|
+
}
|
|
97
|
+
const exportButton = (listPage.buttons ?? []).find((b) => b.action === 'export');
|
|
98
|
+
if (exportButton) {
|
|
99
|
+
return `xpath=//a[contains(@class,'mini-dataexport')][normalize-space()='${exportButton.label || '导出'}']`;
|
|
100
|
+
}
|
|
101
|
+
return '.mini-button.mini-dataexport';
|
|
102
|
+
}
|
|
75
103
|
/**
|
|
76
104
|
* 从 code_list.pages[role=list/add/edit].pageId 提取 iframe src keyword。
|
|
77
105
|
*
|
|
@@ -275,19 +303,29 @@ function buildChildGridSlots(codeList, sourceRootOverride) {
|
|
|
275
303
|
return slots.length > 0 ? slots : undefined;
|
|
276
304
|
}
|
|
277
305
|
/**
|
|
278
|
-
* 把 list 页 queryFields 映射成 skeletonSlots.crud.searchConditions
|
|
306
|
+
* 把 list 页 queryFields 映射成 skeletonSlots.crud.searchConditions。
|
|
307
|
+
*
|
|
308
|
+
* 白名单语义(query-component-map.ts 单一事实来源):
|
|
309
|
+
* - input/listbox/datepicker/tree → 生成对应槽位;
|
|
310
|
+
* - 未识别组件(显式写了但不在映射表)→ 返回 null 不进槽位,用例由 AI 桶承载;
|
|
311
|
+
* - 上游未写 component → 回退 input(保住 CRUD 锚点主链路),由调用方记 inference-trace。
|
|
279
312
|
*
|
|
280
313
|
* @param codeList - 解析后的 code_list。
|
|
281
314
|
* @param listPage - 列表页 page item。
|
|
282
|
-
* @returns 对齐 StageSkeletonCrudSearchConditionSlot
|
|
315
|
+
* @returns 对齐 StageSkeletonCrudSearchConditionSlot 的槽位数组(null 已过滤)。
|
|
283
316
|
*/
|
|
284
317
|
function buildSearchConditionSlots(codeList, listPage, addOrEditPage) {
|
|
285
318
|
const addFormLabels = new Set((addOrEditPage?.formFields ?? [])
|
|
286
319
|
.filter((f) => typeof f !== 'string')
|
|
287
320
|
.map((f) => f.label));
|
|
288
|
-
return (listPage.queryFields ?? [])
|
|
321
|
+
return (listPage.queryFields ?? [])
|
|
322
|
+
.map((fieldInfo) => {
|
|
289
323
|
const field = mapQueryFieldToField(fieldInfo);
|
|
290
|
-
const
|
|
324
|
+
const resolution = resolveQueryComponent(fieldInfo);
|
|
325
|
+
if (resolution.source === 'unrecognized') {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const component = resolution.kind;
|
|
291
329
|
if (component === 'input') {
|
|
292
330
|
// MiniUI mini-textbox 的 name 属性通常是 `dataBean.<field>` 或省略;只有 label 属性稳定。
|
|
293
331
|
// 用 label 定位 outer div (mini-textbox 容器),再取内部 input.mini-textbox-input。
|
|
@@ -303,6 +341,33 @@ function buildSearchConditionSlots(codeList, listPage, addOrEditPage) {
|
|
|
303
341
|
listLocator: miniTextboxByLabel,
|
|
304
342
|
};
|
|
305
343
|
}
|
|
344
|
+
if (component === 'datepicker') {
|
|
345
|
+
// mini-daterangepicker/mini-datepicker:单输入框形态。优先用 elementHints 的 id 锚点,
|
|
346
|
+
// 缺省按 label 容器派生;运行态若发现默认值无法形成有效过滤证据,再由 demote/ODAV 接管原用例。
|
|
347
|
+
const idHint = (fieldInfo.elementHints ?? []).find((h) => h.attr === 'id' && h.value)?.value;
|
|
348
|
+
return {
|
|
349
|
+
component: 'datepicker',
|
|
350
|
+
field,
|
|
351
|
+
label: fieldInfo.label,
|
|
352
|
+
listLocator: idHint ? `xpath=//*[@id='${idHint}']` : `xpath=//div[@label='${fieldInfo.label}']//input`,
|
|
353
|
+
searchAssertion: 'none',
|
|
354
|
+
triggerSearch: true,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (component === 'tree') {
|
|
358
|
+
// mini-filtertree:左树右表,默认按页面既有 nodeselect 联动执行;若真实页面需要显式搜索,
|
|
359
|
+
// 由运行失败后的 demote/ODAV 保底接管原用例。
|
|
360
|
+
const idHint = (fieldInfo.elementHints ?? []).find((h) => h.attr === 'id' && h.value)?.value;
|
|
361
|
+
return {
|
|
362
|
+
component: 'tree',
|
|
363
|
+
field,
|
|
364
|
+
label: fieldInfo.label,
|
|
365
|
+
...(idHint ? { treeLocator: `#${idHint}` } : {}),
|
|
366
|
+
nodeStrategy: 'first_leaf',
|
|
367
|
+
searchOnSelect: true,
|
|
368
|
+
searchAssertion: 'none',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
306
371
|
// listbox:真实下拉值来自 dataOptions 字典 (运行时才能确认),assembler 不猜值;
|
|
307
372
|
// 用 skeleton 支持的 valueStrategy=first_non_placeholder + skipValues=['请选择'] 让运行时
|
|
308
373
|
// 选首个有效项。searchAssertion='none' 表示 Read 工作流不对 listbox 校验命中数(因为选到
|
|
@@ -329,7 +394,8 @@ function buildSearchConditionSlots(codeList, listPage, addOrEditPage) {
|
|
|
329
394
|
]
|
|
330
395
|
: [],
|
|
331
396
|
};
|
|
332
|
-
})
|
|
397
|
+
})
|
|
398
|
+
.filter((slot) => slot !== null);
|
|
333
399
|
}
|
|
334
400
|
/**
|
|
335
401
|
* 把 code_list 中文 label 映射到契约 field(kebab-ish 英文)。
|
|
@@ -350,30 +416,23 @@ function mapQueryFieldToField(fieldInfo) {
|
|
|
350
416
|
const bindMatch = fieldInfo.bind?.match(/dataBean\.([A-Za-z0-9_]+)/);
|
|
351
417
|
return bindMatch?.[1] ?? mapLabelToField(fieldInfo.label);
|
|
352
418
|
}
|
|
353
|
-
/**
|
|
354
|
-
* 归一化 queryFields 的组件类型,支持上游摘要用 component=mini-textbox/mini-combobox。
|
|
355
|
-
*/
|
|
356
|
-
function normalizeQueryComponent(fieldInfo) {
|
|
357
|
-
const raw = fieldInfo.type ?? fieldInfo.component ?? '';
|
|
358
|
-
if (raw === 'input' || raw === 'mini-textbox')
|
|
359
|
-
return 'input';
|
|
360
|
-
return 'listbox';
|
|
361
|
-
}
|
|
362
419
|
/**
|
|
363
420
|
* 按查询字段推导 CRUD 用例阶段序列。
|
|
364
421
|
*
|
|
365
|
-
*
|
|
422
|
+
* 规则:input/listbox 且在新增表单中出现的查询字段(可写)产出完整 create/read/update/delete 四条;
|
|
423
|
+
* datepicker/tree 不参与 create/update/delete(createSelections 机制只认 listbox),一律只产 read 一条;
|
|
366
424
|
* 不在表单中的字段(如系统管理的状态)只产出 read 一条。
|
|
367
425
|
* export 不在此计划内,由装配器单独追加。
|
|
368
426
|
*
|
|
369
|
-
* @param searchFields -
|
|
427
|
+
* @param searchFields - 列表页查询字段(有序,仅含白名单组件)。
|
|
370
428
|
* @param formFieldLabels - 新增表单字段标签集合。
|
|
371
429
|
* @returns 有序阶段计划,顺序与 buildGlueCases 产出的用例顺序一一对应。
|
|
372
430
|
*/
|
|
373
431
|
export function planCrudCaseStages(searchFields, formFieldLabels) {
|
|
374
432
|
const stages = [];
|
|
375
433
|
for (const field of searchFields) {
|
|
376
|
-
|
|
434
|
+
const canWrite = field.component === 'input' || field.component === 'listbox';
|
|
435
|
+
if (canWrite && formFieldLabels.includes(field.label)) {
|
|
377
436
|
stages.push({ actionKind: 'create', queryKeyLabel: field.label, component: field.component }, { actionKind: 'read', queryKeyLabel: field.label, component: field.component }, { actionKind: 'update', queryKeyLabel: field.label, component: field.component }, { actionKind: 'delete', queryKeyLabel: field.label, component: field.component });
|
|
378
437
|
}
|
|
379
438
|
else {
|
|
@@ -425,7 +484,7 @@ function pickQueryKeyLabel(label, searchFieldLabels, dataKeyLabel) {
|
|
|
425
484
|
* 仅 delete 用例消费;缺省按 success_delete 出文案。
|
|
426
485
|
* @returns testcase 执行字段。
|
|
427
486
|
*/
|
|
428
|
-
function buildGlueExecutionFields(actionKind, queryKeyLabel, queryComponent, queryKeyInForm, searchFieldLabels, dataKeyLabel, deleteOutcome) {
|
|
487
|
+
export function buildGlueExecutionFields(actionKind, queryKeyLabel, queryComponent, queryKeyInForm, searchFieldLabels, dataKeyLabel, deleteOutcome) {
|
|
429
488
|
const queryKey = actionKind === 'export'
|
|
430
489
|
? pickQueryKeyLabel(dataKeyLabel ?? searchFieldLabels[0] ?? '不适用', searchFieldLabels, dataKeyLabel)
|
|
431
490
|
: pickQueryKeyLabel(queryKeyLabel, searchFieldLabels, dataKeyLabel);
|
|
@@ -458,7 +517,7 @@ function buildGlueExecutionFields(actionKind, queryKeyLabel, queryComponent, que
|
|
|
458
517
|
`1. 在列表页${queryKey}查询条件中输入或选择一个有效查询值。`,
|
|
459
518
|
'2. 点击{{查询按钮}}。',
|
|
460
519
|
].join(' '),
|
|
461
|
-
assertion: '
|
|
520
|
+
assertion: '查询请求应真实发出且成功;搜索前后列表总数应可读取并对比——数据量无变化或查询字段不在列表列时,本用例标记为需人工复核。',
|
|
462
521
|
};
|
|
463
522
|
}
|
|
464
523
|
return {
|
|
@@ -659,6 +718,42 @@ function buildUncoveredCandidates(gapCandidates, firstRole, sequence, codeList,
|
|
|
659
718
|
};
|
|
660
719
|
});
|
|
661
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* 为未识别组件的查询字段构建 AI 候选用例。
|
|
723
|
+
*
|
|
724
|
+
* 与 buildUncoveredCandidates(custom 按钮路径)分离:查询字段无按钮元数据可反查,
|
|
725
|
+
* 直接按字段事实生成「操作查询控件并触发查询」的 AI 任务。
|
|
726
|
+
*
|
|
727
|
+
* @param queryFieldResolutions - 查询字段与其组件解析结果。
|
|
728
|
+
* @param firstRole - 首个角色(用作 requiredRole)。
|
|
729
|
+
* @param sequence - 用例编号上下文。
|
|
730
|
+
*/
|
|
731
|
+
function buildQueryComponentUncoveredCandidates(queryFieldResolutions, firstRole, sequence) {
|
|
732
|
+
return queryFieldResolutions
|
|
733
|
+
.filter((r) => r.resolution.source === 'unrecognized')
|
|
734
|
+
.map(({ fieldInfo, resolution }) => {
|
|
735
|
+
const raw = resolution.source === 'unrecognized' ? resolution.raw : '';
|
|
736
|
+
const steps = [
|
|
737
|
+
`1. 在列表页定位「${fieldInfo.label}」查询控件(原始组件 ${raw},骨架 glue 白名单未覆盖)。`,
|
|
738
|
+
'2. 观察控件真实形态并完成一次有效查询值设置(如下拉选项、日期范围、树节点等)。',
|
|
739
|
+
'3. 触发查询(点击查询按钮或控件自带联动)。',
|
|
740
|
+
].join(' ');
|
|
741
|
+
const assertion = `「${fieldInfo.label}」查询链路应可驱动:查询请求被触发,列表刷新或返回结果。`;
|
|
742
|
+
return {
|
|
743
|
+
caseId: nextCaseId(sequence),
|
|
744
|
+
label: `${fieldInfo.label}查询(组件 ${raw})`,
|
|
745
|
+
generationStrategy: 'ai',
|
|
746
|
+
coverageStatus: 'gap',
|
|
747
|
+
requiredRole: firstRole,
|
|
748
|
+
businessIntent: `覆盖查询字段「${fieldInfo.label}」(原始组件 ${raw} 未在 glue 白名单,需运行时观察交互)。`,
|
|
749
|
+
assertionExpectation: assertion,
|
|
750
|
+
queryKey: fieldInfo.label,
|
|
751
|
+
precondition: `1. 使用角色:${firstRole}。 2. 完成系统登录并进入目标待测页面。`,
|
|
752
|
+
steps,
|
|
753
|
+
assertion,
|
|
754
|
+
};
|
|
755
|
+
});
|
|
756
|
+
}
|
|
662
757
|
/**
|
|
663
758
|
* 按 code_list 里的系统与角色事实生成 testcase.testAccess 占位。
|
|
664
759
|
*
|
|
@@ -714,10 +809,27 @@ export function assembleJson5ContractAndTestcase(input) {
|
|
|
714
809
|
codeList.roles[0]?.role ??
|
|
715
810
|
'业务角色待确认';
|
|
716
811
|
const directUrl = resolveDirectUrl(input, listPage);
|
|
717
|
-
|
|
812
|
+
// 组件形态解析:白名单(input/listbox/datepicker/tree)写规范 kind 进契约;
|
|
813
|
+
// input/listbox 进入 glue,datepicker/tree 的查询语义由 ODAV 保底;
|
|
814
|
+
// 上游显式写了但未识别的组件保留原始字符串,用例路由 AI 桶,不再静默压扁成 listbox。
|
|
815
|
+
const queryFieldResolutions = (listPage.queryFields ?? []).map((fieldInfo) => ({
|
|
816
|
+
fieldInfo,
|
|
817
|
+
resolution: resolveQueryComponent(fieldInfo),
|
|
818
|
+
}));
|
|
819
|
+
const searchFields = queryFieldResolutions.map(({ fieldInfo, resolution }) => ({
|
|
718
820
|
field: mapQueryFieldToField(fieldInfo),
|
|
719
821
|
label: fieldInfo.label,
|
|
720
|
-
component:
|
|
822
|
+
component: resolution.source === 'unrecognized'
|
|
823
|
+
? resolution.raw
|
|
824
|
+
: resolution.kind,
|
|
825
|
+
}));
|
|
826
|
+
// glue 通道承载所有已识别的通用查询字段;若运行态证明某个字段的骨架动作不足,
|
|
827
|
+
// 再通过 demote/ODAV 接管原 glue 用例,不在装配期拆成第二条用例。
|
|
828
|
+
const glueSearchFields = queryFieldResolutions
|
|
829
|
+
.filter((r) => r.resolution.source !== 'unrecognized')
|
|
830
|
+
.map(({ fieldInfo, resolution }) => ({
|
|
831
|
+
label: fieldInfo.label,
|
|
832
|
+
component: resolution.kind,
|
|
721
833
|
}));
|
|
722
834
|
const searchFieldLabels = searchFields.map((fieldInfo) => fieldInfo.label);
|
|
723
835
|
const addOrEditPage = codeList.pages.find((p) => p.role === 'add') ?? codeList.pages.find((p) => p.role === 'edit');
|
|
@@ -766,10 +878,51 @@ export function assembleJson5ContractAndTestcase(input) {
|
|
|
766
878
|
// 条件性删除标记:骨架 runDeleteWorkflow 据此在运行时按「记录是否仍存在」分流断言
|
|
767
879
|
// (存在 → 断言阻断文案;不存在 → 断言删除成功),而非 slots 级单一预期。
|
|
768
880
|
...(conditionalDelete ? { conditionalDelete: true } : {}),
|
|
881
|
+
// 查询请求级断言关键字:FUI 框架的查询请求走 `/rest/<模块路径>/<restController>/page_Refresh`
|
|
882
|
+
// 通用端点(方法名在加密 body 里,不在 URL——001 运行时实据),因此用 initAction
|
|
883
|
+
// (restController 名,出现在 URL 路径)作关键字;initAction 缺失时回退 dataGrid.backendMethod。
|
|
884
|
+
...((listPage.initAction ?? listPage.dataGrid?.backendMethod)
|
|
885
|
+
? { searchResponseUrlKeyword: listPage.initAction ?? listPage.dataGrid?.backendMethod }
|
|
886
|
+
: {}),
|
|
769
887
|
},
|
|
770
888
|
};
|
|
889
|
+
// 列表页列头标签:搜索校验的「字段列可见性」判定依据(查询字段不在列中时骨架标记需人工复核)
|
|
890
|
+
const gridColumns = (listPage.dataGrid?.columns ?? [])
|
|
891
|
+
.map((column) => column.label)
|
|
892
|
+
.filter((label) => Boolean(label));
|
|
893
|
+
if (gridColumns.length > 0) {
|
|
894
|
+
skeletonSlotsCrud.gridColumns = gridColumns;
|
|
895
|
+
}
|
|
771
896
|
const childGridSlots = buildChildGridSlots(codeList, input.sourceRoot);
|
|
772
|
-
//
|
|
897
|
+
// unresolvedSlots:装配期无法确认的槽位显式登记(契约可观测性 gate)。
|
|
898
|
+
const unresolvedSlots = [];
|
|
899
|
+
// 1) 未识别查询组件 → AI 通道路由登记
|
|
900
|
+
for (const { fieldInfo, resolution } of queryFieldResolutions) {
|
|
901
|
+
if (resolution.source !== 'unrecognized')
|
|
902
|
+
continue;
|
|
903
|
+
unresolvedSlots.push({
|
|
904
|
+
slotPath: `scenario.searchFields[${fieldInfo.label}]`,
|
|
905
|
+
kind: 'unsupported_query_component',
|
|
906
|
+
rawComponent: resolution.raw,
|
|
907
|
+
reason: `查询字段「${fieldInfo.label}」的组件 ${resolution.raw} 不在 glue 白名单映射表(query-component-map.ts),用例改走 AI 通道。`,
|
|
908
|
+
suggestedAction: 'ai_runtime',
|
|
909
|
+
detectedAt: 'assemble',
|
|
910
|
+
evidence: { source: 'code_list', ref: `pages[role=list].queryFields[${fieldInfo.label}]` },
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
// 2) 多列表页显式化:当前管线只装配第一个 list 页,其余登记待人工拆分
|
|
914
|
+
const listPages = codeList.pages.filter((p) => p.role === 'list');
|
|
915
|
+
if (listPages.length > 1) {
|
|
916
|
+
unresolvedSlots.push({
|
|
917
|
+
slotPath: 'scenario.skeletonSlots.crud',
|
|
918
|
+
kind: 'multi_list_page',
|
|
919
|
+
reason: `code_list 含 ${listPages.length} 个 role=list 页面,当前管线仅装配第一个(${listPage.pageId ?? listPage.path}),跳过:${listPages.slice(1).map((p) => p.pageId ?? p.path).join('、')}。`,
|
|
920
|
+
suggestedAction: 'manual_split',
|
|
921
|
+
detectedAt: 'assemble',
|
|
922
|
+
evidence: { source: 'code_list', ref: 'pages[role=list]' },
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
// 动态推导 gapCandidates:当前阶段保留控件类 custom 按钮 + 未识别组件查询字段。
|
|
773
926
|
// 状态流转、子表、详情、统计看板/移动端等 PRD-only/业务类场景暂不进 gapCandidates,
|
|
774
927
|
// 后续可通过 code_list.gapHints 让业务人工声明后再扩展。
|
|
775
928
|
const gapCandidates = [];
|
|
@@ -783,6 +936,16 @@ export function assembleJson5ContractAndTestcase(input) {
|
|
|
783
936
|
generationStrategy: 'ai',
|
|
784
937
|
});
|
|
785
938
|
});
|
|
939
|
+
// 未识别组件查询字段进 AI 桶(gapId 规范:query-component:<field>)
|
|
940
|
+
for (const { fieldInfo, resolution } of queryFieldResolutions) {
|
|
941
|
+
if (resolution.source !== 'unrecognized')
|
|
942
|
+
continue;
|
|
943
|
+
gapCandidates.push({
|
|
944
|
+
gapId: `query-component:${mapQueryFieldToField(fieldInfo)}`,
|
|
945
|
+
label: fieldInfo.label,
|
|
946
|
+
generationStrategy: 'ai',
|
|
947
|
+
});
|
|
948
|
+
}
|
|
786
949
|
const contract = {
|
|
787
950
|
schemaVersion: 'ep-stage-contract/v1-draft',
|
|
788
951
|
contractId: `${module}-contract`,
|
|
@@ -800,15 +963,20 @@ export function assembleJson5ContractAndTestcase(input) {
|
|
|
800
963
|
searchFields,
|
|
801
964
|
skeletonSlots: {
|
|
802
965
|
crud: skeletonSlotsCrud,
|
|
803
|
-
export: {
|
|
966
|
+
export: {
|
|
967
|
+
moduleId: module,
|
|
968
|
+
label: '导出',
|
|
969
|
+
mode: 'all',
|
|
970
|
+
locators: { exportButton: buildExportButtonLocator(listPage) },
|
|
971
|
+
},
|
|
804
972
|
childGrids: childGridSlots,
|
|
805
973
|
},
|
|
806
974
|
gapCandidates,
|
|
807
975
|
},
|
|
808
|
-
unresolvedSlots
|
|
976
|
+
unresolvedSlots,
|
|
809
977
|
};
|
|
810
978
|
const caseIdSequence = { pageIndex: 1, nextCaseIndex: 1 };
|
|
811
|
-
const cases = buildGlueCases(moduleName,
|
|
979
|
+
const cases = buildGlueCases(moduleName, glueSearchFields, formFieldLabels, caseIdSequence, dataKeyLabel, {
|
|
812
980
|
deletePolicy,
|
|
813
981
|
blockedDeleteMessage,
|
|
814
982
|
conditional: conditionalDelete,
|
|
@@ -819,6 +987,8 @@ export function assembleJson5ContractAndTestcase(input) {
|
|
|
819
987
|
sourceRootOverride: input.sourceRoot,
|
|
820
988
|
});
|
|
821
989
|
const uncoveredCandidates = buildUncoveredCandidates(gapCandidates, firstRole, caseIdSequence, codeList, buttonInferences);
|
|
990
|
+
// 未识别组件查询字段的 AI 候选(按钮反查路径不适用,独立构建)
|
|
991
|
+
uncoveredCandidates.push(...buildQueryComponentUncoveredCandidates(queryFieldResolutions, firstRole, caseIdSequence));
|
|
822
992
|
const testcase = {
|
|
823
993
|
schemaVersion: 'ep-stage-testcase/v1-draft',
|
|
824
994
|
testcaseId: `${module}-testcase`,
|
|
@@ -843,20 +1013,27 @@ export function assembleJson5ContractAndTestcase(input) {
|
|
|
843
1013
|
source: 'framework-default',
|
|
844
1014
|
reason: '014 草案已确认 MiniUI 默认规则 + text=确定 确认按钮 + addAutofillTrigger 默认能力',
|
|
845
1015
|
},
|
|
1016
|
+
exportButtonLocator: {
|
|
1017
|
+
source: 'code-list-derived',
|
|
1018
|
+
reason: '优先取 list 页 toolbarActions 导出组件的 elementHints id;无 id 时按 mini-dataexport 实际渲染类名兜底',
|
|
1019
|
+
},
|
|
846
1020
|
assertions: {
|
|
847
1021
|
source: 'framework-default',
|
|
848
1022
|
reason: "deletePolicy='success_delete'(MiniUI 默认删除成功策略)",
|
|
849
1023
|
},
|
|
850
1024
|
queryFields: {
|
|
851
1025
|
source: 'code-list-derived',
|
|
852
|
-
reason: '
|
|
1026
|
+
reason: '组件形态经 query-component-map.ts 显式映射:白名单(input/listbox/datepicker/tree)进契约槽位;' +
|
|
1027
|
+
'所有已识别组件进入 glue,用例先走通用骨架;运行态若语义超出骨架,再由同一 caseId 的 ODAV 接管;' +
|
|
1028
|
+
'未识别组件进 AI 桶并登记 unresolvedSlots;上游未写 component 回退 input(defaulted,待 calibrate 运行时纠正)。' +
|
|
1029
|
+
'input 的 dataKey 字段用 timestamp_prefix;listbox 取首项作 value',
|
|
853
1030
|
},
|
|
854
1031
|
childGrids: {
|
|
855
1032
|
source: 'html-derived',
|
|
856
1033
|
reason: '从 add/edit 页 HTML 的 datagrid 列 editor.type 推导 component;locators 按 MiniUI 子表默认规则生成',
|
|
857
1034
|
},
|
|
858
1035
|
},
|
|
859
|
-
unresolvedSlotsCount:
|
|
1036
|
+
unresolvedSlotsCount: unresolvedSlots.length,
|
|
860
1037
|
customButtonInferences: Array.from(buttonInferences.values()).map((inf) => ({
|
|
861
1038
|
label: inf.label,
|
|
862
1039
|
pattern: inf.pattern,
|