@kkeve/suite 0.0.20 → 0.0.21
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.
|
@@ -55,6 +55,82 @@ const path = __importStar(require("node:path"));
|
|
|
55
55
|
const screenshotDiff_js_1 = require("../core/judge/screenshotDiff.js");
|
|
56
56
|
const confidence_js_1 = require("../core/judge/confidence.js");
|
|
57
57
|
const reportData_js_1 = require("../core/generateReport/reportData.js");
|
|
58
|
+
/** Cypress 最终状态 → 报告状态;pending 明确是跳过,其余非 passed 均按失败处理 */
|
|
59
|
+
function mapCypressState(rawState) {
|
|
60
|
+
const state = String(rawState || '').toLowerCase();
|
|
61
|
+
if (state === 'passed')
|
|
62
|
+
return 'passed';
|
|
63
|
+
if (state === 'pending' || state === 'skipped')
|
|
64
|
+
return 'skipped';
|
|
65
|
+
if (state === 'timedout' || state === 'timed_out')
|
|
66
|
+
return 'timedOut';
|
|
67
|
+
return 'failed';
|
|
68
|
+
}
|
|
69
|
+
/** 从 Cypress after:spec / after:run 的 TestResult 提取开始时间与耗时 */
|
|
70
|
+
function timingOfCypressTest(test) {
|
|
71
|
+
const attempts = Array.isArray(test?.attempts) ? test.attempts : [];
|
|
72
|
+
const last = attempts[attempts.length - 1] || {};
|
|
73
|
+
const duration = Number(last.wallClockDuration ?? test?.duration ?? 0);
|
|
74
|
+
const startedAt = last.wallClockStartedAt || test?.wallClockStartedAt;
|
|
75
|
+
if (startedAt) {
|
|
76
|
+
const parsed = new Date(startedAt);
|
|
77
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
78
|
+
return { duration, startTime: parsed.toISOString() };
|
|
79
|
+
}
|
|
80
|
+
return { duration, startTime: new Date(Date.now() - (Number.isFinite(duration) ? duration : 0)).toISOString() };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 用 Cypress 引擎掌握的最终测试结果补录 / 校正状态。
|
|
84
|
+
*
|
|
85
|
+
* 浏览器侧 `/report/test` 依赖全局 afterEach 与场景状态;beforeEach 等钩子
|
|
86
|
+
* 直接失败时状态可能拿不到,导致用例没有任何上报,最终被 reportData 补成
|
|
87
|
+
* `missing`。Cypress 的 after:spec / after:run 仍能提供权威结果,这里是唯一
|
|
88
|
+
* 不依赖用例内钩子的兜底入口。
|
|
89
|
+
*/
|
|
90
|
+
function mergeCypressRunTests(state, tests, specFile = '') {
|
|
91
|
+
let merged = 0;
|
|
92
|
+
for (const test of Array.isArray(tests) ? tests : []) {
|
|
93
|
+
const titlePath = Array.isArray(test?.title) ? test.title.map((part) => String(part)) : [];
|
|
94
|
+
const title = (titlePath[titlePath.length - 1] || String(test?.title || '')).trim();
|
|
95
|
+
if (!title)
|
|
96
|
+
continue;
|
|
97
|
+
const suiteTitle = titlePath.length >= 2 ? String(titlePath[titlePath.length - 2] || '').trim() : '';
|
|
98
|
+
const status = mapCypressState(test?.state);
|
|
99
|
+
const { duration, startTime } = timingOfCypressTest(test);
|
|
100
|
+
const error = status === 'passed' || status === 'skipped'
|
|
101
|
+
? undefined
|
|
102
|
+
: String(test?.displayError || test?.attempts?.[test.attempts.length - 1]?.error?.message || '');
|
|
103
|
+
const existing = state.results.find((r) => r.title === title && (!specFile || !r.file || r.file === specFile));
|
|
104
|
+
if (existing) {
|
|
105
|
+
// Cypress 的最终状态优先于浏览器侧可能拿到的中间状态;日志与附件保留。
|
|
106
|
+
existing.status = status;
|
|
107
|
+
existing.duration = duration || existing.duration;
|
|
108
|
+
existing.startTime = startTime || existing.startTime;
|
|
109
|
+
// Cypress 最终结论权威:失败才写错误,最终 passed/skipped 必须清掉
|
|
110
|
+
// 浏览器侧中间状态可能留下的误报错误,避免报告出现「通过但带错误」。
|
|
111
|
+
existing.error = error;
|
|
112
|
+
if (!existing.suiteTitle && suiteTitle)
|
|
113
|
+
existing.suiteTitle = suiteTitle;
|
|
114
|
+
if (!existing.file && specFile)
|
|
115
|
+
existing.file = specFile;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
state.results.push({
|
|
119
|
+
title,
|
|
120
|
+
suiteTitle,
|
|
121
|
+
file: specFile,
|
|
122
|
+
status,
|
|
123
|
+
duration,
|
|
124
|
+
startTime,
|
|
125
|
+
error,
|
|
126
|
+
logs: [],
|
|
127
|
+
attachments: [],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
merged++;
|
|
131
|
+
}
|
|
132
|
+
return merged;
|
|
133
|
+
}
|
|
58
134
|
function resultDirOf() {
|
|
59
135
|
// 优先使用 runner 注入的报告目录;缺失时回退旧布局,兼容历史执行环境。
|
|
60
136
|
if (process.env.KEVE_RESULT_DIR)
|
|
@@ -300,7 +376,10 @@ async function setupKeveBridge(on, config) {
|
|
|
300
376
|
}
|
|
301
377
|
catch { /* 软链失败不影响报告生成 */ }
|
|
302
378
|
}
|
|
303
|
-
|
|
379
|
+
// reportData.summary 是 summaryJson 本体,total 在其嵌套的 summary 字段下。
|
|
380
|
+
// 直接读 summary.total 恒为 undefined,日志会误报「用例 0」,掩盖真实用例数。
|
|
381
|
+
const cases = reportData?.summary?.summary?.total ?? reportData?.summary?.total;
|
|
382
|
+
return { ok: true, resultsPath, reportDataPath, cases: cases ?? undefined };
|
|
304
383
|
};
|
|
305
384
|
const handle = async (route, payload, signal) => {
|
|
306
385
|
switch (route) {
|
|
@@ -551,8 +630,15 @@ async function setupKeveBridge(on, config) {
|
|
|
551
630
|
console.log(`[keve-bridge] listening on 127.0.0.1:${port} → ${state.resultDir}`);
|
|
552
631
|
// 报告收口放在 after:run:此时所有 spec 的 /report/test 都已落盘,
|
|
553
632
|
// 且不依赖任何场景级的 afterAll(硬失败/中断也能出报告)。
|
|
554
|
-
on('after:run', async () => {
|
|
633
|
+
on('after:run', async (runResults) => {
|
|
555
634
|
try {
|
|
635
|
+
// 最后一次用 Cypress run 全量结果校正,防止个别 spec 的 after:spec 未触发。
|
|
636
|
+
let merged = 0;
|
|
637
|
+
for (const run of Array.isArray(runResults?.runs) ? runResults.runs : []) {
|
|
638
|
+
merged += mergeCypressRunTests(state, run?.tests || [], String(run?.spec?.relative || ''));
|
|
639
|
+
}
|
|
640
|
+
if (merged > 0)
|
|
641
|
+
console.log(`[keve-bridge] Cypress 最终结果校正 ${merged} 条`);
|
|
556
642
|
const out = await finalize();
|
|
557
643
|
console.log(`[keve-bridge] report-data.json → ${out.reportDataPath}(用例 ${out.cases ?? 0})`);
|
|
558
644
|
}
|
|
@@ -572,6 +658,9 @@ async function setupKeveBridge(on, config) {
|
|
|
572
658
|
* after:spec 早于 after:run 触发,此时 results 已由浏览器侧 /report/test 落盘。
|
|
573
659
|
*/
|
|
574
660
|
on('after:spec', async (_spec, results) => {
|
|
661
|
+
const merged = mergeCypressRunTests(state, results?.tests || [], String(results?.spec?.relative || ''));
|
|
662
|
+
if (merged > 0)
|
|
663
|
+
console.log(`[keve-bridge] Cypress 用例结果校正 ${merged} 条`);
|
|
575
664
|
const video = results?.video;
|
|
576
665
|
if (video && fs.existsSync(video)) {
|
|
577
666
|
// 相对 taskDir,与 Playwright 侧 videoPath 的语义一致
|
package/package.json
CHANGED
package/scripts/run-dsl-task.mjs
CHANGED
|
@@ -550,15 +550,17 @@ function rewriteOpenPageTargets(input, cases) {
|
|
|
550
550
|
|
|
551
551
|
/**
|
|
552
552
|
* 逐条统计 caseResults 状态。
|
|
553
|
-
*
|
|
554
|
-
*
|
|
553
|
+
*
|
|
554
|
+
* 只有显式 `skipped` 才算跳过;`missing` / `timedOut` / `unknown` / `error`
|
|
555
|
+
* 都代表用例没有得到有效执行结果或执行异常,必须计为失败。历史实现把它们
|
|
556
|
+
* 统一算成 skipped,导致 Cypress 已报 `1 failed` 时平台仍展示 0 失败。
|
|
555
557
|
*/
|
|
556
558
|
function summarizeReport(reportData) {
|
|
557
559
|
const cases = Array.isArray(reportData?.caseResults) ? reportData.caseResults : [];
|
|
558
560
|
if (cases.length) {
|
|
559
561
|
const passed = cases.filter((item) => item?.status === 'passed').length;
|
|
560
|
-
const
|
|
561
|
-
const
|
|
562
|
+
const skipped = cases.filter((item) => item?.status === 'skipped').length;
|
|
563
|
+
const failed = cases.length - passed - skipped;
|
|
562
564
|
return { passed, failed, skipped, total: cases.length };
|
|
563
565
|
}
|
|
564
566
|
const summary = reportData?.summary?.summary || reportData?.summary || {};
|