@epoint-testtech/ep-stage-skill 0.0.4-alpha.1 → 0.0.6
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 -206
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -274
- 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 -112
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -72
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +295 -289
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -0
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -145
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -43
- 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 +166 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +91 -0
- package/codex-skill/ep-stage/page-element-scan/evals/evals.json +37 -0
- package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +85 -0
- package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +153 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +298 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +749 -0
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +98 -95
- package/dist/src/cli/generate-testcase.d.ts +17 -51
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +74 -181
- package/dist/src/cli/index.d.ts +3 -2
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +51 -17
- package/dist/src/cli/run.d.ts +136 -29
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +507 -65
- package/dist/src/cli/spec.d.ts +17 -13
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +59 -33
- package/dist/src/contracts/crud-business-module.d.ts +6 -0
- package/dist/src/contracts/crud-business-module.d.ts.map +1 -1
- package/dist/src/contracts/glue-contract-v1.d.ts +98 -0
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -0
- package/dist/src/contracts/glue-contract-v1.js +9 -0
- package/dist/src/contracts/observable-chain.d.ts +4 -0
- package/dist/src/contracts/observable-chain.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +192 -0
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -0
- package/dist/src/extractors/code-list-json5.js +91 -0
- package/dist/src/extractors/html-page.d.ts +9 -0
- package/dist/src/extractors/html-page.d.ts.map +1 -1
- package/dist/src/extractors/html-page.js +26 -2
- package/dist/src/extractors/prd-extractor.d.ts +70 -0
- package/dist/src/extractors/prd-extractor.d.ts.map +1 -0
- package/dist/src/extractors/prd-extractor.js +248 -0
- package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
- package/dist/src/generators/stage-skeleton-script.d.ts +87 -20
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +647 -238
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/normalizers/crud-contract.d.ts +3 -0
- package/dist/src/normalizers/crud-contract.d.ts.map +1 -1
- package/dist/src/normalizers/crud-contract.js +3 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts +88 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -0
- package/dist/src/runtime/ai-runtime-trace.js +119 -0
- package/dist/src/testcase/button-interaction-inference.d.ts +37 -0
- package/dist/src/testcase/button-interaction-inference.d.ts.map +1 -0
- package/dist/src/testcase/button-interaction-inference.js +385 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts +44 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -0
- package/dist/src/testcase/contract-testcase-assembler.js +806 -0
- package/dist/src/testcase/prd-button-mapper.d.ts +72 -0
- package/dist/src/testcase/prd-button-mapper.d.ts.map +1 -0
- package/dist/src/testcase/prd-button-mapper.js +148 -0
- package/dist/src/testcase/runtime-context.d.ts +9 -0
- package/dist/src/testcase/runtime-context.d.ts.map +1 -0
- package/dist/src/testcase/runtime-context.js +1 -0
- package/dist/src/testcase/test-access.d.ts +38 -0
- package/dist/src/testcase/test-access.d.ts.map +1 -0
- package/dist/src/testcase/test-access.js +106 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts +10 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts.map +1 -0
- package/dist/src/testcase/testcase-execution-fields.js +1 -0
- package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
- package/dist/src/testcase/testcase-generator.js +6 -0
- package/dist/src/testcase/testcase-markdown.d.ts +11 -0
- package/dist/src/testcase/testcase-markdown.d.ts.map +1 -0
- package/dist/src/testcase/testcase-markdown.js +181 -0
- package/dist/src/testcase/testcase-skeleton.d.ts +4 -0
- package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -1
- package/dist/src/testcase/testcase-spec-assembly.d.ts +4 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -1
- package/dist/src/testcase/testcase-v1-draft.d.ts +58 -0
- package/dist/src/testcase/testcase-v1-draft.d.ts.map +1 -0
- package/dist/src/testcase/testcase-v1-draft.js +13 -0
- package/dist/src/validation/contract.d.ts +14 -0
- package/dist/src/validation/contract.d.ts.map +1 -0
- package/dist/src/validation/contract.js +132 -0
- package/dist/src/validation/credentials.d.ts +7 -2
- package/dist/src/validation/credentials.d.ts.map +1 -1
- package/dist/src/validation/credentials.js +15 -4
- package/dist/src/validation/index.d.ts +1 -0
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +1 -0
- package/dist/src/validation/testcase.d.ts +9 -7
- package/dist/src/validation/testcase.d.ts.map +1 -1
- package/dist/src/validation/testcase.js +181 -34
- package/docs/README.md +124 -134
- package/docs/mvp-usage-guide.md +298 -298
- 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 +2 -2
- package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +0 -10
- package/dist/test/cli/extract-contract.test.d.ts +0 -2
- package/dist/test/cli/extract-contract.test.d.ts.map +0 -1
- package/dist/test/cli/extract-contract.test.js +0 -32
- package/dist/test/cli/generate-testcase-inference.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase-inference.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase-inference.test.js +0 -63
- package/dist/test/cli/generate-testcase-paths.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase-paths.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase-paths.test.js +0 -165
- package/dist/test/cli/generate-testcase-requirements.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase-requirements.test.js +0 -229
- package/dist/test/cli/generate-testcase.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase.test.js +0 -130
- package/dist/test/cli/index.test.d.ts +0 -2
- package/dist/test/cli/index.test.d.ts.map +0 -1
- package/dist/test/cli/index.test.js +0 -113
- package/dist/test/cli/probe.test.d.ts +0 -2
- package/dist/test/cli/probe.test.d.ts.map +0 -1
- package/dist/test/cli/probe.test.js +0 -31
- package/dist/test/cli/run.test.d.ts +0 -2
- package/dist/test/cli/run.test.d.ts.map +0 -1
- package/dist/test/cli/run.test.js +0 -149
- package/dist/test/cli/spec-boundary.test.d.ts +0 -2
- package/dist/test/cli/spec-boundary.test.d.ts.map +0 -1
- package/dist/test/cli/spec-boundary.test.js +0 -198
- package/dist/test/cli/spec.test.d.ts +0 -2
- package/dist/test/cli/spec.test.d.ts.map +0 -1
- package/dist/test/cli/spec.test.js +0 -196
- package/dist/test/code-list.test.d.ts +0 -2
- package/dist/test/code-list.test.d.ts.map +0 -1
- package/dist/test/code-list.test.js +0 -39
- package/dist/test/crud-contract.test.d.ts +0 -2
- package/dist/test/crud-contract.test.d.ts.map +0 -1
- package/dist/test/crud-contract.test.js +0 -799
- package/dist/test/gap-inference.test.d.ts +0 -2
- package/dist/test/gap-inference.test.d.ts.map +0 -1
- package/dist/test/gap-inference.test.js +0 -597
- package/dist/test/generalization.test.d.ts +0 -2
- package/dist/test/generalization.test.d.ts.map +0 -1
- package/dist/test/generalization.test.js +0 -73
- package/dist/test/inference/module-hints-inference.test.d.ts +0 -2
- package/dist/test/inference/module-hints-inference.test.d.ts.map +0 -1
- package/dist/test/inference/module-hints-inference.test.js +0 -25
- package/dist/test/inference/source-analysis.test.d.ts +0 -2
- package/dist/test/inference/source-analysis.test.d.ts.map +0 -1
- package/dist/test/inference/source-analysis.test.js +0 -23
- package/dist/test/material-inventory.test.d.ts +0 -2
- package/dist/test/material-inventory.test.d.ts.map +0 -1
- package/dist/test/material-inventory.test.js +0 -154
- package/dist/test/observable-chain.test.d.ts +0 -2
- package/dist/test/observable-chain.test.d.ts.map +0 -1
- package/dist/test/observable-chain.test.js +0 -123
- package/dist/test/observable-pipeline.test.d.ts +0 -2
- package/dist/test/observable-pipeline.test.d.ts.map +0 -1
- package/dist/test/observable-pipeline.test.js +0 -470
- package/dist/test/page-structure.test.d.ts +0 -2
- package/dist/test/page-structure.test.d.ts.map +0 -1
- package/dist/test/page-structure.test.js +0 -45
- package/dist/test/scenario-inference.test.d.ts +0 -2
- package/dist/test/scenario-inference.test.d.ts.map +0 -1
- package/dist/test/scenario-inference.test.js +0 -73
- package/dist/test/stage-context.test.d.ts +0 -2
- package/dist/test/stage-context.test.d.ts.map +0 -1
- package/dist/test/stage-context.test.js +0 -395
- package/dist/test/testcase-generator.test.d.ts +0 -2
- package/dist/test/testcase-generator.test.d.ts.map +0 -1
- package/dist/test/testcase-generator.test.js +0 -276
- package/dist/test/testcase-skeleton.test.d.ts +0 -2
- package/dist/test/testcase-skeleton.test.d.ts.map +0 -1
- package/dist/test/testcase-skeleton.test.js +0 -185
- package/dist/test/testcase-spec-assembly.test.d.ts +0 -2
- package/dist/test/testcase-spec-assembly.test.d.ts.map +0 -1
- package/dist/test/testcase-spec-assembly.test.js +0 -105
- package/dist/test/util/credentials.test.d.ts +0 -2
- package/dist/test/util/credentials.test.d.ts.map +0 -1
- package/dist/test/util/credentials.test.js +0 -64
- package/dist/test/util/i18n-testcase.test.d.ts +0 -2
- package/dist/test/util/i18n-testcase.test.d.ts.map +0 -1
- package/dist/test/util/i18n-testcase.test.js +0 -119
- package/dist/test/util/softlink.test.d.ts +0 -2
- package/dist/test/util/softlink.test.d.ts.map +0 -1
- package/dist/test/util/softlink.test.js +0 -82
- package/dist/test/validation/credentials.test.d.ts +0 -2
- package/dist/test/validation/credentials.test.d.ts.map +0 -1
- package/dist/test/validation/credentials.test.js +0 -72
- package/dist/test/validation/projects-index.test.d.ts +0 -2
- package/dist/test/validation/projects-index.test.d.ts.map +0 -1
- package/dist/test/validation/projects-index.test.js +0 -48
- package/dist/test/validation/testcase.test.d.ts +0 -2
- package/dist/test/validation/testcase.test.d.ts.map +0 -1
- package/dist/test/validation/testcase.test.js +0 -129
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -7
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code_list.json5 输入读取器
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. assertCodeListJson5Path — 路径硬校验,拒绝非 .json5 文件(含 .md)
|
|
6
|
+
* 2. extractCodeListJson5 — 读取并解析 JSON5 文件,校验必填字段,返回强类型产出
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import JSON5 from 'json5';
|
|
11
|
+
import { normalizeTestAccessConfig, } from '../testcase/test-access.js';
|
|
12
|
+
/* ──────── 路径硬校验 ──────── */
|
|
13
|
+
/**
|
|
14
|
+
* 校验 filePath 是否为合法的 code_list.json5 路径。
|
|
15
|
+
* 拒绝非 .json5 后缀的文件(包括 .md)。
|
|
16
|
+
*
|
|
17
|
+
* @param filePath - 待校验的文件路径
|
|
18
|
+
* @throws Error 当文件后缀不是 .json5 时抛出中文错误
|
|
19
|
+
*/
|
|
20
|
+
export function assertCodeListJson5Path(filePath) {
|
|
21
|
+
if (!filePath.endsWith('.json5')) {
|
|
22
|
+
throw new Error('glue-generate-testcase 只接受 _docs/code_list.json5');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/* ──────── JSON5 读取与解析 ──────── */
|
|
26
|
+
/**
|
|
27
|
+
* 读取并解析 code_list.json5 文件,返回强类型产出。
|
|
28
|
+
* 校验必填字段 module / moduleName / systems / pages,缺失时抛出中文错误。
|
|
29
|
+
*
|
|
30
|
+
* @param filePath - code_list.json5 文件的绝对路径
|
|
31
|
+
* @returns 解析后的强类型数据
|
|
32
|
+
* @throws Error 路径非法 / 文件不存在 / 必填字段缺失
|
|
33
|
+
*/
|
|
34
|
+
export function extractCodeListJson5Object(raw) {
|
|
35
|
+
if (!raw || typeof raw !== 'object') {
|
|
36
|
+
throw new Error('code_list.json5 顶层必须为对象');
|
|
37
|
+
}
|
|
38
|
+
const data = raw;
|
|
39
|
+
// 必填字段校验
|
|
40
|
+
if (!data.module) {
|
|
41
|
+
throw new Error('code_list.json5 缺少必填字段: module');
|
|
42
|
+
}
|
|
43
|
+
if (!data.moduleName) {
|
|
44
|
+
throw new Error('code_list.json5 缺少必填字段: moduleName');
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(data.systems)) {
|
|
47
|
+
throw new Error('code_list.json5 缺少必填字段: systems');
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(data.pages)) {
|
|
50
|
+
throw new Error('code_list.json5 缺少必填字段: pages');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
module: data.module,
|
|
54
|
+
moduleName: data.moduleName,
|
|
55
|
+
description: data.description,
|
|
56
|
+
requirement: data.requirement,
|
|
57
|
+
sourceRoot: data.sourceRoot,
|
|
58
|
+
systems: data.systems,
|
|
59
|
+
roles: (data.roles ?? []),
|
|
60
|
+
pages: data.pages,
|
|
61
|
+
dataModels: data.dataModels,
|
|
62
|
+
entities: data.entities,
|
|
63
|
+
businessRules: data.businessRules,
|
|
64
|
+
testAccess: data.testAccess === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: normalizeTestAccessConfig(data.testAccess, 'code_list.json5.testAccess'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 读取并解析 code_list.json5 文件,返回强类型产出。
|
|
71
|
+
* 校验必填字段 module / moduleName / systems / pages,缺失时抛出中文错误。
|
|
72
|
+
*
|
|
73
|
+
* @param filePath - code_list.json5 文件的绝对路径
|
|
74
|
+
* @returns 解析后的强类型数据
|
|
75
|
+
* @throws Error 路径非法 / 文件不存在 / 必填字段缺失
|
|
76
|
+
*/
|
|
77
|
+
export function extractCodeListJson5(filePath) {
|
|
78
|
+
assertCodeListJson5Path(filePath);
|
|
79
|
+
const absPath = path.resolve(filePath);
|
|
80
|
+
if (!existsSync(absPath)) {
|
|
81
|
+
throw new Error(`code_list.json5 文件不存在: ${absPath}`);
|
|
82
|
+
}
|
|
83
|
+
const raw = readFileSync(absPath, 'utf-8');
|
|
84
|
+
const parsed = JSON5.parse(raw);
|
|
85
|
+
const extracted = extractCodeListJson5Object(parsed);
|
|
86
|
+
return {
|
|
87
|
+
path: absPath,
|
|
88
|
+
requirementDir: path.dirname(absPath),
|
|
89
|
+
...extracted,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -23,6 +23,8 @@ export type ExtractedHtmlGridColumn = {
|
|
|
23
23
|
field: string;
|
|
24
24
|
headerText: string;
|
|
25
25
|
columnType: string;
|
|
26
|
+
editorType?: string;
|
|
27
|
+
editorAction?: string;
|
|
26
28
|
};
|
|
27
29
|
export type ExtractedHtmlRowAction = {
|
|
28
30
|
label: string;
|
|
@@ -36,6 +38,13 @@ export type ExtractedHtmlGrid = {
|
|
|
36
38
|
selectionColumn?: ExtractedHtmlGridColumn;
|
|
37
39
|
indexColumn?: ExtractedHtmlGridColumn;
|
|
38
40
|
rowActions: ExtractedHtmlRowAction[];
|
|
41
|
+
/** MiniUI cell-edit 信号 */
|
|
42
|
+
allowCellEdit?: boolean;
|
|
43
|
+
cellEditAction?: string;
|
|
44
|
+
allowCellSelect?: boolean;
|
|
45
|
+
editNextOnEnterKey?: boolean;
|
|
46
|
+
oncellbeginedit?: boolean;
|
|
47
|
+
onrowdblclick?: boolean;
|
|
39
48
|
};
|
|
40
49
|
export type ExtractedPageStructureSignal = {
|
|
41
50
|
staticPresent: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-page.d.ts","sourceRoot":"","sources":["../../../src/extractors/html-page.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"html-page.d.ts","sourceRoot":"","sources":["../../../src/extractors/html-page.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,6 +38,17 @@ function buildButtonLocator(label, tagName) {
|
|
|
38
38
|
}
|
|
39
39
|
return `xpath=//a[contains(@class,'mini-button')][normalize-space()='${label}'] | //button[contains(@class,'mini-button')][normalize-space()='${label}']`;
|
|
40
40
|
}
|
|
41
|
+
function parseEditorAttribute(editorAttribute) {
|
|
42
|
+
const normalized = normalizeAttribute(editorAttribute);
|
|
43
|
+
if (!normalized)
|
|
44
|
+
return {};
|
|
45
|
+
const typeMatch = normalized.match(/type\s*:\s*['"]([^'"]+)['"]/i);
|
|
46
|
+
const actionMatch = normalized.match(/action\s*:\s*['"]([^'"]+)['"]/i);
|
|
47
|
+
return {
|
|
48
|
+
type: typeMatch?.[1],
|
|
49
|
+
action: actionMatch?.[1],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
41
52
|
function buildFieldLocator(label, controlType) {
|
|
42
53
|
if (controlType === 'mini-textbox') {
|
|
43
54
|
return `//div[@label='${label}']//input[@class='mini-textbox-input']`;
|
|
@@ -116,7 +127,14 @@ export function extractHtmlPage(filePath) {
|
|
|
116
127
|
const field = column.attr('field') ?? '';
|
|
117
128
|
const columnType = column.attr('type') ?? (field ? 'data' : 'unknown');
|
|
118
129
|
const headerText = normalizeText(column.clone().children().remove().end().text());
|
|
119
|
-
const
|
|
130
|
+
const editor = parseEditorAttribute(column.attr('editor'));
|
|
131
|
+
const extractedColumn = {
|
|
132
|
+
field,
|
|
133
|
+
headerText,
|
|
134
|
+
columnType,
|
|
135
|
+
editorType: editor.type,
|
|
136
|
+
editorAction: editor.action,
|
|
137
|
+
};
|
|
120
138
|
if (columnType === 'checkcolumn') {
|
|
121
139
|
selectionColumn = extractedColumn;
|
|
122
140
|
return;
|
|
@@ -149,7 +167,13 @@ export function extractHtmlPage(filePath) {
|
|
|
149
167
|
dataColumns,
|
|
150
168
|
selectionColumn,
|
|
151
169
|
indexColumn,
|
|
152
|
-
rowActions
|
|
170
|
+
rowActions,
|
|
171
|
+
allowCellEdit: booleanAttribute(grid.attr('allowCellEdit')) || undefined,
|
|
172
|
+
cellEditAction: normalizeAttribute(grid.attr('cellEditAction')) || undefined,
|
|
173
|
+
allowCellSelect: booleanAttribute(grid.attr('allowCellSelect')) || undefined,
|
|
174
|
+
editNextOnEnterKey: booleanAttribute(grid.attr('editNextOnEnterKey')) || undefined,
|
|
175
|
+
oncellbeginedit: normalizeAttribute(grid.attr('oncellbeginedit')) ? true : undefined,
|
|
176
|
+
onrowdblclick: normalizeAttribute(grid.attr('onrowdblclick')) ? true : undefined,
|
|
153
177
|
});
|
|
154
178
|
});
|
|
155
179
|
const scriptContent = $('script')
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD markdown 解析器。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 在需求目录中发现 PRD markdown 文件。
|
|
6
|
+
* 2. 解析 PRD 章节 4.1(功能列表)和 4.2.x(功能详细说明)。
|
|
7
|
+
* 3. 输出结构化的 PrdFunctionEntry[],供 testcase 装配阶段确定性使用。
|
|
8
|
+
*
|
|
9
|
+
* 设计取舍:
|
|
10
|
+
* - 全程使用正则/行解析,不调用 LLM,保证幂等性。
|
|
11
|
+
* - 只识别当前观察到的 PRD 结构(`4.1` 表格 + `4.2.x` 子章节 + 粗体字段标题)。
|
|
12
|
+
* - 解析失败时返回空数组并在 inferenceTrace 中记录,不阻断 pipeline。
|
|
13
|
+
*/
|
|
14
|
+
/** PRD 中的单个功能条目。 */
|
|
15
|
+
export type PrdFunctionEntry = {
|
|
16
|
+
/** 功能编号,如 F001。 */
|
|
17
|
+
featureId: string;
|
|
18
|
+
/** 功能名称,如「同步窗口管理系统」。 */
|
|
19
|
+
featureName: string;
|
|
20
|
+
/** 功能模块,如「大厅地址管理」。 */
|
|
21
|
+
featureModule?: string;
|
|
22
|
+
/** 功能描述。 */
|
|
23
|
+
description: string;
|
|
24
|
+
/** 输入条件。 */
|
|
25
|
+
inputConditions?: string;
|
|
26
|
+
/** 处理逻辑,保留列表顺序。 */
|
|
27
|
+
processingLogic: string[];
|
|
28
|
+
/** 输出结果,保留列表顺序。 */
|
|
29
|
+
outputResult: string[];
|
|
30
|
+
/** 业务规则,保留列表顺序。 */
|
|
31
|
+
businessRules: string[];
|
|
32
|
+
/** 约束条件,保留列表顺序。 */
|
|
33
|
+
constraints: string[];
|
|
34
|
+
};
|
|
35
|
+
/** PRD 提取产物。 */
|
|
36
|
+
export type PrdExtraction = {
|
|
37
|
+
/** PRD 文件绝对路径。 */
|
|
38
|
+
prdPath: string;
|
|
39
|
+
/** 解析出的功能条目,按 PRD 顺序排列。 */
|
|
40
|
+
functions: PrdFunctionEntry[];
|
|
41
|
+
/** 解析时间 ISO 字符串,仅用于审计,不影响 testcase 生成。 */
|
|
42
|
+
generatedAt: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* 在目录中查找 PRD markdown 文件。
|
|
46
|
+
*
|
|
47
|
+
* 规则(满足任一即返回):
|
|
48
|
+
* 1. 文件名包含 `prd`(大小写不敏感)且以 `.md` 结尾。
|
|
49
|
+
* 2. 文件正文包含 `## 4. 功能需求` 标题。
|
|
50
|
+
*
|
|
51
|
+
* @param dir - 需求文档目录。
|
|
52
|
+
* @returns PRD 文件绝对路径;不存在时返回 undefined。
|
|
53
|
+
*/
|
|
54
|
+
export declare function findPrdMarkdown(dir: string): string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* 读取 PRD 文件并提取功能条目。
|
|
57
|
+
*
|
|
58
|
+
* @param prdPath - PRD markdown 文件绝对路径。
|
|
59
|
+
* @param now - 可选的解析时间戳,用于测试冻结。
|
|
60
|
+
* @returns PRD 提取产物。
|
|
61
|
+
*/
|
|
62
|
+
export declare function loadPrdExtraction(prdPath: string, now?: string): PrdExtraction;
|
|
63
|
+
/**
|
|
64
|
+
* 解析 PRD markdown 文本,提取功能列表 + 功能详细说明。
|
|
65
|
+
*
|
|
66
|
+
* @param markdown - PRD 完整 markdown 文本。
|
|
67
|
+
* @returns 按 PRD 顺序排列的功能条目数组。
|
|
68
|
+
*/
|
|
69
|
+
export declare function extractPrdFunctions(markdown: string): PrdFunctionEntry[];
|
|
70
|
+
//# sourceMappingURL=prd-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prd-extractor.d.ts","sourceRoot":"","sources":["../../../src/extractors/prd-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,oBAAoB;AACpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,mBAAmB;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,mBAAmB;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,gBAAgB;AAChB,MAAM,MAAM,aAAa,GAAG;IAC1B,kBAAkB;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsB/D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAA2B,GAAG,aAAa,CAIhG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAqBxE"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD markdown 解析器。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 在需求目录中发现 PRD markdown 文件。
|
|
6
|
+
* 2. 解析 PRD 章节 4.1(功能列表)和 4.2.x(功能详细说明)。
|
|
7
|
+
* 3. 输出结构化的 PrdFunctionEntry[],供 testcase 装配阶段确定性使用。
|
|
8
|
+
*
|
|
9
|
+
* 设计取舍:
|
|
10
|
+
* - 全程使用正则/行解析,不调用 LLM,保证幂等性。
|
|
11
|
+
* - 只识别当前观察到的 PRD 结构(`4.1` 表格 + `4.2.x` 子章节 + 粗体字段标题)。
|
|
12
|
+
* - 解析失败时返回空数组并在 inferenceTrace 中记录,不阻断 pipeline。
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
/**
|
|
17
|
+
* 在目录中查找 PRD markdown 文件。
|
|
18
|
+
*
|
|
19
|
+
* 规则(满足任一即返回):
|
|
20
|
+
* 1. 文件名包含 `prd`(大小写不敏感)且以 `.md` 结尾。
|
|
21
|
+
* 2. 文件正文包含 `## 4. 功能需求` 标题。
|
|
22
|
+
*
|
|
23
|
+
* @param dir - 需求文档目录。
|
|
24
|
+
* @returns PRD 文件绝对路径;不存在时返回 undefined。
|
|
25
|
+
*/
|
|
26
|
+
export function findPrdMarkdown(dir) {
|
|
27
|
+
if (!existsSync(dir)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const candidates = readdirSync(dir).filter((file) => file.endsWith('.md'));
|
|
31
|
+
for (const file of candidates) {
|
|
32
|
+
const fullPath = path.join(dir, file);
|
|
33
|
+
if (/prd/i.test(file)) {
|
|
34
|
+
return fullPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const file of candidates) {
|
|
38
|
+
const fullPath = path.join(dir, file);
|
|
39
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
40
|
+
if (/##\s*4\.\s*功能需求/.test(content)) {
|
|
41
|
+
return fullPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 读取 PRD 文件并提取功能条目。
|
|
48
|
+
*
|
|
49
|
+
* @param prdPath - PRD markdown 文件绝对路径。
|
|
50
|
+
* @param now - 可选的解析时间戳,用于测试冻结。
|
|
51
|
+
* @returns PRD 提取产物。
|
|
52
|
+
*/
|
|
53
|
+
export function loadPrdExtraction(prdPath, now = new Date().toISOString()) {
|
|
54
|
+
const markdown = readFileSync(prdPath, 'utf8');
|
|
55
|
+
const functions = extractPrdFunctions(markdown);
|
|
56
|
+
return { prdPath, functions, generatedAt: now };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 解析 PRD markdown 文本,提取功能列表 + 功能详细说明。
|
|
60
|
+
*
|
|
61
|
+
* @param markdown - PRD 完整 markdown 文本。
|
|
62
|
+
* @returns 按 PRD 顺序排列的功能条目数组。
|
|
63
|
+
*/
|
|
64
|
+
export function extractPrdFunctions(markdown) {
|
|
65
|
+
const functionList = parseFunctionList(markdown);
|
|
66
|
+
const details = parseFunctionDetails(markdown);
|
|
67
|
+
const detailMap = new Map(details.map((d) => [d.featureId, d]));
|
|
68
|
+
// 以功能列表为基准顺序,合并详细说明;若详细说明缺失则使用列表中的描述兜底。
|
|
69
|
+
return functionList.map((listItem) => {
|
|
70
|
+
const detail = detailMap.get(listItem.featureId);
|
|
71
|
+
return {
|
|
72
|
+
featureId: listItem.featureId,
|
|
73
|
+
featureName: listItem.featureName,
|
|
74
|
+
featureModule: listItem.featureModule,
|
|
75
|
+
description: detail?.description ?? listItem.description,
|
|
76
|
+
inputConditions: detail?.inputConditions,
|
|
77
|
+
processingLogic: detail?.processingLogic ?? [],
|
|
78
|
+
outputResult: detail?.outputResult ?? [],
|
|
79
|
+
businessRules: detail?.businessRules ?? [],
|
|
80
|
+
constraints: detail?.constraints ?? [],
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 解析 4.1 功能列表表格。
|
|
86
|
+
*
|
|
87
|
+
* 表格列(按当前 PRD 约定):
|
|
88
|
+
* | 功能模块 | 功能编号 | 功能名称 | 功能描述 | 功能类型 | ... |
|
|
89
|
+
*/
|
|
90
|
+
function parseFunctionList(markdown) {
|
|
91
|
+
const result = [];
|
|
92
|
+
const sectionMatch = markdown.match(/###\s*4\.1\s*功能列表[\s\S]*?(?=###\s*4\.2|##\s+5|\n##\s)/);
|
|
93
|
+
if (!sectionMatch) {
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
const lines = sectionMatch[0].split('\n');
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const trimmed = line.trim();
|
|
99
|
+
if (!trimmed.startsWith('|'))
|
|
100
|
+
continue;
|
|
101
|
+
if (trimmed.includes('功能编号'))
|
|
102
|
+
continue; // header / separator
|
|
103
|
+
if (/^\|\s*-+/.test(trimmed))
|
|
104
|
+
continue; // markdown separator
|
|
105
|
+
const cells = trimmed
|
|
106
|
+
.split('|')
|
|
107
|
+
.map((c) => c.trim())
|
|
108
|
+
.filter((c) => c.length > 0);
|
|
109
|
+
if (cells.length < 4)
|
|
110
|
+
continue;
|
|
111
|
+
const featureModule = cells[0];
|
|
112
|
+
const featureId = cells[1];
|
|
113
|
+
const featureName = cells[2];
|
|
114
|
+
const description = cells[3];
|
|
115
|
+
if (!/^F\d{3,}$/i.test(featureId))
|
|
116
|
+
continue;
|
|
117
|
+
result.push({ featureId: featureId.toUpperCase(), featureName, featureModule, description });
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 解析 4.2.x 功能详细说明。
|
|
123
|
+
*/
|
|
124
|
+
function parseFunctionDetails(markdown) {
|
|
125
|
+
const result = [];
|
|
126
|
+
// 匹配每个 4.2.x 子章节:#### 4.2.x Fxxx 功能名称
|
|
127
|
+
const sectionRegex = /####\s*4\.2\.(\d+)\s+((F\d{3,})\s+(.+?))\s*(?=\n)/g;
|
|
128
|
+
let match;
|
|
129
|
+
const sections = [];
|
|
130
|
+
while ((match = sectionRegex.exec(markdown)) !== null) {
|
|
131
|
+
sections.push({
|
|
132
|
+
start: match.index,
|
|
133
|
+
featureId: match[3].toUpperCase(),
|
|
134
|
+
featureName: match[4].trim(),
|
|
135
|
+
end: match.index + match[0].length,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
for (let i = 0; i < sections.length; i++) {
|
|
139
|
+
const current = sections[i];
|
|
140
|
+
const nextSection = sections[i + 1];
|
|
141
|
+
const sectionEnd = findSectionEnd(current.end, markdown, nextSection?.start);
|
|
142
|
+
const sectionBody = markdown.slice(current.end, sectionEnd);
|
|
143
|
+
result.push(parseFunctionDetailSection(current.featureId, current.featureName, sectionBody));
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 计算子章节结束位置。
|
|
149
|
+
*
|
|
150
|
+
* 结束位置取以下候选的最小值:
|
|
151
|
+
* 1. 下一个同层 `####` 子章节的开始位置。
|
|
152
|
+
* 2. 下一个更高层标题(`###` 或 `##`)的开始位置。
|
|
153
|
+
* 3. markdown 末尾。
|
|
154
|
+
*/
|
|
155
|
+
function findSectionEnd(start, markdown, nextSectionStart) {
|
|
156
|
+
const nextHeadingMatch = markdown.slice(start).match(/\n#{1,3}\s/m);
|
|
157
|
+
const nextHeadingStart = nextHeadingMatch
|
|
158
|
+
? start + nextHeadingMatch.index + 1 // +1 跳过前面的换行
|
|
159
|
+
: undefined;
|
|
160
|
+
const candidates = [nextSectionStart, nextHeadingStart].filter((n) => n !== undefined);
|
|
161
|
+
return candidates.length > 0 ? Math.min(...candidates) : markdown.length;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 解析单个 4.2.x 子章节内容。
|
|
165
|
+
*/
|
|
166
|
+
function parseFunctionDetailSection(featureId, featureName, sectionBody) {
|
|
167
|
+
const fields = splitDetailFields(sectionBody);
|
|
168
|
+
return {
|
|
169
|
+
featureId,
|
|
170
|
+
featureName,
|
|
171
|
+
description: fields['功能描述']?.join('\n').trim() ?? '',
|
|
172
|
+
inputConditions: fields['输入条件']?.join('\n').trim() || undefined,
|
|
173
|
+
processingLogic: extractListItems(fields['处理逻辑']),
|
|
174
|
+
outputResult: extractListItems(fields['输出结果']),
|
|
175
|
+
businessRules: extractListItems(fields['业务规则']),
|
|
176
|
+
constraints: extractListItems(fields['约束条件']),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 按粗体字段标题把子章节内容切分成多个字段块。
|
|
181
|
+
*
|
|
182
|
+
* 字段标题形如 `**功能描述**:` 或 `**功能描述**:`(注意中文冒号)。
|
|
183
|
+
*/
|
|
184
|
+
function splitDetailFields(body) {
|
|
185
|
+
const lines = body.split('\n');
|
|
186
|
+
const fields = {};
|
|
187
|
+
let currentField;
|
|
188
|
+
const fieldHeaderRegex = /^\*\*([^*]+)\*\*[::]\s*(.*)$/;
|
|
189
|
+
for (const rawLine of lines) {
|
|
190
|
+
const line = rawLine.trimEnd();
|
|
191
|
+
const headerMatch = line.trim().match(fieldHeaderRegex);
|
|
192
|
+
if (headerMatch) {
|
|
193
|
+
currentField = headerMatch[1].trim();
|
|
194
|
+
const rest = headerMatch[2].trim();
|
|
195
|
+
fields[currentField] = [];
|
|
196
|
+
if (rest) {
|
|
197
|
+
fields[currentField].push(rest);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (currentField && line.trim().length > 0) {
|
|
202
|
+
// 跳过空行,但保留列表项和缩进内容
|
|
203
|
+
fields[currentField].push(line);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return fields;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 从字段块中提取列表项。
|
|
210
|
+
*
|
|
211
|
+
* 支持:
|
|
212
|
+
* - 有序列表 `1. ...`
|
|
213
|
+
* - 无序列表 `- ...`
|
|
214
|
+
* - 缩进列表(二级列表作为前一项的延续,不单独拆分)
|
|
215
|
+
*
|
|
216
|
+
* 过滤:
|
|
217
|
+
* - 空项
|
|
218
|
+
* - 纯 `--` 等表格/排版分隔符
|
|
219
|
+
*/
|
|
220
|
+
function extractListItems(lines) {
|
|
221
|
+
if (!lines || lines.length === 0)
|
|
222
|
+
return [];
|
|
223
|
+
const items = [];
|
|
224
|
+
for (const rawLine of lines) {
|
|
225
|
+
const line = rawLine.trim();
|
|
226
|
+
if (line.length === 0)
|
|
227
|
+
continue;
|
|
228
|
+
if (/^-+$/.test(line))
|
|
229
|
+
continue; // 过滤 '--' 分隔符
|
|
230
|
+
const listMatch = line.match(/^(?:\d+\.|-)\s+(.+)$/);
|
|
231
|
+
if (listMatch) {
|
|
232
|
+
const content = listMatch[1].trim();
|
|
233
|
+
if (content)
|
|
234
|
+
items.push(content);
|
|
235
|
+
}
|
|
236
|
+
else if (line.startsWith('-')) {
|
|
237
|
+
const content = line.replace(/^-\s*/, '').trim();
|
|
238
|
+
if (content)
|
|
239
|
+
items.push(content);
|
|
240
|
+
}
|
|
241
|
+
else if (items.length > 0 && rawLine.startsWith(' ')) {
|
|
242
|
+
// 缩进内容追加到前一项
|
|
243
|
+
items[items.length - 1] += ' ' + line;
|
|
244
|
+
}
|
|
245
|
+
// 非列表行(如普通段落)忽略,避免把标题残留混入
|
|
246
|
+
}
|
|
247
|
+
return items;
|
|
248
|
+
}
|
|
@@ -104,20 +104,20 @@ function renderGapFindingBlock(workflow, label, assertion) {
|
|
|
104
104
|
const kind = /暂未实现|未实现|未配置|联系管理员/.test(observed)
|
|
105
105
|
? 'feature_incomplete'
|
|
106
106
|
: 'behavior_mismatch';
|
|
107
|
-
return `
|
|
108
|
-
gapFinding: {
|
|
109
|
-
kind: ${singleQuoted(kind)},
|
|
110
|
-
summary: ${singleQuoted(`运行态推理发现「${label}」未满足业务预期。`)},
|
|
111
|
-
observed: ${singleQuoted(observed)},
|
|
107
|
+
return `
|
|
108
|
+
gapFinding: {
|
|
109
|
+
kind: ${singleQuoted(kind)},
|
|
110
|
+
summary: ${singleQuoted(`运行态推理发现「${label}」未满足业务预期。`)},
|
|
111
|
+
observed: ${singleQuoted(observed)},
|
|
112
112
|
},`;
|
|
113
113
|
}
|
|
114
114
|
function renderRuntimeAwareAssertionBlock(input) {
|
|
115
115
|
const exploration = input.workflow.resolvedRuntime?.exploration;
|
|
116
116
|
if (!exploration) {
|
|
117
|
-
return `
|
|
118
|
-
await expect(${input.repoVariable}).toBeVisible();
|
|
119
|
-
await ${input.repoVariable}.click();
|
|
120
|
-
|
|
117
|
+
return `
|
|
118
|
+
await expect(${input.repoVariable}).toBeVisible();
|
|
119
|
+
await ${input.repoVariable}.click();
|
|
120
|
+
|
|
121
121
|
await expect(page.getByText(${singleQuoted(input.assertion)})).toBeVisible({ timeout: 30000 });`;
|
|
122
122
|
}
|
|
123
123
|
const confirmVariable = confirmButtonVariableName(input.label);
|
|
@@ -125,33 +125,33 @@ function renderRuntimeAwareAssertionBlock(input) {
|
|
|
125
125
|
const actualVariable = actualFeedbackVariableName(input.label);
|
|
126
126
|
const runtimeObserved = exploration.observedMessage?.replace(/\s+/g, ' ').trim() ?? '';
|
|
127
127
|
const confirmBlock = exploration.confirmDialogPresent
|
|
128
|
-
? `
|
|
129
|
-
|
|
130
|
-
const ${confirmVariable} = ${input.locatorOwner}
|
|
131
|
-
.locator("xpath=//*[contains(@class,'mini-messagebox')]//a[normalize-space()='确定' or normalize-space()='确认']")
|
|
132
|
-
.first();
|
|
133
|
-
await expect(${confirmVariable}).toBeVisible({ timeout: 15000 });
|
|
128
|
+
? `
|
|
129
|
+
|
|
130
|
+
const ${confirmVariable} = ${input.locatorOwner}
|
|
131
|
+
.locator("xpath=//*[contains(@class,'mini-messagebox')]//a[normalize-space()='确定' or normalize-space()='确认']")
|
|
132
|
+
.first();
|
|
133
|
+
await expect(${confirmVariable}).toBeVisible({ timeout: 15000 });
|
|
134
134
|
await ${confirmVariable}.click();`
|
|
135
135
|
: '';
|
|
136
|
-
return `
|
|
137
|
-
await expect(${input.repoVariable}).toBeVisible();
|
|
138
|
-
await ${input.repoVariable}.click();${confirmBlock}
|
|
139
|
-
|
|
140
|
-
const runtimeObserved反馈文案 = ${singleQuoted(runtimeObserved)};
|
|
141
|
-
const ${feedbackVariable} = ${input.locatorOwner}.locator("xpath=//*[contains(@class,'mini-messagebox') or contains(@class,'mini-tips')]");
|
|
142
|
-
await page.waitForTimeout(1000);
|
|
143
|
-
const 反馈文本列表 = (await ${feedbackVariable}.allInnerTexts())
|
|
144
|
-
.map((text) => text.replace(/\\s+/g, ' ').trim())
|
|
145
|
-
.filter(Boolean);
|
|
146
|
-
const ${actualVariable} =
|
|
147
|
-
反馈文本列表.find((text) => runtimeObserved反馈文案 && text.includes(runtimeObserved反馈文案)) ??
|
|
148
|
-
反馈文本列表.find((text) => text.includes(${singleQuoted(input.assertion)})) ??
|
|
149
|
-
runtimeObserved反馈文案 ??
|
|
150
|
-
反馈文本列表.sort((a, b) => b.length - a.length)[0] ??
|
|
151
|
-
'';
|
|
152
|
-
expect(
|
|
153
|
-
${actualVariable}.includes(${singleQuoted(input.assertion)}),
|
|
154
|
-
${singleQuoted(`${input.label}未达成业务预期:期望出现「${input.assertion}」,系统实际反馈「`)} + ${actualVariable} + '」',
|
|
136
|
+
return `
|
|
137
|
+
await expect(${input.repoVariable}).toBeVisible();
|
|
138
|
+
await ${input.repoVariable}.click();${confirmBlock}
|
|
139
|
+
|
|
140
|
+
const runtimeObserved反馈文案 = ${singleQuoted(runtimeObserved)};
|
|
141
|
+
const ${feedbackVariable} = ${input.locatorOwner}.locator("xpath=//*[contains(@class,'mini-messagebox') or contains(@class,'mini-tips')]");
|
|
142
|
+
await page.waitForTimeout(1000);
|
|
143
|
+
const 反馈文本列表 = (await ${feedbackVariable}.allInnerTexts())
|
|
144
|
+
.map((text) => text.replace(/\\s+/g, ' ').trim())
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
const ${actualVariable} =
|
|
147
|
+
反馈文本列表.find((text) => runtimeObserved反馈文案 && text.includes(runtimeObserved反馈文案)) ??
|
|
148
|
+
反馈文本列表.find((text) => text.includes(${singleQuoted(input.assertion)})) ??
|
|
149
|
+
runtimeObserved反馈文案 ??
|
|
150
|
+
反馈文本列表.sort((a, b) => b.length - a.length)[0] ??
|
|
151
|
+
'';
|
|
152
|
+
expect(
|
|
153
|
+
${actualVariable}.includes(${singleQuoted(input.assertion)}),
|
|
154
|
+
${singleQuoted(`${input.label}未达成业务预期:期望出现「${input.assertion}」,系统实际反馈「`)} + ${actualVariable} + '」',
|
|
155
155
|
).toBe(true);`;
|
|
156
156
|
}
|
|
157
157
|
export function renderAgentWorkflowTest(options) {
|
|
@@ -176,40 +176,40 @@ export function renderAgentWorkflowTest(options) {
|
|
|
176
176
|
workflow,
|
|
177
177
|
});
|
|
178
178
|
const iframeSetup = iframeSrcKeyword
|
|
179
|
-
? `
|
|
180
|
-
const ${frameVariable} = page.locator(${singleQuoted(`iframe[src*="${iframeSrcKeyword}"]`)}).contentFrame();
|
|
179
|
+
? `
|
|
180
|
+
const ${frameVariable} = page.locator(${singleQuoted(`iframe[src*="${iframeSrcKeyword}"]`)}).contentFrame();
|
|
181
181
|
`
|
|
182
182
|
: '';
|
|
183
|
-
return `
|
|
184
|
-
test(${singleQuoted(title)}, async ({ page }) => {
|
|
185
|
-
test.slow();
|
|
186
|
-
|
|
187
|
-
await runRecordedWorkflow(
|
|
188
|
-
recorder,
|
|
189
|
-
{
|
|
190
|
-
moduleId: ${singleQuoted(options.moduleId)},
|
|
191
|
-
workflowId: ${singleQuoted(workflow.workflowId)},
|
|
192
|
-
workflowKind: 'custom',
|
|
193
|
-
workflowName: ${singleQuoted(label)},
|
|
194
|
-
executionSource: 'agent_inferred',
|
|
195
|
-
businessIntent: ${singleQuoted(intent)},
|
|
196
|
-
elementActions: ${arrayLiteral(actions)},
|
|
197
|
-
assertionExpectation: ${singleQuoted(assertion)},
|
|
198
|
-
evidenceSources: ${arrayLiteral(evidenceSources(workflow))},
|
|
199
|
-
evidenceRefs: ${renderEvidenceRefs(workflow)},
|
|
200
|
-
reviewReason: ${singleQuoted(workflow.reviewReason ?? '该工作流由 gap-planner 识别为自定义业务动作。')},
|
|
201
|
-
playwrightSuiteTitle: ${singleQuoted(options.suiteTitle)},
|
|
202
|
-
playwrightTestTitle: ${singleQuoted(title)},
|
|
203
|
-
${gapFindingBlock}
|
|
204
|
-
},
|
|
205
|
-
async () => {
|
|
206
|
-
await openModule(page);
|
|
207
|
-
${iframeSetup}
|
|
208
|
-
const ${repoVariable} = ${locatorOwner}.locator(${locatorLiteral(locator)});
|
|
209
|
-
${assertionBlock}
|
|
210
|
-
},
|
|
211
|
-
);
|
|
212
|
-
});
|
|
183
|
+
return `
|
|
184
|
+
test(${singleQuoted(title)}, async ({ page }) => {
|
|
185
|
+
test.slow();
|
|
186
|
+
|
|
187
|
+
await runRecordedWorkflow(
|
|
188
|
+
recorder,
|
|
189
|
+
{
|
|
190
|
+
moduleId: ${singleQuoted(options.moduleId)},
|
|
191
|
+
workflowId: ${singleQuoted(workflow.workflowId)},
|
|
192
|
+
workflowKind: 'custom',
|
|
193
|
+
workflowName: ${singleQuoted(label)},
|
|
194
|
+
executionSource: 'agent_inferred',
|
|
195
|
+
businessIntent: ${singleQuoted(intent)},
|
|
196
|
+
elementActions: ${arrayLiteral(actions)},
|
|
197
|
+
assertionExpectation: ${singleQuoted(assertion)},
|
|
198
|
+
evidenceSources: ${arrayLiteral(evidenceSources(workflow))},
|
|
199
|
+
evidenceRefs: ${renderEvidenceRefs(workflow)},
|
|
200
|
+
reviewReason: ${singleQuoted(workflow.reviewReason ?? '该工作流由 gap-planner 识别为自定义业务动作。')},
|
|
201
|
+
playwrightSuiteTitle: ${singleQuoted(options.suiteTitle)},
|
|
202
|
+
playwrightTestTitle: ${singleQuoted(title)},
|
|
203
|
+
${gapFindingBlock}
|
|
204
|
+
},
|
|
205
|
+
async () => {
|
|
206
|
+
await openModule(page);
|
|
207
|
+
${iframeSetup}
|
|
208
|
+
const ${repoVariable} = ${locatorOwner}.locator(${locatorLiteral(locator)});
|
|
209
|
+
${assertionBlock}
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
213
|
`;
|
|
214
214
|
}
|
|
215
215
|
export function appendAgentWorkflowTest(options) {
|