@kkeve/suite 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.
Files changed (115) hide show
  1. package/dist/bin/keve.js +2 -1
  2. package/dist/commands/clean.js +2 -2
  3. package/dist/commands/config.js +7 -7
  4. package/dist/commands/init.js +2 -2
  5. package/dist/commands/report.js +7 -7
  6. package/dist/commands/run.js +12 -11
  7. package/dist/commands/test.js +7 -7
  8. package/dist/config.js +7 -7
  9. package/dist/core/action-log-writer.js +4 -1
  10. package/dist/core/decorator/keve-decorators.d.ts +128 -0
  11. package/dist/core/decorator/keve-decorators.js +254 -0
  12. package/dist/core/decorator/keve-host.d.ts +40 -0
  13. package/dist/core/decorator/keve-host.js +33 -0
  14. package/dist/core/{keve-registry.d.ts → decorator/keve-registry.d.ts} +19 -0
  15. package/dist/core/{keve-registry.js → decorator/keve-registry.js} +13 -1
  16. package/dist/core/generateReport/report.d.ts +1 -1
  17. package/dist/core/generateReport/report.js +2 -2
  18. package/dist/core/generateReport/reportData.d.ts +3 -0
  19. package/dist/core/generateReport/reportData.js +49 -15
  20. package/dist/core/judge/confidence.d.ts +119 -0
  21. package/dist/core/judge/confidence.js +295 -0
  22. package/dist/core/judge/goal-heuristics.d.ts +32 -0
  23. package/dist/core/judge/goal-heuristics.js +128 -0
  24. package/dist/core/judge/pngCodec.d.ts +23 -0
  25. package/dist/core/judge/pngCodec.js +215 -0
  26. package/dist/core/judge/screenshotDiff.d.ts +112 -0
  27. package/dist/core/judge/screenshotDiff.js +420 -0
  28. package/dist/core/page-object/definePage.d.ts +95 -0
  29. package/dist/core/page-object/definePage.js +62 -0
  30. package/dist/core/page-object/elementResolver.d.ts +32 -0
  31. package/dist/core/page-object/elementResolver.js +236 -0
  32. package/dist/core/page-object/engine-types.d.ts +140 -0
  33. package/dist/core/page-object/engine-types.js +34 -0
  34. package/dist/core/sso-cookie.d.ts +18 -0
  35. package/dist/core/sso-cookie.js +10 -0
  36. package/dist/dsl/index.d.ts +15 -0
  37. package/dist/dsl/index.js +29 -0
  38. package/dist/dsl/locatorSource.d.ts +25 -0
  39. package/dist/dsl/locatorSource.js +323 -0
  40. package/dist/dsl/ops.d.ts +46 -0
  41. package/dist/dsl/ops.js +95 -0
  42. package/dist/dsl/toDecorator.d.ts +98 -0
  43. package/dist/dsl/toDecorator.js +747 -0
  44. package/dist/engine-cypress/adapter.d.ts +108 -0
  45. package/dist/engine-cypress/adapter.js +514 -0
  46. package/dist/engine-cypress/agent-page.d.ts +159 -0
  47. package/dist/engine-cypress/agent-page.js +566 -0
  48. package/dist/engine-cypress/agentDom.d.ts +22 -0
  49. package/dist/engine-cypress/agentDom.js +590 -0
  50. package/dist/engine-cypress/bridge.d.ts +39 -0
  51. package/dist/engine-cypress/bridge.js +598 -0
  52. package/dist/engine-cypress/cdp.d.ts +78 -0
  53. package/dist/engine-cypress/cdp.js +339 -0
  54. package/dist/engine-cypress/host.d.ts +22 -0
  55. package/dist/engine-cypress/host.js +480 -0
  56. package/dist/engine-cypress/index.d.ts +26 -0
  57. package/dist/engine-cypress/index.js +58 -0
  58. package/dist/engine-cypress/keveGoal.d.ts +43 -0
  59. package/dist/engine-cypress/keveGoal.js +265 -0
  60. package/dist/engine-cypress/pageOps.d.ts +41 -0
  61. package/dist/engine-cypress/pageOps.js +439 -0
  62. package/dist/engine-cypress/reporter.d.ts +20 -0
  63. package/dist/engine-cypress/reporter.js +128 -0
  64. package/dist/engine-cypress/setup.d.ts +21 -0
  65. package/dist/engine-cypress/setup.js +27 -0
  66. package/dist/engine-cypress/shotBuffer.d.ts +40 -0
  67. package/dist/engine-cypress/shotBuffer.js +41 -0
  68. package/dist/engine-cypress/state.d.ts +56 -0
  69. package/dist/engine-cypress/state.js +81 -0
  70. package/dist/engine-playwright/engineAdapter.d.ts +78 -0
  71. package/dist/engine-playwright/engineAdapter.js +371 -0
  72. package/dist/engine-playwright/global-setup.d.ts +28 -0
  73. package/dist/engine-playwright/global-setup.js +142 -0
  74. package/dist/{core → engine-playwright}/keve-report.d.ts +0 -1
  75. package/dist/engine-playwright/keve-report.js +137 -0
  76. package/dist/{core → engine-playwright}/keve-test.d.ts +10 -8
  77. package/dist/{core → engine-playwright}/keve-test.js +204 -200
  78. package/dist/{core → engine-playwright}/pw-config.js +8 -7
  79. package/dist/index.d.ts +13 -8
  80. package/dist/index.js +43 -28
  81. package/dist/page-agent/agent.d.ts +82 -7
  82. package/dist/page-agent/agent.js +252 -88
  83. package/dist/page-agent/diagnostics.d.ts +184 -0
  84. package/dist/page-agent/diagnostics.js +377 -0
  85. package/dist/page-agent/hooks.d.ts +3 -2
  86. package/dist/page-agent/hooks.js +4 -3
  87. package/dist/page-agent/page-like.d.ts +116 -0
  88. package/dist/page-agent/page-like.js +14 -0
  89. package/dist/page-agent/playwright-page.d.ts +20 -0
  90. package/dist/page-agent/playwright-page.js +98 -0
  91. package/dist/page-agent/system-prompt.d.ts +7 -0
  92. package/dist/page-agent/system-prompt.js +10 -0
  93. package/dist/page-agent/tools.d.ts +1 -1
  94. package/dist/page-agent/tools.js +144 -8
  95. package/package.json +24 -11
  96. package/scripts/gen-system-prompt.mjs +36 -0
  97. package/scripts/run-dsl-task.mjs +619 -0
  98. package/scripts/runtime/cypress-preprocessor.mjs +44 -0
  99. package/scripts/runtime/cypress.config.mjs +35 -0
  100. package/scripts/runtime/playwright.config.mjs +41 -0
  101. package/scripts/verify-shot-diff.mjs +243 -0
  102. package/dist/commands/auth-update.d.ts +0 -16
  103. package/dist/commands/auth-update.js +0 -88
  104. package/dist/core/auth.d.ts +0 -17
  105. package/dist/core/auth.js +0 -224
  106. package/dist/core/cdp-resolver.d.ts +0 -5
  107. package/dist/core/cdp-resolver.js +0 -93
  108. package/dist/core/global-setup.d.ts +0 -6
  109. package/dist/core/global-setup.js +0 -123
  110. package/dist/core/keve-decorators.d.ts +0 -51
  111. package/dist/core/keve-decorators.js +0 -95
  112. package/dist/core/keve-report.js +0 -376
  113. /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
  114. /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
  115. /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+ /**
3
+ * pageOps — 注入 AUT 的「操作原语 + 网络记录」源码
4
+ *
5
+ * 与 locatorSource.ts 同样以「自包含源码字符串」形式注入:isolated world 里的
6
+ * 函数无法闭包引用 bundle 变量,只能整段送过去求值。
7
+ *
8
+ * 职责边界:
9
+ * locatorSource → 定位(ElementRef / hop 链 → 元素集合)
10
+ * pageOps → 动作与读取(点击/输入/拖拽/文本/可见性/属性/滚动)
11
+ * netLog → fetch/XHR 记录,支撑 waitForResponse 的软等待门
12
+ *
13
+ * 交互走原生 DOM 事件(探针已验证 React 合成事件能正常响应),
14
+ * 与 Playwright 的 locator click 在「触发应用逻辑」这一层语义等价。
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.pageOpsSource = pageOpsSource;
18
+ exports.netLogSource = netLogSource;
19
+ exports.bootstrapSource = bootstrapSource;
20
+ exports.diagnosticsSource = diagnosticsSource;
21
+ function pageOpsSource() {
22
+ return `(() => {
23
+ "use strict";
24
+ if (window.__keveOpsSource === "v1") return "cached";
25
+
26
+ function locator() {
27
+ if (!window.__keveLocator) throw new Error("[keve] locator 未注入");
28
+ return window.__keveLocator;
29
+ }
30
+
31
+ /** refs → 末端元素集合(定位语义与 PlaywrightEngine.resolveChain 对齐) */
32
+ function resolve(refs) {
33
+ if (!refs || !refs.length) throw new Error("[keve] 缺少元素定位参数");
34
+ var res = locator().resolveRefChain(refs);
35
+ return (res && res.els) || [];
36
+ }
37
+
38
+ function first(refs) {
39
+ var els = resolve(refs);
40
+ if (!els.length) throw new Error("[keve] 元素未找到: " + JSON.stringify(refs).slice(0, 300));
41
+ return els[0];
42
+ }
43
+
44
+ function isVisible(el) {
45
+ if (!el || !el.isConnected) return false;
46
+ var rect = el.getBoundingClientRect();
47
+ if (rect.width === 0 && rect.height === 0) return false;
48
+ var style = window.getComputedStyle(el);
49
+ if (!style) return true;
50
+ if (style.visibility === "hidden" || style.display === "none") return false;
51
+ if (Number(style.opacity) === 0) return false;
52
+ return true;
53
+ }
54
+
55
+ /** 原生事件序列:点击/双击/悬停都走真实 MouseEvent,保证 React 合成事件生效 */
56
+ function mouse(el, type, detail) {
57
+ var rect = el.getBoundingClientRect();
58
+ var init = {
59
+ bubbles: true, cancelable: true, composed: true, view: window,
60
+ detail: detail || 1, button: 0,
61
+ clientX: Math.round(rect.left + rect.width / 2),
62
+ clientY: Math.round(rect.top + rect.height / 2),
63
+ };
64
+ el.dispatchEvent(new MouseEvent(type, init));
65
+ }
66
+
67
+ function clickEl(el, dbl) {
68
+ el.scrollIntoView({ block: "center", inline: "center" });
69
+ el.focus && el.focus();
70
+ mouse(el, "mousedown");
71
+ mouse(el, "mouseup");
72
+ if (typeof el.click === "function") el.click();
73
+ else mouse(el, "click");
74
+ mouse(el, "click");
75
+ if (dbl) {
76
+ mouse(el, "mousedown", 2);
77
+ mouse(el, "mouseup", 2);
78
+ mouse(el, "dblclick", 2);
79
+ }
80
+ }
81
+
82
+ function setValue(el, text) {
83
+ var value = text == null ? "" : String(text);
84
+ el.scrollIntoView({ block: "center", inline: "center" });
85
+ el.focus && el.focus();
86
+ if (el.isContentEditable) {
87
+ el.textContent = value;
88
+ el.dispatchEvent(new InputEvent("input", { bubbles: true, data: value }));
89
+ return;
90
+ }
91
+ var proto = el instanceof HTMLTextAreaElement
92
+ ? HTMLTextAreaElement.prototype
93
+ : (el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype);
94
+ var desc = Object.getOwnPropertyDescriptor(proto, "value");
95
+ if (desc && desc.set) desc.set.call(el, value);
96
+ else el.value = value;
97
+ el.dispatchEvent(new Event("input", { bubbles: true }));
98
+ el.dispatchEvent(new Event("change", { bubbles: true }));
99
+ }
100
+
101
+ function textOf(el) {
102
+ if (!el) return "";
103
+ var t = el.value != null && String(el.value) !== "" ? String(el.value) : String(el.textContent || "");
104
+ return t.replace(/\\s+/g, " ").trim();
105
+ }
106
+
107
+ function attrOf(el, name) {
108
+ if (!el) return null;
109
+ return el.getAttribute(name);
110
+ }
111
+
112
+ function classList(el) {
113
+ return el ? String(el.getAttribute("class") || "") : "";
114
+ }
115
+
116
+ function dragEl(src, dst) {
117
+ var dt = null;
118
+ try { dt = new DataTransfer(); } catch (e) { dt = null; }
119
+ var types = ["dragstart", "dragenter", "dragover", "drop", "dragend"];
120
+ for (var i = 0; i < types.length; i += 1) {
121
+ var target = (types[i] === "dragstart" || types[i] === "dragend") ? src : dst;
122
+ var ev = new DragEvent(types[i], { bubbles: true, cancelable: true, dataTransfer: dt });
123
+ target.dispatchEvent(ev);
124
+ }
125
+ }
126
+
127
+ window.__keveOps = {
128
+ version: "v1",
129
+ resolve: resolve,
130
+ count: function (refs) { return resolve(refs).length; },
131
+ countNonEmpty: function (refs) {
132
+ return resolve(refs).filter(function (el) { return textOf(el).length > 0; }).length;
133
+ },
134
+ text: function (refs) { return textOf(first(refs)); },
135
+ texts: function (refs) { return resolve(refs).map(textOf); },
136
+ visible: function (refs) { return isVisible(first(refs)); },
137
+ hidden: function (refs) { var els = resolve(refs); return !els.length || !isVisible(els[0]); },
138
+ exists: function (refs) { return resolve(refs).length > 0; },
139
+ attr: function (refs, name) { return attrOf(first(refs), name); },
140
+ classes: function (refs) { return classList(first(refs)); },
141
+ click: function (refs, dbl) { clickEl(first(refs), !!dbl); return "ok"; },
142
+ hover: function (refs) { var el = first(refs); el.scrollIntoView({ block: "center" }); mouse(el, "mouseover"); mouse(el, "mouseenter"); mouse(el, "mousemove"); return "ok"; },
143
+ type: function (refs, text) { setValue(first(refs), text); return "ok"; },
144
+ clear: function (refs) { setValue(first(refs), ""); return "ok"; },
145
+ scrollIntoView: function (refs) { first(refs).scrollIntoView({ block: "center", inline: "center" }); return "ok"; },
146
+ scrollPage: function (dy) { window.scrollBy(0, Number(dy) || 600); return "ok"; },
147
+ drag: function (srcRefs, dstRefs) { dragEl(first(srcRefs), first(dstRefs)); return "ok"; },
148
+ url: function () { return location.href; },
149
+ title: function () { return String(document.title || ""); },
150
+ waitSelector: function (css, visible) {
151
+ var els = locator().queryAllDeep(document, css);
152
+ if (!els.length) return false;
153
+ return visible === false ? !isVisible(els[0]) : isVisible(els[0]);
154
+ },
155
+ evaluate: function (expr) {
156
+ // 平台 WRITE_GLOBAL 的 javascript hop 是表达式形态
157
+ return (0, eval)("(" + expr + ")");
158
+ },
159
+ };
160
+ window.__keveOpsSource = "v1";
161
+ return "ok";
162
+ })()`;
163
+ }
164
+ /**
165
+ * 网络记录:waitForResponse 的就绪门依赖它。
166
+ * 在 AUT 里最早注入,之后所有 fetch/XHR 都会留下 {url, method, status, at}。
167
+ * 只记录元数据不记录 body —— 读 body 会消费流,影响被测应用自身逻辑。
168
+ */
169
+ function netLogSource() {
170
+ return `(() => {
171
+ "use strict";
172
+ if (window.__keveNetLogSource === "v1") return "cached";
173
+ var log = [];
174
+ window.__keveNetLog = log;
175
+
176
+ function push(entry) {
177
+ log.push(entry);
178
+ if (log.length > 500) log.splice(0, log.length - 500);
179
+ }
180
+
181
+ var origFetch = window.fetch;
182
+ if (typeof origFetch === "function") {
183
+ window.fetch = function (input, init) {
184
+ var url = typeof input === "string" ? input : (input && input.url) || "";
185
+ var method = String((init && init.method) || (input && input.method) || "GET").toUpperCase();
186
+ var at = Date.now();
187
+ var p = origFetch.apply(this, arguments);
188
+ if (p && typeof p.then === "function") {
189
+ p.then(function (res) {
190
+ push({ url: String(url), method: method, status: res && res.status, at: at });
191
+ return res;
192
+ }, function (err) {
193
+ push({ url: String(url), method: method, status: 0, at: at, error: String(err && err.message || err) });
194
+ });
195
+ }
196
+ return p;
197
+ };
198
+ }
199
+
200
+ var XO = XMLHttpRequest.prototype.open;
201
+ var XS = XMLHttpRequest.prototype.send;
202
+ XMLHttpRequest.prototype.open = function (method, url) {
203
+ this.__keve = { method: String(method || "GET").toUpperCase(), url: String(url || ""), at: Date.now() };
204
+ return XO.apply(this, arguments);
205
+ };
206
+ XMLHttpRequest.prototype.send = function () {
207
+ var self = this;
208
+ this.addEventListener("loadend", function () {
209
+ var meta = self.__keve || {};
210
+ push({ url: meta.url || "", method: meta.method || "GET", status: self.status, at: meta.at || Date.now() });
211
+ });
212
+ return XS.apply(this, arguments);
213
+ };
214
+
215
+ window.__keveNetLogSource = "v1";
216
+ return "ok";
217
+ })()`;
218
+ }
219
+ /** 三项注入合并(幂等,重复调用无副作用) */
220
+ function bootstrapSource(locatorSrc) {
221
+ return `${locatorSrc};\n${netLogSource()};\n${pageOpsSource()}`;
222
+ }
223
+ /**
224
+ * 运行诊断采集(AUT 主世界)。
225
+ *
226
+ * Cypress 侧不能订阅 CDP 域事件(`Cypress.automation` 只有 request/response,
227
+ * 没有 domain event 通道),所以这里在**主世界**劫持 console / 全局错误 /
228
+ * fetch / XHR,并用 PerformanceObserver 采集 LCP 与 longtask。
229
+ *
230
+ * 采集结果写入 `document.documentElement.dataset.keveDiagnostics`,因为
231
+ * isolated world 与主世界共享 DOM、但不共享 JS 全局,DOM 属性是唯一稳定的
232
+ * 跨世界只读通道。Agent 结束后由 isolated world 读一次快照即可。
233
+ *
234
+ * 约束:
235
+ * - 必须调用原始 console / fetch / XHR,避免递归采集;
236
+ * - 不读 body(会消费流,影响被测应用);
237
+ * - 文本与样本都做截断/限量,避免诊断本身撑爆报告;
238
+ * - 只写 DOM 属性,不改 `window.__keveNetLog`(waitForResponse 依赖它)。
239
+ */
240
+ function diagnosticsSource() {
241
+ return `(() => {
242
+ "use strict";
243
+ if (window.__keveDiagnosticsSource === "v1") return "cached";
244
+
245
+ var MAX_TEXT = 1000;
246
+ var MAX_RECORDS = 300;
247
+ var browserRecords = [];
248
+ var networkRecords = [];
249
+ var flushTimer = null;
250
+ var lcpMs = 0;
251
+ var longTaskCount = 0;
252
+ var longTaskMs = 0;
253
+
254
+ function clip(value, max) {
255
+ var s = String(value == null ? "" : value);
256
+ return s.length > max ? s.slice(0, max) + "…" : s;
257
+ }
258
+
259
+ function flush() {
260
+ try {
261
+ var root = document.documentElement || document.body;
262
+ if (!root) {
263
+ scheduleFlush();
264
+ return;
265
+ }
266
+ var page = {};
267
+ try {
268
+ var nav = (performance.getEntriesByType && performance.getEntriesByType("navigation") || [])[0];
269
+ if (nav) {
270
+ if (nav.domContentLoadedEventEnd) page.domContentLoadedMs = Math.round(nav.domContentLoadedEventEnd);
271
+ if (nav.loadEventEnd) page.loadMs = Math.round(nav.loadEventEnd);
272
+ }
273
+ if (lcpMs > 0) page.lcpMs = Math.round(lcpMs);
274
+ if (longTaskCount > 0) {
275
+ page.longTaskCount = longTaskCount;
276
+ page.longTaskMs = Math.round(longTaskMs);
277
+ }
278
+ var mem = performance.memory;
279
+ if (mem && mem.usedJSHeapSize) page.heapUsedMb = Math.round(mem.usedJSHeapSize / 1048576 * 10) / 10;
280
+ } catch (e) { /* 性能 API 缺失时忽略 */ }
281
+ root.dataset.keveDiagnostics = JSON.stringify({
282
+ browser: browserRecords,
283
+ network: networkRecords,
284
+ page: page
285
+ });
286
+ } catch (e) { /* 属性写入失败不影响用例 */ }
287
+ }
288
+
289
+ function scheduleFlush() {
290
+ if (flushTimer) return;
291
+ flushTimer = setTimeout(function () {
292
+ flushTimer = null;
293
+ flush();
294
+ }, 100);
295
+ }
296
+
297
+ function pushBrowser(kind, level, text) {
298
+ browserRecords.push({ kind: kind, level: level, text: clip(text, MAX_TEXT), at: Date.now() });
299
+ if (browserRecords.length > MAX_RECORDS) browserRecords.splice(0, browserRecords.length - MAX_RECORDS);
300
+ scheduleFlush();
301
+ }
302
+
303
+ function pushNetwork(entry) {
304
+ entry.url = clip(entry.url, 500);
305
+ if (entry.error) entry.error = clip(entry.error, 300);
306
+ networkRecords.push(entry);
307
+ if (networkRecords.length > MAX_RECORDS) networkRecords.splice(0, networkRecords.length - MAX_RECORDS);
308
+ scheduleFlush();
309
+ }
310
+
311
+ // ── Console:只摘 error/warn,普通 log 不占样本额度 ──
312
+ ["error", "warn"].forEach(function (level) {
313
+ var orig = console[level];
314
+ if (typeof orig !== "function") return;
315
+ console[level] = function () {
316
+ try {
317
+ pushBrowser("console", level, Array.prototype.map.call(arguments, function (a) {
318
+ if (a instanceof Error) return a.stack || a.message;
319
+ if (typeof a === "object") {
320
+ try { return JSON.stringify(a); } catch (e) { return String(a); }
321
+ }
322
+ return String(a);
323
+ }).join(" "));
324
+ } catch (e) { /* 采集失败不影响原始输出 */ }
325
+ return orig.apply(console, arguments);
326
+ };
327
+ });
328
+
329
+ // ── 全局未捕获错误 / 未处理的 Promise rejection ──
330
+ window.addEventListener("error", function (event) {
331
+ var err = event && event.error;
332
+ var text = (err && (err.stack || err.message)) || (event && event.message) || "unknown error";
333
+ pushBrowser("pageerror", "error", text);
334
+ }, true);
335
+
336
+ window.addEventListener("unhandledrejection", function (event) {
337
+ var reason = event && event.reason;
338
+ var text = (reason && (reason.stack || reason.message)) || String(reason || "unhandled rejection");
339
+ pushBrowser("exception", "error", text);
340
+ });
341
+
342
+ // ── fetch ──
343
+ var origFetch = window.fetch;
344
+ if (typeof origFetch === "function") {
345
+ window.fetch = function (input, init) {
346
+ var url = typeof input === "string" ? input : (input && input.url) || "";
347
+ var method = String((init && init.method) || (input && input.method) || "GET").toUpperCase();
348
+ var startedAt = Date.now();
349
+ var p = origFetch.apply(this, arguments);
350
+ if (p && typeof p.then === "function") {
351
+ p.then(function (res) {
352
+ pushNetwork({
353
+ url: String(url),
354
+ method: method,
355
+ status: res && res.status,
356
+ durationMs: Date.now() - startedAt,
357
+ at: startedAt
358
+ });
359
+ return res;
360
+ }, function (err) {
361
+ pushNetwork({
362
+ url: String(url),
363
+ method: method,
364
+ status: 0,
365
+ durationMs: Date.now() - startedAt,
366
+ error: String((err && err.message) || err),
367
+ at: startedAt
368
+ });
369
+ });
370
+ }
371
+ return p;
372
+ };
373
+ }
374
+
375
+ // ── XHR ──
376
+ var XO = XMLHttpRequest.prototype.open;
377
+ var XS = XMLHttpRequest.prototype.send;
378
+ XMLHttpRequest.prototype.open = function (method, url) {
379
+ this.__keveDiag = {
380
+ method: String(method || "GET").toUpperCase(),
381
+ url: String(url || ""),
382
+ startedAt: Date.now()
383
+ };
384
+ return XO.apply(this, arguments);
385
+ };
386
+ XMLHttpRequest.prototype.send = function () {
387
+ var self = this;
388
+ this.addEventListener("loadend", function () {
389
+ var meta = self.__keveDiag || {};
390
+ pushNetwork({
391
+ url: meta.url || "",
392
+ method: meta.method || "GET",
393
+ status: self.status,
394
+ durationMs: meta.startedAt ? Date.now() - meta.startedAt : undefined,
395
+ error: self.status === 0 ? "network error" : undefined,
396
+ at: meta.startedAt
397
+ });
398
+ });
399
+ return XS.apply(this, arguments);
400
+ };
401
+
402
+ // ── 性能:LCP / longtask(buffered 让先发生的条目也能补收) ──
403
+ try {
404
+ if (typeof PerformanceObserver === "function") {
405
+ var lcpObs = new PerformanceObserver(function (list) {
406
+ var entries = list.getEntries();
407
+ for (var i = 0; i < entries.length; i++) {
408
+ var t = entries[i].startTime || entries[i].renderTime || 0;
409
+ if (t > lcpMs) lcpMs = t;
410
+ }
411
+ scheduleFlush();
412
+ });
413
+ lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
414
+
415
+ var longObs = new PerformanceObserver(function (list) {
416
+ var entries = list.getEntries();
417
+ for (var j = 0; j < entries.length; j++) {
418
+ longTaskCount += 1;
419
+ longTaskMs += Number(entries[j].duration || 0);
420
+ }
421
+ scheduleFlush();
422
+ });
423
+ longObs.observe({ type: "longtask", buffered: true });
424
+ }
425
+ } catch (e) { /* 浏览器不支持时静默降级 */ }
426
+
427
+ if (document.readyState === "loading") {
428
+ document.addEventListener("DOMContentLoaded", flush, { once: true });
429
+ }
430
+ window.addEventListener("load", flush, { once: true });
431
+ // isolated world 读取前会 dispatch 该事件,要求主世界立即刷新 DOM 快照,
432
+ // 避免最后一条网络/console 记录落在 debounce 窗口里没被读走。
433
+ window.addEventListener("keve:flush-diagnostics", flush);
434
+
435
+ window.__keveDiagnosticsSource = "v1";
436
+ flush();
437
+ return "ok";
438
+ })()`;
439
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * reporter — Cypress 侧结果收集
3
+ *
4
+ * Cypress 没有 Playwright 那种「进程内 Reporter」能直接拿到 TestCase/TestResult;
5
+ * 这里用全局 afterEach 捞出当前用例的状态,经 HTTP 桥送回 Node 落盘。
6
+ *
7
+ * 落盘形态与 Playwright 完全一致:
8
+ * - confidence-data.jsonl:由 core/confidence.ts 的分类逻辑生成(同源)
9
+ * - test-results.json:Playwright JSON reporter 的结构(bridge 内合成)
10
+ * - report-data.json:同一份 generateReportData 汇总
11
+ *
12
+ * Mocha 的 afterEach 拿不到「用例耗时」的精确起点,这里用 beginTestState 的
13
+ * 时间戳兜底;Cypress 的 `this.currentTest.duration` 在有值时优先。
14
+ */
15
+ /**
16
+ * 安装结果收集钩子。
17
+ * 需要在场景注册之前调用(与 installCypressHost 同一时机),保证 afterEach
18
+ * 能拿到装饰器执行过程中写入 state 的附件。
19
+ */
20
+ export declare function installCypressReporter(): void;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * reporter — Cypress 侧结果收集
4
+ *
5
+ * Cypress 没有 Playwright 那种「进程内 Reporter」能直接拿到 TestCase/TestResult;
6
+ * 这里用全局 afterEach 捞出当前用例的状态,经 HTTP 桥送回 Node 落盘。
7
+ *
8
+ * 落盘形态与 Playwright 完全一致:
9
+ * - confidence-data.jsonl:由 core/confidence.ts 的分类逻辑生成(同源)
10
+ * - test-results.json:Playwright JSON reporter 的结构(bridge 内合成)
11
+ * - report-data.json:同一份 generateReportData 汇总
12
+ *
13
+ * Mocha 的 afterEach 拿不到「用例耗时」的精确起点,这里用 beginTestState 的
14
+ * 时间戳兜底;Cypress 的 `this.currentTest.duration` 在有值时优先。
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.installCypressReporter = installCypressReporter;
18
+ const adapter_js_1 = require("./adapter.js");
19
+ const state_js_1 = require("./state.js");
20
+ /** 从 Mocha 的 currentTest 里取错误文案(Cypress 会把失败信息挂在不同字段) */
21
+ function extractError(test) {
22
+ if (!test)
23
+ return undefined;
24
+ const err = test.err;
25
+ if (!err)
26
+ return undefined;
27
+ return String(err.message || err.stack || err);
28
+ }
29
+ function mapStatus(test) {
30
+ const state = String(test?.state || '');
31
+ if (state === 'passed')
32
+ return 'passed';
33
+ if (state === 'pending')
34
+ return 'skipped';
35
+ if (test?.timedOut)
36
+ return 'timedOut';
37
+ return 'failed';
38
+ }
39
+ /**
40
+ * suite 标题 = @keveModel 注册时的 describe 标题(`M_SG90061: …`)。
41
+ *
42
+ * Cypress 12 的 Mocha Test 上**没有** `titlePath`(探针实测 ownKeys 无此字段),
43
+ * 只能沿 `parent` 链向上收集,取最靠近用例的那个非 root 祖先标题 —— 语义等价于
44
+ * Playwright `suitePath` 的末段,reportData.buildCaseResultMap 就是按末段取模块名。
45
+ */
46
+ function suiteTitleOf(test) {
47
+ const titles = [];
48
+ let node = test?.parent;
49
+ while (node && !node.root) {
50
+ if (node.title)
51
+ titles.push(String(node.title));
52
+ node = node.parent;
53
+ }
54
+ return titles.length ? titles[titles.length - 1] : '';
55
+ }
56
+ let installed = false;
57
+ /**
58
+ * 安装结果收集钩子。
59
+ * 需要在场景注册之前调用(与 installCypressHost 同一时机),保证 afterEach
60
+ * 能拿到装饰器执行过程中写入 state 的附件。
61
+ */
62
+ function installCypressReporter() {
63
+ if (installed)
64
+ return;
65
+ installed = true;
66
+ const c = globalThis.Cypress;
67
+ const afterEach = c?.afterEach || globalThis.afterEach;
68
+ if (typeof afterEach !== 'function') {
69
+ throw new Error('[cypress-engine] 未找到 afterEach:本模块只能在 Cypress spec 中运行');
70
+ }
71
+ // 注册在根级:Cypress 会先跑 suite 内的 afterEach(场景 afterHook),最后跑根级;
72
+ // 因此这里能拿到装饰器执行期间写入的全部附件。
73
+ afterEach(function () {
74
+ const test = this.currentTest;
75
+ if (!test)
76
+ return;
77
+ const state = (0, state_js_1.getTestState)();
78
+ const suiteTitle = suiteTitleOf(test);
79
+ const title = state?.title || '';
80
+ // 只认 @keveScene 注册的用例:state 必须存在且标题形如 `S90061: 描述`。
81
+ // 注意不能退化成看 suite 标题前缀 —— Cypress 在 hook 失败时也会跑根级 afterEach,
82
+ // 那时 suite 名仍是 `M_` 开头,会把 hook 误收成一条空用例(state.title 为空)。
83
+ if (!state || !/^[A-Z][A-Z0-9_]*\d*:/.test(title))
84
+ return;
85
+ const rawStatus = mapStatus(test);
86
+ // 应用侧未捕获异常已被 Cypress 放行(return false),不会中断测试执行;
87
+ // 最终状态仍以 Cypress 的真实用例结果为准。通过用例不因页面运行时噪音
88
+ // 显示失败,失败用例再把未捕获异常合并进错误文本做根因补充。
89
+ const uncaughtErrors = state?.uncaughtErrors || [];
90
+ const status = rawStatus;
91
+ const testError = rawStatus === 'passed' || rawStatus === 'skipped' ? undefined : extractError(test);
92
+ const error = uncaughtErrors.length && rawStatus !== 'passed' && rawStatus !== 'skipped'
93
+ ? [...uncaughtErrors, testError || ''].filter(Boolean).join('\n')
94
+ : testError;
95
+ const record = {
96
+ title,
97
+ suiteTitle,
98
+ file: state?.file || '',
99
+ status,
100
+ duration: Number(test.duration || 0),
101
+ startTime: new Date(Date.now() - Number(test.duration || 0)).toISOString(),
102
+ error,
103
+ logs: state?.logs || [],
104
+ uncaughtErrors,
105
+ attachments: state?.attachments || [],
106
+ };
107
+ (0, state_js_1.endTestState)();
108
+ // 必须 return Cypress 链:这样 Cypress 会等请求真正返回后才结束该 hook,
109
+ // 保证 after:run(Node 侧 finalize)读到的是完整结果集。
110
+ // 注意不能用 `await cy.xxx()`,只能用返回链的形态。
111
+ // 注意:这里必须是命令链 `cy.wrap`,不是运行时对象 `Cypress.wrap`(不存在)。
112
+ const chain = globalThis.cy;
113
+ if (!chain || typeof chain.wrap !== 'function') {
114
+ console.error(`[cypress-engine] 结果落盘失败(${record.title}):未找到 cy 命令链`);
115
+ return;
116
+ }
117
+ return chain.wrap(null, { log: false }).then({ timeout: 60_000 }, async () => {
118
+ try {
119
+ const out = await (0, adapter_js_1.createBridge)().post('/report/test', record);
120
+ if (!out?.ok)
121
+ throw new Error(out?.error || 'unknown');
122
+ }
123
+ catch (err) {
124
+ console.error(`[cypress-engine] 结果落盘失败(${record.title}):${err?.message || err}`);
125
+ }
126
+ });
127
+ });
128
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @kkeve/suite/engine-cypress/setup — Cypress 引擎的 Node 侧入口
3
+ *
4
+ * 只给 cypress.config 的 `setupNodeEvents` 使用(Node 环境):
5
+ *
6
+ * import { cypressSetup } from '@kkeve/suite/engine-cypress/setup';
7
+ *
8
+ * export default defineConfig({
9
+ * e2e: {
10
+ * setupNodeEvents(on, config) {
11
+ * return cypressSetup(on, config);
12
+ * },
13
+ * },
14
+ * });
15
+ *
16
+ * 职责:起本地 HTTP 桥(截图对比 / 文件落盘 / 报告汇总),并把端口写回 config.env。
17
+ * ⚠️ 绝不能从浏览器侧入口(@kkeve/suite/engine-cypress)import 本模块 ——
18
+ * 它依赖 node:http / node:fs。
19
+ */
20
+ export { setupKeveBridge, setupKeveBridge as cypressSetup, type CypressTestResultRecord, } from './bridge.js';
21
+ export { setupKeveBridge as default } from './bridge.js';
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * @kkeve/suite/engine-cypress/setup — Cypress 引擎的 Node 侧入口
4
+ *
5
+ * 只给 cypress.config 的 `setupNodeEvents` 使用(Node 环境):
6
+ *
7
+ * import { cypressSetup } from '@kkeve/suite/engine-cypress/setup';
8
+ *
9
+ * export default defineConfig({
10
+ * e2e: {
11
+ * setupNodeEvents(on, config) {
12
+ * return cypressSetup(on, config);
13
+ * },
14
+ * },
15
+ * });
16
+ *
17
+ * 职责:起本地 HTTP 桥(截图对比 / 文件落盘 / 报告汇总),并把端口写回 config.env。
18
+ * ⚠️ 绝不能从浏览器侧入口(@kkeve/suite/engine-cypress)import 本模块 ——
19
+ * 它依赖 node:http / node:fs。
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.default = exports.cypressSetup = exports.setupKeveBridge = void 0;
23
+ var bridge_js_1 = require("./bridge.js");
24
+ Object.defineProperty(exports, "setupKeveBridge", { enumerable: true, get: function () { return bridge_js_1.setupKeveBridge; } });
25
+ Object.defineProperty(exports, "cypressSetup", { enumerable: true, get: function () { return bridge_js_1.setupKeveBridge; } });
26
+ var bridge_js_2 = require("./bridge.js");
27
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return bridge_js_2.setupKeveBridge; } });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * shotBuffer — 截图对比结果的 goal 级缓冲(浏览器侧)
3
+ *
4
+ * core/screenshotDiff.ts 是 Node 模块(依赖 fs),不能被打进 Cypress 的浏览器
5
+ * bundle。但结构必须一致:keveGoal 要把本 goal 内所有对比结果(matched /
6
+ * baseline_required / exceeded)写进 attachment 的 screenshotDiff 字段。
7
+ * 这里只做「暂存 + 取出」,真正的对比在 Node 侧经 bridge 完成。
8
+ */
9
+ export interface CyShotDiffResult {
10
+ key: string;
11
+ status: 'matched' | 'exceeded' | 'baseline_required' | 'size_mismatch';
12
+ diffRatio: number | null;
13
+ threshold: number;
14
+ baselineVersion: number;
15
+ baselineState?: 'candidate' | 'active';
16
+ baselinePath: string;
17
+ actualPath: string;
18
+ diffPath: string;
19
+ env: string;
20
+ viewport: string;
21
+ /** 差异热区(与 core/screenshotDiff.ts 的 ShotDiffResult.regions 同形,始终存在) */
22
+ regions: Array<{
23
+ x: number;
24
+ y: number;
25
+ w: number;
26
+ h: number;
27
+ ratio: number;
28
+ }>;
29
+ message: string;
30
+ comparedAt: string;
31
+ }
32
+ /** 与 Playwright 的 ScreenshotDiffError 同形:带 shotDiff 结构,keveGoal 据此短路 */
33
+ export declare class CypressScreenshotDiffError extends Error {
34
+ shotDiff: CyShotDiffResult;
35
+ constructor(result: CyShotDiffResult);
36
+ }
37
+ export declare function beginGoalShotDiffCapture(): void;
38
+ export declare function isGoalShotDiffCaptureActive(): boolean;
39
+ export declare function drainGoalShotDiff(): CyShotDiffResult[];
40
+ export declare function recordShotDiff(result: CyShotDiffResult): void;