@epoint-testtech/ep-stage-skill 0.0.6 → 0.0.7
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/glue-run-test/SKILL.md +4 -3
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +3 -0
- package/dist/src/cli/run-report.d.ts +43 -0
- package/dist/src/cli/run-report.d.ts.map +1 -0
- package/dist/src/cli/run-report.js +292 -0
- package/dist/src/cli/run.d.ts +2 -0
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +35 -3
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: glue-run-test
|
|
3
3
|
description: 使用时机:当用户对某需求要「跑测试 / 验证 / 端到端执行 / 跑胶水测试 / 看报告」或显式触发 `/glue-run-test <testcase.json5路径>` 时使用。
|
|
4
|
-
version: 2026-08-12+
|
|
4
|
+
version: 2026-08-12+2f7c618
|
|
5
5
|
argument-hint: "<testcase.json5绝对/相对路径>"
|
|
6
6
|
compatibility: 需要由 stage-create 生成的 glue 模式 Playwright 项目 + 已审过的 `testcase.json5` + 同目录 `credentials.json5`。
|
|
7
7
|
metadata:
|
|
@@ -215,7 +215,7 @@ CLI 非 0 退出 → 复述 stderr,不进 Step 7。
|
|
|
215
215
|
| `<projectDir>/src/tests/<需求名>/.ep-stage/ai-runtime-trace.json5` | prepare-tasks 先写 tasks;run 回写 results / repairRounds |
|
|
216
216
|
| `<projectDir>/src/tests/<需求名>/.ep-stage/elementmap.json` | Agent O-D-A-V 落的页面控件定位清单(含 grid columns);run 不触碰 |
|
|
217
217
|
| `<projectDir>/src/tests/<需求名>/.ep-stage/repair-runs/<round>/repair-report.md` | 失败时才产;含失败分类与摘要,待人审确认 |
|
|
218
|
-
| `<projectDir>/e2e/glue-code-mvp/glue-report/<run-timestamp>/` | run 子命令产;执行账本(`run-output.log` 完整执行输出 + `run-info.json`) |
|
|
218
|
+
| `<projectDir>/e2e/glue-code-mvp/glue-report/<run-timestamp>/` | run 子命令产;执行账本(`index.html` glue 风格综合报告(全部用例)+ `playwright-results.json` + `run-output.log` 完整执行输出 + `run-info.json`) |
|
|
219
219
|
| `<projectDir>/playwright-report/index.html` | run 子命令随执行产;Playwright 综合报告(用例状态/错误/截图/trace 回放),`npx playwright show-report` 打开 |
|
|
220
220
|
| `<projectDir>/stage-report/` | run 子命令随执行产;stage-core 流程视角报告(index.html + report.pdf) |
|
|
221
221
|
|
|
@@ -241,7 +241,8 @@ Step 7 全部通过后,按以下中文格式呈现:
|
|
|
241
241
|
> Spec:`<projectDir>/src/tests/<需求名>/crud.spec.ts`(glue 骨架 + Agent snippet 合并)
|
|
242
242
|
> AI Runtime Trace:`<projectDir>/src/tests/<需求名>/.ep-stage/ai-runtime-trace.json5`
|
|
243
243
|
> 报告目录:`<projectDir>/e2e/glue-code-mvp/glue-report/<run-timestamp>/`(run-output.log + run-info.json)
|
|
244
|
-
> 综合报告:`<projectDir>/
|
|
244
|
+
> 综合报告:`<projectDir>/e2e/glue-code-mvp/glue-report/<run-timestamp>/index.html`(glue 风格,覆盖全部用例)
|
|
245
|
+
> 技术细节:`<projectDir>/playwright-report/index.html`(trace/截图/视频)+ `<projectDir>/stage-report/`(流程 PDF)
|
|
245
246
|
> 运行版本:`version = v1`
|
|
246
247
|
>
|
|
247
248
|
> 下一步:① 调整 `testcase.json5` / `contract.json5` 后重跑 `/glue-run-test <testcase.json5路径>`;② 失败时查看 repair-report.md 确认修补方向后再重跑。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AAOA;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AAOA;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAuD1E;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAchC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS3F"}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -42,6 +42,9 @@ export async function main(argv = process.argv) {
|
|
|
42
42
|
`report: ${result.reportDir}`,
|
|
43
43
|
`trace: ${result.aiRuntimeTracePath}`,
|
|
44
44
|
];
|
|
45
|
+
if (result.comprehensiveReportPath) {
|
|
46
|
+
lines.push(`综合报告: ${result.comprehensiveReportPath}`);
|
|
47
|
+
}
|
|
45
48
|
if (result.repairReportPath) {
|
|
46
49
|
lines.push(`repair: ${result.repairReportPath}`);
|
|
47
50
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-report.ts — run 子命令的综合报告渲染(glue 风格单文件 HTML)。
|
|
3
|
+
*
|
|
4
|
+
* 输入:Playwright JSON reporter 产物(PLAYWRIGHT_JSON_OUTPUT_NAME 指定落盘)。
|
|
5
|
+
* 输出:<reportDir>/index.html —— 与脚手架 stage-glue 报告同款样式
|
|
6
|
+
* (概览 + 统计卡片 + 状态色卡片 + 页脚原生报告入口),
|
|
7
|
+
* 但数据来自 Playwright 真实执行结果(覆盖全部用例,含 AI snippet)。
|
|
8
|
+
*/
|
|
9
|
+
export type RunReportTestItem = {
|
|
10
|
+
title: string;
|
|
11
|
+
suitePath: string;
|
|
12
|
+
projectName: string;
|
|
13
|
+
status: 'passed' | 'failed' | 'flaky' | 'skipped';
|
|
14
|
+
durationMs: number;
|
|
15
|
+
location?: string;
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
};
|
|
18
|
+
export type RunReportData = {
|
|
19
|
+
suiteTitle: string;
|
|
20
|
+
startedAt: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
specFile?: string;
|
|
23
|
+
items: RunReportTestItem[];
|
|
24
|
+
/** AI 任务标题集合(命中者标记为 Agent 推理执行)。 */
|
|
25
|
+
aiTaskTitles: string[];
|
|
26
|
+
/** 相对 reportDir 的链接。 */
|
|
27
|
+
playwrightReportHref?: string;
|
|
28
|
+
stageReportHref?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 把 Playwright JSON reporter 输出摊平为报告用例条目。
|
|
32
|
+
*/
|
|
33
|
+
export declare function parsePlaywrightJsonResults(jsonText: string): {
|
|
34
|
+
suiteTitle: string;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
startedAt: string;
|
|
37
|
+
items: RunReportTestItem[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 渲染 glue 风格综合报告(单文件、内联样式、无外部依赖)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderRunReportHtml(data: RunReportData): string;
|
|
43
|
+
//# sourceMappingURL=run-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-report.d.ts","sourceRoot":"","sources":["../../../src/cli/run-report.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,oCAAoC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,wBAAwB;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA4CF;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B,CAwCA;AA4DD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAkL/D"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-report.ts — run 子命令的综合报告渲染(glue 风格单文件 HTML)。
|
|
3
|
+
*
|
|
4
|
+
* 输入:Playwright JSON reporter 产物(PLAYWRIGHT_JSON_OUTPUT_NAME 指定落盘)。
|
|
5
|
+
* 输出:<reportDir>/index.html —— 与脚手架 stage-glue 报告同款样式
|
|
6
|
+
* (概览 + 统计卡片 + 状态色卡片 + 页脚原生报告入口),
|
|
7
|
+
* 但数据来自 Playwright 真实执行结果(覆盖全部用例,含 AI snippet)。
|
|
8
|
+
*/
|
|
9
|
+
function escapeHtml(value) {
|
|
10
|
+
return value
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"');
|
|
15
|
+
}
|
|
16
|
+
function mapTestStatus(status) {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'expected':
|
|
19
|
+
return 'passed';
|
|
20
|
+
case 'unexpected':
|
|
21
|
+
return 'failed';
|
|
22
|
+
case 'flaky':
|
|
23
|
+
return 'flaky';
|
|
24
|
+
default:
|
|
25
|
+
return 'skipped';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 把 Playwright JSON reporter 输出摊平为报告用例条目。
|
|
30
|
+
*/
|
|
31
|
+
export function parsePlaywrightJsonResults(jsonText) {
|
|
32
|
+
const parsed = JSON.parse(jsonText);
|
|
33
|
+
const items = [];
|
|
34
|
+
const walk = (suite, trail) => {
|
|
35
|
+
const nextTrail = suite.title ? [...trail, suite.title] : trail;
|
|
36
|
+
for (const spec of suite.specs ?? []) {
|
|
37
|
+
for (const test of spec.tests ?? []) {
|
|
38
|
+
const lastResult = test.results?.[test.results.length - 1];
|
|
39
|
+
const errorMessage = lastResult?.error?.message ?? lastResult?.errors?.[0]?.message;
|
|
40
|
+
items.push({
|
|
41
|
+
title: spec.title,
|
|
42
|
+
suitePath: nextTrail.join(' › '),
|
|
43
|
+
projectName: test.projectName ?? '',
|
|
44
|
+
status: mapTestStatus(test.status),
|
|
45
|
+
durationMs: lastResult?.duration ?? 0,
|
|
46
|
+
location: spec.file !== undefined && spec.line !== undefined
|
|
47
|
+
? `${spec.file}:${spec.line}`
|
|
48
|
+
: spec.file,
|
|
49
|
+
errorMessage: errorMessage?.split('\n').slice(0, 12).join('\n'),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const child of suite.suites ?? []) {
|
|
54
|
+
walk(child, nextTrail);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
for (const suite of parsed.suites ?? []) {
|
|
58
|
+
walk(suite, []);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
suiteTitle: parsed.suites?.[0]?.title ?? 'Playwright 测试套件',
|
|
62
|
+
durationMs: Math.round(parsed.stats?.duration ?? 0),
|
|
63
|
+
startedAt: parsed.stats?.startTime ?? '',
|
|
64
|
+
items,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const STATUS_LABEL = {
|
|
68
|
+
passed: '通过',
|
|
69
|
+
failed: '失败',
|
|
70
|
+
flaky: '不稳定',
|
|
71
|
+
skipped: '跳过',
|
|
72
|
+
};
|
|
73
|
+
const STATUS_BADGE_CLASS = {
|
|
74
|
+
passed: 'passed',
|
|
75
|
+
failed: 'failed',
|
|
76
|
+
flaky: 'needs_review',
|
|
77
|
+
skipped: 'unresolved',
|
|
78
|
+
};
|
|
79
|
+
function formatDuration(ms) {
|
|
80
|
+
if (ms >= 60_000) {
|
|
81
|
+
return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`;
|
|
82
|
+
}
|
|
83
|
+
if (ms >= 1000) {
|
|
84
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
85
|
+
}
|
|
86
|
+
return `${ms}ms`;
|
|
87
|
+
}
|
|
88
|
+
function formatDisplayTimestamp(iso) {
|
|
89
|
+
if (!iso)
|
|
90
|
+
return '-';
|
|
91
|
+
const d = new Date(iso);
|
|
92
|
+
if (Number.isNaN(d.getTime()))
|
|
93
|
+
return iso;
|
|
94
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
95
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
96
|
+
}
|
|
97
|
+
function renderTestCard(item, strategy) {
|
|
98
|
+
const errorHtml = item.errorMessage
|
|
99
|
+
? `<div class="error"><strong>错误信息:</strong><pre>${escapeHtml(item.errorMessage)}</pre></div>`
|
|
100
|
+
: '';
|
|
101
|
+
return `
|
|
102
|
+
<article class="workflow workflow--${escapeHtml(STATUS_BADGE_CLASS[item.status])}" data-strategy="${escapeHtml(strategy)}" data-status="${escapeHtml(item.status)}">
|
|
103
|
+
<header class="workflow__header">
|
|
104
|
+
<div class="workflow__title">
|
|
105
|
+
<h2>${escapeHtml(item.title)}</h2>
|
|
106
|
+
<code class="workflow__id">${escapeHtml(item.location ?? '')}</code>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="workflow__meta">
|
|
109
|
+
<span class="badge badge--kind">${escapeHtml(item.projectName)}</span>
|
|
110
|
+
<span class="badge badge--source">${escapeHtml(strategy === 'agent_inferred' ? 'Agent推理执行' : '胶水编码执行')}</span>
|
|
111
|
+
<span class="badge badge--status badge--${escapeHtml(STATUS_BADGE_CLASS[item.status])}">${escapeHtml(STATUS_LABEL[item.status])}</span>
|
|
112
|
+
</div>
|
|
113
|
+
</header>
|
|
114
|
+
<dl class="workflow__body">
|
|
115
|
+
<dt>套件路径</dt><dd>${escapeHtml(item.suitePath)}</dd>
|
|
116
|
+
<dt>耗时</dt><dd>${escapeHtml(formatDuration(item.durationMs))}</dd>
|
|
117
|
+
</dl>
|
|
118
|
+
${errorHtml}
|
|
119
|
+
</article>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 渲染 glue 风格综合报告(单文件、内联样式、无外部依赖)。
|
|
124
|
+
*/
|
|
125
|
+
export function renderRunReportHtml(data) {
|
|
126
|
+
const count = (s) => data.items.filter((i) => i.status === s).length;
|
|
127
|
+
const agentCount = data.items.filter((i) => data.aiTaskTitles.includes(i.title)).length;
|
|
128
|
+
const overviewRows = [
|
|
129
|
+
['测试套件', data.suiteTitle],
|
|
130
|
+
['执行时间', formatDisplayTimestamp(data.startedAt)],
|
|
131
|
+
['执行耗时', formatDuration(data.durationMs)],
|
|
132
|
+
['Spec 文件', data.specFile ?? '-'],
|
|
133
|
+
];
|
|
134
|
+
const overviewHtml = overviewRows
|
|
135
|
+
.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`)
|
|
136
|
+
.join('');
|
|
137
|
+
const strategyCards = [
|
|
138
|
+
['总测试流程', data.items.length, 'total', ''],
|
|
139
|
+
['胶水编码执行', data.items.length - agentCount, 'glue', 'glue_code'],
|
|
140
|
+
['Agent推理执行', agentCount, 'agent', 'agent_inferred'],
|
|
141
|
+
];
|
|
142
|
+
const resultCards = [
|
|
143
|
+
['通过', count('passed'), 'passed', 'passed'],
|
|
144
|
+
['失败', count('failed'), 'failed', 'failed'],
|
|
145
|
+
['不稳定', count('flaky'), 'review', 'flaky'],
|
|
146
|
+
['跳过', count('skipped'), 'unresolved', 'skipped'],
|
|
147
|
+
];
|
|
148
|
+
const renderCards = (cards, filterGroup) => cards
|
|
149
|
+
.map(([label, value, kind, filterValue]) => `
|
|
150
|
+
<div class="summary-card summary-card--${kind}" data-filter-group="${filterGroup}" data-filter-value="${filterValue}" role="button" tabindex="0">
|
|
151
|
+
<div class="summary-card__value">${value}</div>
|
|
152
|
+
<div class="summary-card__label">${escapeHtml(label)}</div>
|
|
153
|
+
</div>`)
|
|
154
|
+
.join('');
|
|
155
|
+
const workflowsHtml = data.items.length
|
|
156
|
+
? data.items
|
|
157
|
+
.map((item) => renderTestCard(item, data.aiTaskTitles.includes(item.title) ? 'agent_inferred' : 'glue_code'))
|
|
158
|
+
.join('\n')
|
|
159
|
+
: '<p class="muted">(本次运行未发现任何用例结果)</p>';
|
|
160
|
+
const footerLinks = [
|
|
161
|
+
data.playwrightReportHref
|
|
162
|
+
? `<a class="pw-link" href="${escapeHtml(data.playwrightReportHref)}" target="_blank" rel="noopener">Playwright 原生报告(trace / 截图 / 视频)</a>`
|
|
163
|
+
: '',
|
|
164
|
+
data.stageReportHref
|
|
165
|
+
? `<a class="pw-link pw-link--secondary" href="${escapeHtml(data.stageReportHref)}" target="_blank" rel="noopener">stage-core 流程报告(PDF)</a>`
|
|
166
|
+
: '',
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join(' ');
|
|
170
|
+
const style = `
|
|
171
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: #f7f8fa; color: #1f2933; margin: 0; padding: 24px; }
|
|
172
|
+
h1 { margin: 0 0 16px; font-size: 22px; }
|
|
173
|
+
h2 { font-size: 16px; margin: 16px 0 8px; }
|
|
174
|
+
section.overview { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); margin-bottom: 24px; }
|
|
175
|
+
.overview-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
176
|
+
.overview-table th, .overview-table td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #eee; font-size: 13px; }
|
|
177
|
+
.overview-table th { width: 140px; color: #52606d; font-weight: 600; }
|
|
178
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
|
179
|
+
.summary-card { background: #f0f4f8; border-radius: 6px; padding: 12px; text-align: center; cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; }
|
|
180
|
+
.summary-card:hover { box-shadow: 0 2px 6px rgba(0,0,0,0.1); transform: translateY(-1px); }
|
|
181
|
+
.summary-card--active { box-shadow: 0 0 0 2px #2563eb; }
|
|
182
|
+
.summary-card__value { font-size: 22px; font-weight: 600; }
|
|
183
|
+
.summary-card__label { font-size: 12px; color: #52606d; margin-top: 4px; }
|
|
184
|
+
.summary-card--passed { background: #e3fcec; }
|
|
185
|
+
.summary-card--failed { background: #ffe3e3; }
|
|
186
|
+
.summary-card--review { background: #fff4d6; }
|
|
187
|
+
.summary-card--unresolved { background: #f0e8ff; }
|
|
188
|
+
article.workflow { background: #fff; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border-left: 4px solid #cbd2d9; }
|
|
189
|
+
article.workflow--passed { border-left-color: #2f9e44; }
|
|
190
|
+
article.workflow--failed { border-left-color: #e03131; }
|
|
191
|
+
article.workflow--needs_review { border-left-color: #f59f00; }
|
|
192
|
+
article.workflow--unresolved { border-left-color: #7048e8; }
|
|
193
|
+
.workflow__header { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; }
|
|
194
|
+
.workflow__title h2 { margin: 0; font-size: 16px; }
|
|
195
|
+
.workflow__id { font-size: 12px; color: #52606d; }
|
|
196
|
+
.workflow__meta { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
197
|
+
.badge { display: inline-block; padding: 2px 8px; font-size: 12px; border-radius: 12px; background: #e4e7eb; color: #1f2933; }
|
|
198
|
+
.badge--passed { background: #2f9e44; color: #fff; }
|
|
199
|
+
.badge--failed { background: #e03131; color: #fff; }
|
|
200
|
+
.badge--needs_review { background: #f59f00; color: #fff; }
|
|
201
|
+
.badge--unresolved { background: #7048e8; color: #fff; }
|
|
202
|
+
dl.workflow__body { display: grid; grid-template-columns: 100px 1fr; gap: 6px 12px; margin: 12px 0 4px; font-size: 13px; }
|
|
203
|
+
dl.workflow__body dt { color: #52606d; font-weight: 600; }
|
|
204
|
+
code { background: #f0f4f8; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
205
|
+
.error pre { background: #fff5f5; color: #c92a2a; padding: 8px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; }
|
|
206
|
+
.muted { color: #829ab1; font-size: 13px; }
|
|
207
|
+
footer.footer { background: #fff; border-radius: 8px; padding: 16px 20px; margin-top: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
|
208
|
+
.pw-link { display: inline-block; padding: 8px 16px; background: #2563eb; color: #fff; border-radius: 4px; text-decoration: none; font-size: 14px; margin-right: 8px; }
|
|
209
|
+
.pw-link:hover { background: #1d4ed8; }
|
|
210
|
+
.pw-link--secondary { background: #334e68; }
|
|
211
|
+
.workflow-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 16px 0 8px; }
|
|
212
|
+
.workflow-toolbar h2 { margin: 0; }
|
|
213
|
+
.workflow-filters { display: flex; gap: 8px; }
|
|
214
|
+
.workflow-filters select { padding: 4px 8px; font-size: 13px; border: 1px solid #cbd2d9; border-radius: 4px; background: #fff; color: #1f2933; }
|
|
215
|
+
.summary-section-title { font-size: 13px; color: #52606d; margin: 12px 0 4px; font-weight: 600; }
|
|
216
|
+
`;
|
|
217
|
+
return `<!DOCTYPE html>
|
|
218
|
+
<html lang="zh-CN">
|
|
219
|
+
<head>
|
|
220
|
+
<meta charset="UTF-8" />
|
|
221
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
222
|
+
<title>胶水编码-测试执行</title>
|
|
223
|
+
<style>${style}</style>
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<section class="overview">
|
|
227
|
+
<h1>胶水编码-测试执行(综合报告)</h1>
|
|
228
|
+
<table class="overview-table">${overviewHtml}</table>
|
|
229
|
+
<h3 class="summary-section-title">执行策略</h3>
|
|
230
|
+
<div class="summary-grid summary-grid--strategy">${renderCards(strategyCards, 'strategy')}</div>
|
|
231
|
+
<h3 class="summary-section-title">执行结果</h3>
|
|
232
|
+
<div class="summary-grid summary-grid--result">${renderCards(resultCards, 'status')}</div>
|
|
233
|
+
</section>
|
|
234
|
+
<section class="workflows">
|
|
235
|
+
<div class="workflow-toolbar">
|
|
236
|
+
<h2>用例执行明细</h2>
|
|
237
|
+
<div class="workflow-filters">
|
|
238
|
+
<select id="filter-strategy">
|
|
239
|
+
<option value="">全部执行策略</option>
|
|
240
|
+
<option value="glue_code">胶水编码执行</option>
|
|
241
|
+
<option value="agent_inferred">Agent推理执行</option>
|
|
242
|
+
</select>
|
|
243
|
+
<select id="filter-status">
|
|
244
|
+
<option value="">全部执行结果</option>
|
|
245
|
+
<option value="passed">通过</option>
|
|
246
|
+
<option value="failed">失败</option>
|
|
247
|
+
<option value="flaky">不稳定</option>
|
|
248
|
+
<option value="skipped">跳过</option>
|
|
249
|
+
</select>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
${workflowsHtml}
|
|
253
|
+
</section>
|
|
254
|
+
<footer class="footer">
|
|
255
|
+
${footerLinks || '<p class="muted">本次未发现关联报告产物</p>'}
|
|
256
|
+
</footer>
|
|
257
|
+
<script>
|
|
258
|
+
(function() {
|
|
259
|
+
var fs = document.getElementById('filter-strategy');
|
|
260
|
+
var fst = document.getElementById('filter-status');
|
|
261
|
+
var cards = document.querySelectorAll('article.workflow');
|
|
262
|
+
var summaryCards = document.querySelectorAll('.summary-card');
|
|
263
|
+
function apply() {
|
|
264
|
+
var s = fs.value, st = fst.value;
|
|
265
|
+
for (var i = 0; i < cards.length; i++) {
|
|
266
|
+
var c = cards[i];
|
|
267
|
+
var show = (!s || c.dataset.strategy === s) && (!st || c.dataset.status === st);
|
|
268
|
+
c.style.display = show ? '' : 'none';
|
|
269
|
+
}
|
|
270
|
+
for (var j = 0; j < summaryCards.length; j++) {
|
|
271
|
+
var card = summaryCards[j];
|
|
272
|
+
var select = card.dataset.filterGroup === 'strategy' ? fs : fst;
|
|
273
|
+
card.classList.toggle('summary-card--active', select.value !== '' && select.value === card.dataset.filterValue);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
fs.addEventListener('change', apply);
|
|
277
|
+
fst.addEventListener('change', apply);
|
|
278
|
+
for (var j = 0; j < summaryCards.length; j++) {
|
|
279
|
+
(function(card) {
|
|
280
|
+
card.addEventListener('click', function() {
|
|
281
|
+
var select = card.dataset.filterGroup === 'strategy' ? fs : fst;
|
|
282
|
+
select.value = select.value === card.dataset.filterValue ? '' : card.dataset.filterValue;
|
|
283
|
+
apply();
|
|
284
|
+
});
|
|
285
|
+
})(summaryCards[j]);
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
</script>
|
|
289
|
+
</body>
|
|
290
|
+
</html>
|
|
291
|
+
`;
|
|
292
|
+
}
|
package/dist/src/cli/run.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type RunArgs = {
|
|
|
21
21
|
* @property version - 'v1':已接入 Agent snippets 合并、spec 执行和 repair 回路。
|
|
22
22
|
* @property aiRuntimeTracePath - 落盘的 .ep-stage/ai-runtime-trace.json5(含 tasks 与 results)。
|
|
23
23
|
* @property repairReportPath - 执行失败时落盘的 repair-report.md;成功时为 undefined。
|
|
24
|
+
* @property comprehensiveReportPath - glue 风格综合报告 <reportDir>/index.html;渲染失败时为 undefined。
|
|
24
25
|
*/
|
|
25
26
|
export type RunResult = {
|
|
26
27
|
specPath: string;
|
|
@@ -29,6 +30,7 @@ export type RunResult = {
|
|
|
29
30
|
version: 'v1';
|
|
30
31
|
aiRuntimeTracePath?: string;
|
|
31
32
|
repairReportPath?: string;
|
|
33
|
+
comprehensiveReportPath?: string;
|
|
32
34
|
};
|
|
33
35
|
/**
|
|
34
36
|
* runRun 的可注入依赖(测试即契约:允许单测替换推理与执行,避免触发真机)。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAG/E,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/run.ts"],"names":[],"mappings":"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"}
|
package/dist/src/cli/run.js
CHANGED
|
@@ -5,6 +5,7 @@ import JSON5 from 'json5';
|
|
|
5
5
|
import { validateTestcase } from '../validation/testcase.js';
|
|
6
6
|
import { classifyRunFailure, createInitialAiRuntimeTrace, nextRepairRound, writeAiRuntimeTrace, writeRepairReport, } from '../runtime/ai-runtime-trace.js';
|
|
7
7
|
import { runSpec } from './spec.js';
|
|
8
|
+
import { parsePlaywrightJsonResults, renderRunReportHtml } from './run-report.js';
|
|
8
9
|
/**
|
|
9
10
|
* AI 追加段分隔标记。重跑时 detect 已有标记则覆盖(保证幂等)。
|
|
10
11
|
*
|
|
@@ -121,10 +122,11 @@ export function buildPlaywrightCommand(input) {
|
|
|
121
122
|
const escapedBasename = basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
122
123
|
const specArg = `.*${escapedBasename}`;
|
|
123
124
|
// 枚举多个 reporter:list 保证控制台有可被 classifyRunFailure 消费的原生输出;
|
|
125
|
+
// json 落机器可读结果(经 PLAYWRIGHT_JSON_OUTPUT_NAME 指定文件)供 glue 风格综合报告渲染;
|
|
124
126
|
// html 与 stage-core reporter 让综合报告(playwright-report / stage-report)随 run 一并生成,
|
|
125
127
|
// 避免执行完还要手工补跑才能拿到汇总报告。CLI --reporter 会整体覆盖项目 config,
|
|
126
128
|
// 所以必须在同一条参数里显式枚举,不能只加 list。
|
|
127
|
-
const args = ['playwright', 'test', specArg, '--reporter=list,html,@epoint-testtech/stage-core/reporter'];
|
|
129
|
+
const args = ['playwright', 'test', specArg, '--reporter=list,json,html,@epoint-testtech/stage-core/reporter'];
|
|
128
130
|
if (!input.headless) {
|
|
129
131
|
args.push('--headed');
|
|
130
132
|
}
|
|
@@ -148,6 +150,9 @@ async function defaultExecuteFullSpec(input) {
|
|
|
148
150
|
cwd: cmd.cwd,
|
|
149
151
|
encoding: 'utf8',
|
|
150
152
|
shell: true,
|
|
153
|
+
env: input.resultsJsonPath
|
|
154
|
+
? { ...process.env, PLAYWRIGHT_JSON_OUTPUT_NAME: input.resultsJsonPath }
|
|
155
|
+
: process.env,
|
|
151
156
|
});
|
|
152
157
|
const stdout = spawned.stdout ?? '';
|
|
153
158
|
const stderr = spawned.stderr ?? '';
|
|
@@ -504,12 +509,17 @@ export async function runRun(args, deps) {
|
|
|
504
509
|
}
|
|
505
510
|
appendAiSnippets(paths.specPath, aiRuntimeTasks, resolvedResults);
|
|
506
511
|
writeAiRuntimeTrace(tracePath, trace);
|
|
512
|
+
// 报告目录先于执行创建:json reporter 结果文件落这里,供 glue 风格综合报告渲染。
|
|
513
|
+
const reportDir = path.join(paths.projectDir, 'e2e/glue-code-mvp/glue-report', new Date().toISOString().replace(/[:.]/g, '-'));
|
|
514
|
+
mkdirSync(reportDir, { recursive: true });
|
|
515
|
+
const resultsJsonPath = path.join(reportDir, 'playwright-results.json');
|
|
507
516
|
// 执行完整 spec。
|
|
508
517
|
const executor = deps?.executeFullSpec ?? defaultExecuteFullSpec;
|
|
509
518
|
const execution = await executor({
|
|
510
519
|
projectDir: paths.projectDir,
|
|
511
520
|
specPath: paths.specPath,
|
|
512
521
|
headless: args.headless,
|
|
522
|
+
resultsJsonPath,
|
|
513
523
|
});
|
|
514
524
|
// 失败时分类并写 repair report,停止等待人审;成功时 repairReportPath 留空。
|
|
515
525
|
let repairReportPath;
|
|
@@ -535,8 +545,6 @@ export async function runRun(args, deps) {
|
|
|
535
545
|
}
|
|
536
546
|
// 报告目录:落执行账本(完整执行输出 + 机器可读 run 信息),
|
|
537
547
|
// 修补报告只截失败输出片段,完整证据以这里为准。
|
|
538
|
-
const reportDir = path.join(paths.projectDir, 'e2e/glue-code-mvp/glue-report', new Date().toISOString().replace(/[:.]/g, '-'));
|
|
539
|
-
mkdirSync(reportDir, { recursive: true });
|
|
540
548
|
writeFileSync(path.join(reportDir, 'run-output.log'), execution.output);
|
|
541
549
|
writeFileSync(path.join(reportDir, 'run-info.json'), `${JSON.stringify({
|
|
542
550
|
specPath: paths.specPath,
|
|
@@ -545,6 +553,29 @@ export async function runRun(args, deps) {
|
|
|
545
553
|
repairReportPath,
|
|
546
554
|
executedAt: new Date().toISOString(),
|
|
547
555
|
}, null, 2)}\n`);
|
|
556
|
+
// glue 风格综合报告(index.html):数据来自 Playwright JSON 结果,
|
|
557
|
+
// 覆盖全部用例(含 AI snippet);渲染失败只降级缺省,不阻断主流程。
|
|
558
|
+
let comprehensiveReportPath;
|
|
559
|
+
try {
|
|
560
|
+
if (existsSync(resultsJsonPath)) {
|
|
561
|
+
const parsedResults = parsePlaywrightJsonResults(readFileSync(resultsJsonPath, 'utf8'));
|
|
562
|
+
const html = renderRunReportHtml({
|
|
563
|
+
suiteTitle: parsedResults.suiteTitle,
|
|
564
|
+
startedAt: parsedResults.startedAt,
|
|
565
|
+
durationMs: parsedResults.durationMs,
|
|
566
|
+
specFile: paths.specPath,
|
|
567
|
+
items: parsedResults.items,
|
|
568
|
+
aiTaskTitles: aiRuntimeTasks.map((task) => task.title),
|
|
569
|
+
playwrightReportHref: '../../../../playwright-report/index.html',
|
|
570
|
+
stageReportHref: '../../../../stage-report/report.pdf',
|
|
571
|
+
});
|
|
572
|
+
comprehensiveReportPath = path.join(reportDir, 'index.html');
|
|
573
|
+
writeFileSync(comprehensiveReportPath, html);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
comprehensiveReportPath = undefined;
|
|
578
|
+
}
|
|
548
579
|
// credentials.json5 消费边界声明。
|
|
549
580
|
const credentialsNotice = loadCredentialsNotice(testcase, paths.testcasePath);
|
|
550
581
|
return {
|
|
@@ -554,5 +585,6 @@ export async function runRun(args, deps) {
|
|
|
554
585
|
version: 'v1',
|
|
555
586
|
aiRuntimeTracePath: tracePath,
|
|
556
587
|
repairReportPath,
|
|
588
|
+
comprehensiveReportPath,
|
|
557
589
|
};
|
|
558
590
|
}
|