@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,108 @@
1
+ /**
2
+ * adapter — Cypress 版 EngineAdapter
3
+ *
4
+ * 与 PlaywrightEngine 逐方法对齐语义(唯一差异见各方法注释)。定位与操作都通过
5
+ * CDP 在 AUT 的 isolated world 内执行,Node 侧能力(截图对比、落盘)经 HTTP 桥。
6
+ *
7
+ * 本模块**不得** import '@playwright/test'(含 core/engineAdapter.js、elementResolver.js
8
+ * 这类会传递依赖 playwright-core 的模块):playwright-core → node:fs 会把整条链
9
+ * 拉进浏览器 bundle,esbuild 直接失败。类型只允许 `import type`。
10
+ */
11
+ import type { ElementRef, ElementEntry, DefinedPage } from '../core/page-object/definePage.js';
12
+ import type { EngineAdapter, WaitForResponseOptions, WaitForResponseResult } from '../core/page-object/engine-types.js';
13
+ import type { ExpectScreenshotOptions } from '../core/judge/screenshotDiff.js';
14
+ import type { Cdp } from './cdp.js';
15
+ import { type CyShotDiffResult } from './shotBuffer.js';
16
+ /** 浏览器 → Node 的调用封装(端口来自 config.env.KEVE_BRIDGE_PORT) */
17
+ export interface Bridge {
18
+ post(route: string, payload?: Record<string, unknown>): Promise<any>;
19
+ }
20
+ export declare function createBridge(): Bridge;
21
+ declare function escapeRegExp(s: string): string;
22
+ export declare class CypressEngine implements EngineAdapter {
23
+ private cdp;
24
+ private bridge;
25
+ constructor(cdp: Cdp, bridge: Bridge);
26
+ /**
27
+ * 每次调用前确保:AUT 存在 + isolated world 有效 + 定位器/操作原语已注入。
28
+ * 导航会销毁注入,所以这里每次校验 `__keveOpsSource` 版本号(幂等且极廉价)。
29
+ */
30
+ private ensureReady;
31
+ private ops;
32
+ private opsCall;
33
+ /** 定位失败时给出可读错误(Playwright 是等元素超时,这里是立即失败) */
34
+ private withWait;
35
+ navigate(url: string): Promise<void>;
36
+ openPage(page: DefinedPage, overrides?: Record<string, string>): Promise<void>;
37
+ callScene(sceneId: string, keveGoal?: unknown): Promise<void>;
38
+ waitForReady(defPage: DefinedPage): Promise<void>;
39
+ /**
40
+ * 等到匹配的响应。
41
+ * 与 Playwright 的差异:不能「先挂监听再等」——注入早于导航,只能查已有记录;
42
+ * 因此语义是「从本 goal 开始起的时间窗内出现该响应」。软等待(默认)超时不失败。
43
+ */
44
+ waitForResponse(urlPart: string | RegExp, opts?: WaitForResponseOptions): Promise<WaitForResponseResult>;
45
+ click(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
46
+ dblClick(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
47
+ /** 强制点击:CDP 下没有 actionability 检查,语义等同于 click */
48
+ forceClick(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
49
+ type(text: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
50
+ clear(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
51
+ hover(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
52
+ drag(source: ElementRef | ElementEntry, target: ElementRef | ElementEntry): Promise<void>;
53
+ scroll(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
54
+ wait(ms: number): Promise<void>;
55
+ waitForElement(selector: string, opts?: {
56
+ timeout?: number;
57
+ visible?: boolean;
58
+ }): Promise<{
59
+ found: boolean;
60
+ }>;
61
+ refresh(): Promise<void>;
62
+ evaluate(expression: string): Promise<any>;
63
+ screenshot(name: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
64
+ /**
65
+ * 截图对比:Node 侧能力,走桥。
66
+ * 元素级截图(refs 非空)暂按整页截图处理 —— Cypress/CDP 无法取元素级 clip 的
67
+ * 高保真结果(滚动容器与 transform 会错位);调用方若有元素级需求需显式记录。
68
+ */
69
+ expectScreenshot(key: string, opts?: ExpectScreenshotOptions, ...refs: (ElementRef | ElementEntry)[]): Promise<CyShotDiffResult>;
70
+ text(...refs: (ElementRef | ElementEntry)[]): Promise<string>;
71
+ expectVisible(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
72
+ expectHidden(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
73
+ expectText(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
74
+ expectNotText(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
75
+ expectTextEquals(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
76
+ expectTextNotEquals(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
77
+ expectCount(count: number, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
78
+ expectNonEmptyCount(count: number, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
79
+ expectExists(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
80
+ expectNotExists(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
81
+ expectClass(className: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
82
+ expectNotClass(className: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
83
+ expectAttribute(attr: string, value: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
84
+ expectAttributeContains(attr: string, value: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
85
+ expectUrlContains(part: string): Promise<void>;
86
+ expectUrlNotContains(part: string): Promise<void>;
87
+ expectUrlEquals(url: string): Promise<void>;
88
+ expectUrlNotEquals(url: string): Promise<void>;
89
+ expectTitle(part: string): Promise<void>;
90
+ expectTitleEquals(title: string): Promise<void>;
91
+ /** Cypress 下没有 Playwright Page,返回 CDP 驱动(供探索层调用) */
92
+ rawPage(): any;
93
+ /** 当前 AUT URL(keveGoal 记录诊断上下文用;context 失效时回退 frame.url) */
94
+ currentUrl(): Promise<string>;
95
+ /**
96
+ * 截图并落盘到 taskDir/test-artifacts/round-N/screenshots,返回相对 taskDir 的路径。
97
+ * 与 Playwright 侧 captureScreenshot 的产物路径同形,报告可直接消费。
98
+ */
99
+ captureArtifact(name: string): Promise<string>;
100
+ ariaSnapshot(_options?: {
101
+ mode?: string;
102
+ }): Promise<string>;
103
+ injectCookies(cookies: any[]): Promise<void>;
104
+ /** 当前环境(供脚本判断,与 Playwright 侧同源) */
105
+ get env(): string;
106
+ }
107
+ /** URL 断言用的正则转义(导出以便测试) */
108
+ export { escapeRegExp };
@@ -0,0 +1,514 @@
1
+ "use strict";
2
+ /**
3
+ * adapter — Cypress 版 EngineAdapter
4
+ *
5
+ * 与 PlaywrightEngine 逐方法对齐语义(唯一差异见各方法注释)。定位与操作都通过
6
+ * CDP 在 AUT 的 isolated world 内执行,Node 侧能力(截图对比、落盘)经 HTTP 桥。
7
+ *
8
+ * 本模块**不得** import '@playwright/test'(含 core/engineAdapter.js、elementResolver.js
9
+ * 这类会传递依赖 playwright-core 的模块):playwright-core → node:fs 会把整条链
10
+ * 拉进浏览器 bundle,esbuild 直接失败。类型只允许 `import type`。
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.CypressEngine = void 0;
47
+ exports.createBridge = createBridge;
48
+ exports.escapeRegExp = escapeRegExp;
49
+ const definePage_js_1 = require("../core/page-object/definePage.js");
50
+ const engine_types_js_1 = require("../core/page-object/engine-types.js");
51
+ const cdp_js_1 = require("./cdp.js");
52
+ const shotBuffer_js_1 = require("./shotBuffer.js");
53
+ function createBridge() {
54
+ const port = globalThis.Cypress?.env?.('KEVE_BRIDGE_PORT');
55
+ if (!port)
56
+ throw new Error('[cypress-engine] KEVE_BRIDGE_PORT 未注入:请确认 cypress.config 使用了 @kkeve/suite/engine-cypress 的 setupKeveBridge');
57
+ const base = `http://127.0.0.1:${port}`;
58
+ return {
59
+ async post(route, payload = {}) {
60
+ const res = await fetch(base + route, {
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/json' },
63
+ body: JSON.stringify(payload),
64
+ });
65
+ return await res.json();
66
+ },
67
+ };
68
+ }
69
+ /** refs 规范化:工厂函数必须先调用(与 PlaywrightEngine.resolve 一致) */
70
+ function normalizeRefs(refs) {
71
+ return refs.map((r) => {
72
+ if ((0, definePage_js_1.isElementFactory)(r)) {
73
+ throw new Error('Factory function must be called before passing to engine. e.g. engine.click(page.elements.itemByName("时间"))');
74
+ }
75
+ return r;
76
+ });
77
+ }
78
+ function escapeRegExp(s) {
79
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80
+ }
81
+ class CypressEngine {
82
+ cdp;
83
+ bridge;
84
+ constructor(cdp, bridge) {
85
+ this.cdp = cdp;
86
+ this.bridge = bridge;
87
+ }
88
+ // ── AUT 上下文准备 ────────────────────────────────────────────────
89
+ /**
90
+ * 每次调用前确保:AUT 存在 + isolated world 有效 + 定位器/操作原语已注入。
91
+ * 导航会销毁注入,所以这里每次校验 `__keveOpsSource` 版本号(幂等且极廉价)。
92
+ */
93
+ async ensureReady() {
94
+ const has = await this.cdp.evaluateInAut('Boolean(window.__keveOpsSource === "v1" && window.__keveNetLogSource === "v1" && window.__keveLocatorSource === "v1")').catch(() => false);
95
+ if (has)
96
+ return;
97
+ const { locatorSource } = await Promise.resolve().then(() => __importStar(require('../dsl/locatorSource.js')));
98
+ const { bootstrapSource } = await Promise.resolve().then(() => __importStar(require('./pageOps.js')));
99
+ await this.cdp.evaluateInAut(bootstrapSource(locatorSource()));
100
+ }
101
+ async ops(expr) {
102
+ await this.ensureReady();
103
+ return await this.cdp.evaluateInAut(expr);
104
+ }
105
+ async opsCall(fn, ...args) {
106
+ const encoded = args.map((a) => JSON.stringify(a ?? null)).join(',');
107
+ return await this.ops(`window.__keveOps.${fn}(${encoded})`);
108
+ }
109
+ /** 定位失败时给出可读错误(Playwright 是等元素超时,这里是立即失败) */
110
+ async withWait(fn, timeout = 10000, interval = 150) {
111
+ const deadline = Date.now() + timeout;
112
+ let lastErr;
113
+ for (;;) {
114
+ try {
115
+ return await fn();
116
+ }
117
+ catch (err) {
118
+ lastErr = err;
119
+ const msg = String(err?.message || err);
120
+ // 只有「元素未找到 / 断言暂不成立」值得重试;代码错误直接抛
121
+ const retryable = /元素未找到|not found|no-match|index-out-of-range|断言|expected|should|BUT/.test(msg);
122
+ if (!retryable || Date.now() >= deadline)
123
+ throw err;
124
+ await new Promise((r) => setTimeout(r, interval));
125
+ }
126
+ }
127
+ }
128
+ // ── 导航 ─────────────────────────────────────────────────────────
129
+ async navigate(url) {
130
+ if (!/^https?:\/\//.test(String(url || ''))) {
131
+ throw new Error(`navigate: URL 非法(必须是 http/https): ${JSON.stringify(url)}`);
132
+ }
133
+ await this.cdp.goto(url);
134
+ }
135
+ async openPage(page, overrides) {
136
+ await this.navigate((0, engine_types_js_1.resolvePageUrl)(page, overrides));
137
+ }
138
+ async callScene(sceneId, keveGoal) {
139
+ const { sceneImplMap } = await Promise.resolve().then(() => __importStar(require('../core/decorator/keve-registry.js')));
140
+ const fn = sceneImplMap.get(String(sceneId));
141
+ if (!fn)
142
+ throw new Error(`callScene: 场景 '${sceneId}' 未注册(可能被排除或不存在)`);
143
+ await fn({ engine: this, keveGoal });
144
+ }
145
+ async waitForReady(defPage) {
146
+ const cfg = defPage.waitForReady;
147
+ if (!cfg)
148
+ return;
149
+ if (cfg.api) {
150
+ // 软等待:与本文件 waitForResponse 默认语义一致
151
+ await this.waitForResponse(cfg.api, { timeout: 15000 });
152
+ }
153
+ if (cfg.ui) {
154
+ const entry = defPage.elements?.[cfg.ui];
155
+ if (entry && !(0, definePage_js_1.isElementFactory)(entry)) {
156
+ await this.withWait(() => this.expectVisible(entry), 10000);
157
+ }
158
+ }
159
+ }
160
+ /**
161
+ * 等到匹配的响应。
162
+ * 与 Playwright 的差异:不能「先挂监听再等」——注入早于导航,只能查已有记录;
163
+ * 因此语义是「从本 goal 开始起的时间窗内出现该响应」。软等待(默认)超时不失败。
164
+ */
165
+ async waitForResponse(urlPart, opts = {}) {
166
+ const timeout = opts.timeout ?? 15000;
167
+ const started = Date.now();
168
+ const probe = async () => {
169
+ const entry = await this.ops(`(() => {
170
+ var log = window.__keveNetLog || [];
171
+ var part = ${JSON.stringify(typeof urlPart === 'string' ? urlPart : urlPart.source)};
172
+ var isRe = ${typeof urlPart === 'string' ? 'false' : 'true'};
173
+ var method = ${JSON.stringify(opts.method || '')};
174
+ var statuses = ${JSON.stringify(opts.status || [])};
175
+ for (var i = log.length - 1; i >= 0; i -= 1) {
176
+ var e = log[i];
177
+ var hit = isRe ? new RegExp(part).test(String(e.url || '')) : String(e.url || '').indexOf(part) >= 0;
178
+ if (!hit) continue;
179
+ if (method && String(e.method || '').toUpperCase() !== method.toUpperCase()) continue;
180
+ if (statuses.length && statuses.indexOf(e.status) < 0) continue;
181
+ return e;
182
+ }
183
+ return null;
184
+ })()`);
185
+ if (!entry)
186
+ return undefined;
187
+ return { hit: true, url: entry.url, status: entry.status, body: null, elapsedMs: Date.now() - started };
188
+ };
189
+ try {
190
+ const hit = await (0, cdp_js_1.pollUntil)(probe, timeout, 150);
191
+ if (hit) {
192
+ if (opts.afterMs && opts.afterMs > 0)
193
+ await this.wait(opts.afterMs);
194
+ return hit;
195
+ }
196
+ throw new Error(`waitForResponse 超时(${timeout}ms)`);
197
+ }
198
+ catch (err) {
199
+ if (opts.required) {
200
+ throw new Error(`waitForResponse 超时(${timeout}ms):未等到匹配「${String(urlPart)}」的响应`);
201
+ }
202
+ console.warn(`[engine.waitForResponse] 软等待超时(${timeout}ms),继续执行:${String(urlPart)}`);
203
+ return { hit: false, url: '', status: 0, body: null, elapsedMs: Date.now() - started };
204
+ }
205
+ }
206
+ // ── 交互 ─────────────────────────────────────────────────────────
207
+ async click(...refs) {
208
+ const r = normalizeRefs(refs);
209
+ await this.withWait(() => this.opsCall('click', r, false));
210
+ }
211
+ async dblClick(...refs) {
212
+ const r = normalizeRefs(refs);
213
+ await this.withWait(() => this.opsCall('click', r, true));
214
+ }
215
+ /** 强制点击:CDP 下没有 actionability 检查,语义等同于 click */
216
+ async forceClick(...refs) {
217
+ await this.click(...refs);
218
+ }
219
+ async type(text, ...refs) {
220
+ const r = normalizeRefs(refs);
221
+ await this.withWait(() => this.opsCall('type', r, String(text ?? '')));
222
+ }
223
+ async clear(...refs) {
224
+ const r = normalizeRefs(refs);
225
+ await this.withWait(() => this.opsCall('clear', r));
226
+ }
227
+ async hover(...refs) {
228
+ const r = normalizeRefs(refs);
229
+ await this.withWait(() => this.opsCall('hover', r));
230
+ }
231
+ async drag(source, target) {
232
+ const s = normalizeRefs([source]);
233
+ const t = normalizeRefs([target]);
234
+ await this.withWait(() => this.opsCall('drag', s, t));
235
+ }
236
+ async scroll(...refs) {
237
+ if (refs.length === 0) {
238
+ await this.opsCall('scrollPage', 600);
239
+ return;
240
+ }
241
+ const r = normalizeRefs(refs);
242
+ await this.withWait(() => this.opsCall('scrollIntoView', r));
243
+ }
244
+ async wait(ms) {
245
+ await new Promise((r) => setTimeout(r, Math.max(0, Number(ms) || 0)));
246
+ }
247
+ async waitForElement(selector, opts = {}) {
248
+ const timeout = opts.timeout ?? 10000;
249
+ const found = await (0, cdp_js_1.pollUntil)(async () => {
250
+ const ok = await this.ops(`window.__keveOps.waitSelector(${JSON.stringify(selector)}, ${opts.visible === false ? 'false' : 'true'})`);
251
+ return ok ? true : undefined;
252
+ }, timeout, 150);
253
+ return { found: !!found };
254
+ }
255
+ async refresh() {
256
+ await this.cdp.reload();
257
+ }
258
+ async evaluate(expression) {
259
+ return await this.ops(`window.__keveOps.evaluate(${JSON.stringify(expression)})`);
260
+ }
261
+ // ── 截图 / 视觉对比 ──────────────────────────────────────────────
262
+ async screenshot(name, ...refs) {
263
+ await this.expectScreenshot(name, {}, ...refs);
264
+ }
265
+ /**
266
+ * 截图对比:Node 侧能力,走桥。
267
+ * 元素级截图(refs 非空)暂按整页截图处理 —— Cypress/CDP 无法取元素级 clip 的
268
+ * 高保真结果(滚动容器与 transform 会错位);调用方若有元素级需求需显式记录。
269
+ */
270
+ async expectScreenshot(key, opts = {}, ...refs) {
271
+ if (refs.length > 0) {
272
+ console.warn(`[engine.expectScreenshot] Cypress 下元素级截图按整页处理:${key}`);
273
+ }
274
+ const owned = !(0, shotBuffer_js_1.isGoalShotDiffCaptureActive)();
275
+ if (owned)
276
+ (0, shotBuffer_js_1.beginGoalShotDiffCapture)();
277
+ try {
278
+ const pngBase64 = await this.cdp.screenshot();
279
+ const resp = await this.bridge.post('/shot/compare', {
280
+ key,
281
+ pngBase64,
282
+ threshold: opts.threshold,
283
+ mask: opts.mask,
284
+ });
285
+ if (!resp?.ok) {
286
+ if (resp?.code === 'SHOT_DIFF_EXCEEDED') {
287
+ (0, shotBuffer_js_1.recordShotDiff)(resp.shotDiff);
288
+ throw new shotBuffer_js_1.CypressScreenshotDiffError(resp.shotDiff);
289
+ }
290
+ throw new Error(resp?.error || `截图对比失败:${key}`);
291
+ }
292
+ (0, shotBuffer_js_1.recordShotDiff)(resp.result);
293
+ return resp.result;
294
+ }
295
+ finally {
296
+ if (owned)
297
+ (0, shotBuffer_js_1.drainGoalShotDiff)();
298
+ }
299
+ }
300
+ // ── 读取 ─────────────────────────────────────────────────────────
301
+ async text(...refs) {
302
+ const r = normalizeRefs(refs);
303
+ return await this.withWait(() => this.opsCall('text', r));
304
+ }
305
+ // ── 断言 ─────────────────────────────────────────────────────────
306
+ async expectVisible(...refs) {
307
+ const r = normalizeRefs(refs);
308
+ await this.withWait(async () => {
309
+ const ok = await this.opsCall('visible', r);
310
+ if (!ok)
311
+ throw new Error(`断言失败:元素应可见,实际不可见 —— ${JSON.stringify(r).slice(0, 200)}`);
312
+ });
313
+ }
314
+ async expectHidden(...refs) {
315
+ const r = normalizeRefs(refs);
316
+ await this.withWait(async () => {
317
+ const ok = await this.opsCall('hidden', r);
318
+ if (!ok)
319
+ throw new Error(`断言失败:元素应隐藏,实际可见 —— ${JSON.stringify(r).slice(0, 200)}`);
320
+ });
321
+ }
322
+ async expectText(expected, ...refs) {
323
+ const r = normalizeRefs(refs);
324
+ await this.withWait(async () => {
325
+ const actual = await this.opsCall('text', r);
326
+ if (!actual.includes(expected)) {
327
+ throw new Error(`断言失败:元素文本应包含「${expected}」,实际显示「${actual}」而非预期`);
328
+ }
329
+ });
330
+ }
331
+ async expectNotText(expected, ...refs) {
332
+ const r = normalizeRefs(refs);
333
+ await this.withWait(async () => {
334
+ const actual = await this.opsCall('text', r);
335
+ if (actual.includes(expected)) {
336
+ throw new Error(`断言失败:元素文本不应包含「${expected}」,实际为「${actual}」`);
337
+ }
338
+ });
339
+ }
340
+ async expectTextEquals(expected, ...refs) {
341
+ const r = normalizeRefs(refs);
342
+ await this.withWait(async () => {
343
+ const actual = await this.opsCall('text', r);
344
+ if (actual !== String(expected).replace(/\s+/g, ' ').trim()) {
345
+ throw new Error(`断言失败:元素文本应等于「${expected}」,实际显示「${actual}」而非预期`);
346
+ }
347
+ });
348
+ }
349
+ async expectTextNotEquals(expected, ...refs) {
350
+ const r = normalizeRefs(refs);
351
+ await this.withWait(async () => {
352
+ const actual = await this.opsCall('text', r);
353
+ if (actual === String(expected).replace(/\s+/g, ' ').trim()) {
354
+ throw new Error(`断言失败:元素文本不应等于「${expected}」,实际为「${actual}」`);
355
+ }
356
+ });
357
+ }
358
+ async expectCount(count, ...refs) {
359
+ const r = normalizeRefs(refs);
360
+ await this.withWait(async () => {
361
+ const actual = await this.opsCall('count', r);
362
+ if (actual !== Number(count)) {
363
+ throw new Error(`断言失败:元素数量应为 ${count},实际为 ${actual}`);
364
+ }
365
+ });
366
+ }
367
+ async expectNonEmptyCount(count, ...refs) {
368
+ const r = normalizeRefs(refs);
369
+ await this.withWait(async () => {
370
+ const actual = await this.opsCall('countNonEmpty', r);
371
+ if (actual !== Number(count)) {
372
+ throw new Error(`断言失败:文本非空元素数量应为 ${count},实际为 ${actual}`);
373
+ }
374
+ });
375
+ }
376
+ async expectExists(...refs) {
377
+ const r = normalizeRefs(refs);
378
+ await this.withWait(async () => {
379
+ const ok = await this.opsCall('exists', r);
380
+ if (!ok)
381
+ throw new Error(`断言失败:元素应存在,实际不存在 —— ${JSON.stringify(r).slice(0, 200)}`);
382
+ });
383
+ }
384
+ async expectNotExists(...refs) {
385
+ const r = normalizeRefs(refs);
386
+ await this.withWait(async () => {
387
+ const ok = await this.opsCall('exists', r);
388
+ if (ok)
389
+ throw new Error(`断言失败:元素不应存在 —— ${JSON.stringify(r).slice(0, 200)}`);
390
+ });
391
+ }
392
+ async expectClass(className, ...refs) {
393
+ const r = normalizeRefs(refs);
394
+ const classes = await this.withWait(() => this.opsCall('classes', r));
395
+ if (!classes.split(/\s+/).includes(className)) {
396
+ throw new Error(`断言失败:元素应包含 class '${className}',实际为 '${classes}'`);
397
+ }
398
+ }
399
+ async expectNotClass(className, ...refs) {
400
+ const r = normalizeRefs(refs);
401
+ const classes = await this.withWait(() => this.opsCall('classes', r));
402
+ if (classes.split(/\s+/).includes(className)) {
403
+ throw new Error(`断言失败:元素不应包含 class '${className}',实际为 '${classes}'`);
404
+ }
405
+ }
406
+ async expectAttribute(attr, value, ...refs) {
407
+ const r = normalizeRefs(refs);
408
+ await this.withWait(async () => {
409
+ const actual = await this.opsCall('attr', r, attr);
410
+ if (actual !== value) {
411
+ throw new Error(`断言失败:属性 '${attr}' 应等于 '${value}',实际为 ${JSON.stringify(actual)}`);
412
+ }
413
+ });
414
+ }
415
+ async expectAttributeContains(attr, value, ...refs) {
416
+ const r = normalizeRefs(refs);
417
+ await this.withWait(async () => {
418
+ const actual = await this.opsCall('attr', r, attr);
419
+ if (actual === null || !actual.includes(value)) {
420
+ throw new Error(`断言失败:属性 '${attr}' 应包含 '${value}',实际为 ${JSON.stringify(actual)}`);
421
+ }
422
+ });
423
+ }
424
+ async expectUrlContains(part) {
425
+ await this.withWait(async () => {
426
+ const url = await this.opsCall('url');
427
+ if (!url.includes(part))
428
+ throw new Error(`断言失败:URL 应包含 '${part}',实际为 '${url}'`);
429
+ });
430
+ }
431
+ async expectUrlNotContains(part) {
432
+ const url = await this.opsCall('url');
433
+ if (url.includes(part))
434
+ throw new Error(`断言失败:URL 不应包含 '${part}',实际为 '${url}'`);
435
+ }
436
+ async expectUrlEquals(url) {
437
+ await this.withWait(async () => {
438
+ const actual = await this.opsCall('url');
439
+ if (actual !== url)
440
+ throw new Error(`断言失败:URL 应等于 '${url}',实际为 '${actual}'`);
441
+ });
442
+ }
443
+ async expectUrlNotEquals(url) {
444
+ const actual = await this.opsCall('url');
445
+ if (actual === url)
446
+ throw new Error(`断言失败:URL 不应等于 '${url}'`);
447
+ }
448
+ async expectTitle(part) {
449
+ await this.withWait(async () => {
450
+ const title = await this.opsCall('title');
451
+ if (!title.includes(part))
452
+ throw new Error(`断言失败:标题应包含 '${part}',实际为 '${title}'`);
453
+ });
454
+ }
455
+ async expectTitleEquals(title) {
456
+ await this.withWait(async () => {
457
+ const actual = await this.opsCall('title');
458
+ if (actual !== title)
459
+ throw new Error(`断言失败:标题应等于 '${title}',实际为 '${actual}'`);
460
+ });
461
+ }
462
+ // ── 探索辅助 ─────────────────────────────────────────────────────
463
+ /** Cypress 下没有 Playwright Page,返回 CDP 驱动(供探索层调用) */
464
+ rawPage() {
465
+ return this.cdp;
466
+ }
467
+ /** 当前 AUT URL(keveGoal 记录诊断上下文用;context 失效时回退 frame.url) */
468
+ async currentUrl() {
469
+ return await this.cdp.currentUrl();
470
+ }
471
+ /**
472
+ * 截图并落盘到 taskDir/test-artifacts/round-N/screenshots,返回相对 taskDir 的路径。
473
+ * 与 Playwright 侧 captureScreenshot 的产物路径同形,报告可直接消费。
474
+ */
475
+ async captureArtifact(name) {
476
+ const pngBase64 = await this.cdp.screenshot();
477
+ const resp = await this.bridge.post('/shot/save', { name, pngBase64 });
478
+ if (!resp?.ok)
479
+ throw new Error(resp?.error || '截图落盘失败');
480
+ return String(resp.path || '');
481
+ }
482
+ async ariaSnapshot(_options) {
483
+ // 仅取 AUT 的 AX 树(主会话默认调用拿到的是 Cypress runner UI)
484
+ const nodes = await this.cdp.axTree();
485
+ const lines = [];
486
+ for (const node of nodes) {
487
+ if (!node?.role?.value)
488
+ continue;
489
+ if (node.ignored)
490
+ continue;
491
+ const name = node.name?.value ? ` "${String(node.name.value).slice(0, 120)}"` : '';
492
+ lines.push(`- ${node.role.value}${name}`);
493
+ }
494
+ return lines.join('\n');
495
+ }
496
+ async injectCookies(cookies) {
497
+ for (const c of cookies || []) {
498
+ if (!c?.name)
499
+ continue;
500
+ await this.cdp.setCookie({
501
+ name: String(c.name),
502
+ value: String(c.value ?? ''),
503
+ domain: c.domain,
504
+ path: c.path,
505
+ secure: !!c.secure,
506
+ httpOnly: !!c.httpOnly,
507
+ expires: typeof c.expires === 'number' ? c.expires : undefined,
508
+ });
509
+ }
510
+ }
511
+ /** 当前环境(供脚本判断,与 Playwright 侧同源) */
512
+ get env() { return engine_types_js_1.KEVE_ENV; }
513
+ }
514
+ exports.CypressEngine = CypressEngine;