@kkeve/suite 0.0.13 → 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.
- package/dist/bin/keve.js +2 -1
- package/dist/commands/clean.js +2 -2
- package/dist/commands/config.js +7 -7
- package/dist/commands/init.js +2 -2
- package/dist/commands/report.js +7 -7
- package/dist/commands/run.js +16 -12
- package/dist/commands/test.js +7 -7
- package/dist/config.js +7 -7
- package/dist/core/action-log-writer.js +4 -1
- package/dist/core/decorator/keve-decorators.d.ts +128 -0
- package/dist/core/decorator/keve-decorators.js +254 -0
- package/dist/core/decorator/keve-host.d.ts +40 -0
- package/dist/core/decorator/keve-host.js +33 -0
- package/dist/core/{keve-registry.d.ts → decorator/keve-registry.d.ts} +19 -0
- package/dist/core/{keve-registry.js → decorator/keve-registry.js} +13 -1
- package/dist/core/generateReport/report.d.ts +1 -1
- package/dist/core/generateReport/report.js +2 -2
- package/dist/core/generateReport/reportData.d.ts +3 -0
- package/dist/core/generateReport/reportData.js +55 -18
- package/dist/core/judge/confidence.d.ts +119 -0
- package/dist/core/judge/confidence.js +295 -0
- package/dist/core/judge/goal-heuristics.d.ts +32 -0
- package/dist/core/judge/goal-heuristics.js +128 -0
- package/dist/core/judge/pngCodec.d.ts +23 -0
- package/dist/core/judge/pngCodec.js +215 -0
- package/dist/core/judge/screenshotDiff.d.ts +112 -0
- package/dist/core/judge/screenshotDiff.js +420 -0
- package/dist/core/page-object/definePage.d.ts +95 -0
- package/dist/core/page-object/definePage.js +62 -0
- package/dist/core/page-object/elementResolver.d.ts +32 -0
- package/dist/core/page-object/elementResolver.js +236 -0
- package/dist/core/page-object/engine-types.d.ts +140 -0
- package/dist/core/page-object/engine-types.js +34 -0
- package/dist/core/sso-cookie.d.ts +18 -0
- package/dist/core/sso-cookie.js +10 -0
- package/dist/dsl/index.d.ts +15 -0
- package/dist/dsl/index.js +29 -0
- package/dist/dsl/locatorSource.d.ts +25 -0
- package/dist/dsl/locatorSource.js +323 -0
- package/dist/dsl/ops.d.ts +46 -0
- package/dist/dsl/ops.js +95 -0
- package/dist/dsl/toDecorator.d.ts +98 -0
- package/dist/dsl/toDecorator.js +747 -0
- package/dist/engine-cypress/adapter.d.ts +108 -0
- package/dist/engine-cypress/adapter.js +514 -0
- package/dist/engine-cypress/agent-page.d.ts +159 -0
- package/dist/engine-cypress/agent-page.js +566 -0
- package/dist/engine-cypress/agentDom.d.ts +22 -0
- package/dist/engine-cypress/agentDom.js +590 -0
- package/dist/engine-cypress/bridge.d.ts +39 -0
- package/dist/engine-cypress/bridge.js +598 -0
- package/dist/engine-cypress/cdp.d.ts +78 -0
- package/dist/engine-cypress/cdp.js +339 -0
- package/dist/engine-cypress/host.d.ts +22 -0
- package/dist/engine-cypress/host.js +480 -0
- package/dist/engine-cypress/index.d.ts +26 -0
- package/dist/engine-cypress/index.js +58 -0
- package/dist/engine-cypress/keveGoal.d.ts +43 -0
- package/dist/engine-cypress/keveGoal.js +265 -0
- package/dist/engine-cypress/pageOps.d.ts +41 -0
- package/dist/engine-cypress/pageOps.js +439 -0
- package/dist/engine-cypress/reporter.d.ts +20 -0
- package/dist/engine-cypress/reporter.js +128 -0
- package/dist/engine-cypress/setup.d.ts +21 -0
- package/dist/engine-cypress/setup.js +27 -0
- package/dist/engine-cypress/shotBuffer.d.ts +40 -0
- package/dist/engine-cypress/shotBuffer.js +41 -0
- package/dist/engine-cypress/state.d.ts +56 -0
- package/dist/engine-cypress/state.js +81 -0
- package/dist/engine-playwright/engineAdapter.d.ts +78 -0
- package/dist/engine-playwright/engineAdapter.js +371 -0
- package/dist/engine-playwright/global-setup.d.ts +28 -0
- package/dist/engine-playwright/global-setup.js +142 -0
- package/dist/{core → engine-playwright}/keve-report.d.ts +0 -1
- package/dist/engine-playwright/keve-report.js +137 -0
- package/dist/{core → engine-playwright}/keve-test.d.ts +10 -8
- package/dist/{core → engine-playwright}/keve-test.js +257 -207
- package/dist/{core → engine-playwright}/pw-config.js +8 -7
- package/dist/index.d.ts +13 -8
- package/dist/index.js +43 -28
- package/dist/page-agent/agent.d.ts +98 -7
- package/dist/page-agent/agent.js +338 -84
- package/dist/page-agent/diagnostics.d.ts +184 -0
- package/dist/page-agent/diagnostics.js +377 -0
- package/dist/page-agent/hooks.d.ts +3 -2
- package/dist/page-agent/hooks.js +4 -3
- package/dist/page-agent/page-like.d.ts +116 -0
- package/dist/page-agent/page-like.js +14 -0
- package/dist/page-agent/playwright-page.d.ts +20 -0
- package/dist/page-agent/playwright-page.js +98 -0
- package/dist/page-agent/system-prompt.d.ts +7 -0
- package/dist/page-agent/system-prompt.js +10 -0
- package/dist/page-agent/system_prompt.md +1 -1
- package/dist/page-agent/tools.d.ts +1 -1
- package/dist/page-agent/tools.js +441 -9
- package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +11 -0
- package/package.json +24 -11
- package/scripts/gen-system-prompt.mjs +36 -0
- package/scripts/run-dsl-task.mjs +619 -0
- package/scripts/runtime/cypress-preprocessor.mjs +44 -0
- package/scripts/runtime/cypress.config.mjs +35 -0
- package/scripts/runtime/playwright.config.mjs +41 -0
- package/scripts/verify-shot-diff.mjs +243 -0
- package/dist/commands/auth-update.d.ts +0 -16
- package/dist/commands/auth-update.js +0 -88
- package/dist/core/auth.d.ts +0 -17
- package/dist/core/auth.js +0 -224
- package/dist/core/cdp-resolver.d.ts +0 -5
- package/dist/core/cdp-resolver.js +0 -93
- package/dist/core/global-setup.d.ts +0 -6
- package/dist/core/global-setup.js +0 -113
- package/dist/core/keve-decorators.d.ts +0 -51
- package/dist/core/keve-decorators.js +0 -95
- package/dist/core/keve-report.js +0 -376
- /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
- /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
- /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* locatorSource — 浏览器侧元素定位(hop 链)源码生成
|
|
4
|
+
*
|
|
5
|
+
* Cypress 侧没有 Playwright Locator,元素定位必须在 AUT 的 DOM 里自己实现。
|
|
6
|
+
* 本模块不直接操作 DOM,只**生成一段可在页面上下文求值的自包含源码字符串**,
|
|
7
|
+
* 由 engine-cypress 通过 CDP `Runtime.evaluate` 注入一次,之后复用。
|
|
8
|
+
*
|
|
9
|
+
* 之所以生成源码而不是导出普通函数:注入 isolate world 的函数无法闭包引用
|
|
10
|
+
* bundle 里的变量,只能整段作为源码传过去。因此生成物必须零外部引用。
|
|
11
|
+
*
|
|
12
|
+
* 语义严格对齐 elementResolver.ts::applyHops —— 同一套 hop 只有一种解释:
|
|
13
|
+
* selector/id/shadow-dom → CSS
|
|
14
|
+
* class → .a.b(空格分隔多 class)
|
|
15
|
+
* includes/text → 文本包含,取「包含该文本的最深(最小)元素」
|
|
16
|
+
* label → 可见 label 关联的表单控件
|
|
17
|
+
* iframe-url → 同源 iframe 内继续;跨源标记 cross-origin-iframe
|
|
18
|
+
* javascript/source/'' → 跳过
|
|
19
|
+
* hop.index → 1-based;-1/0/非数字表示不取序号
|
|
20
|
+
*
|
|
21
|
+
* 关键差异(相对 Playwright):Playwright 的 locator 链是「集合 → 集合」,
|
|
22
|
+
* 中间不收敛到单元素;只有末端 .first()/.nth(i) 才收敛。这里同样保持
|
|
23
|
+
* 「集合传递」直到末端,保证 `selector` + `includes` 这类链的结果集正确。
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.locatorSource = locatorSource;
|
|
27
|
+
/** 生成页内定位器源码(幂等:重复注入只覆盖同名函数,无副作用) */
|
|
28
|
+
function locatorSource() {
|
|
29
|
+
return `(() => {
|
|
30
|
+
"use strict";
|
|
31
|
+
if (window.__keveLocatorSource === "v1") return "cached";
|
|
32
|
+
|
|
33
|
+
function classToCss(v) {
|
|
34
|
+
return String(v).trim().split(/\\s+/).filter(Boolean).map(function (c) {
|
|
35
|
+
return "." + c.replace(/^\\.+/, "");
|
|
36
|
+
}).join("");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hopToCss(type, value) {
|
|
40
|
+
if (type === "selector") return String(value);
|
|
41
|
+
if (type === "id") return "#" + String(value).replace(/^#/, "");
|
|
42
|
+
if (type === "shadow-dom") return /^[.#[]/.test(String(value))
|
|
43
|
+
? String(value) : classToCss(value);
|
|
44
|
+
return classToCss(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function queryAll(root, css) {
|
|
48
|
+
if (!root || !css) return [];
|
|
49
|
+
try { return Array.prototype.slice.call(root.querySelectorAll(css)); }
|
|
50
|
+
catch (e) { return []; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 递归收集 open shadow root 内的匹配(对齐 Playwright css 自动穿透 open shadow) */
|
|
54
|
+
function queryAllDeep(root, css) {
|
|
55
|
+
var out = queryAll(root, css);
|
|
56
|
+
var hosts = queryAll(root, "*");
|
|
57
|
+
for (var i = 0; i < hosts.length; i += 1) {
|
|
58
|
+
if (hosts[i].shadowRoot) {
|
|
59
|
+
var inner = queryAllDeep(hosts[i].shadowRoot, css);
|
|
60
|
+
for (var j = 0; j < inner.length; j += 1) out.push(inner[j]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function depthOf(el) {
|
|
67
|
+
var d = 0, cur = el;
|
|
68
|
+
while (cur && cur.parentElement) { d += 1; cur = cur.parentElement; }
|
|
69
|
+
return d;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** label 文本 → 关联控件:label[for] / 控件嵌套于 label / aria-label */
|
|
73
|
+
function byLabel(root, text) {
|
|
74
|
+
var needle = String(text).trim();
|
|
75
|
+
var labels = queryAllDeep(root, "label");
|
|
76
|
+
for (var i = 0; i < labels.length; i += 1) {
|
|
77
|
+
var lab = labels[i];
|
|
78
|
+
if (String(lab.textContent || "").indexOf(needle) < 0) continue;
|
|
79
|
+
var forId = lab.getAttribute("for");
|
|
80
|
+
if (forId) {
|
|
81
|
+
var byId = root.getElementById ? root.getElementById(forId) : null;
|
|
82
|
+
if (!byId) {
|
|
83
|
+
var candidates = queryAllDeep(root, "[id]");
|
|
84
|
+
for (var c = 0; c < candidates.length; c += 1) {
|
|
85
|
+
if (candidates[c].id === forId) { byId = candidates[c]; break; }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (byId) return byId;
|
|
89
|
+
}
|
|
90
|
+
var nested = queryAll(lab, "input,textarea,select,button,[role]")[0];
|
|
91
|
+
if (nested) return nested;
|
|
92
|
+
}
|
|
93
|
+
var aria = queryAllDeep(root, "[aria-label]");
|
|
94
|
+
for (var a = 0; a < aria.length; a += 1) {
|
|
95
|
+
if (String(aria[a].getAttribute("aria-label") || "").indexOf(needle) >= 0) return aria[a];
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 一跳 → 新集合。返回 { els, status };els 为空表示该跳落空。
|
|
102
|
+
* index 收敛在这里做(与 Playwright .nth(index-1) 等价)。
|
|
103
|
+
*/
|
|
104
|
+
/** 在 roots 范围内按 role(+name) 找元素(语义对齐 Playwright getByRole 的子集) */
|
|
105
|
+
function resolveByRoleIn(roots, role, name) {
|
|
106
|
+
var pool = [];
|
|
107
|
+
for (var r = 0; r < roots.length; r += 1) {
|
|
108
|
+
pool = pool.concat(queryAllDeep(roots[r], "[role],button,a,input,select,textarea,img"));
|
|
109
|
+
}
|
|
110
|
+
var want = String(role).toLowerCase();
|
|
111
|
+
return pool.filter(function (el) {
|
|
112
|
+
var actual = String(el.getAttribute("role") || implicitRole(el)).toLowerCase();
|
|
113
|
+
if (actual !== want) return false;
|
|
114
|
+
if (name == null || name === "") return true;
|
|
115
|
+
return accessibleName(el).indexOf(String(name)) >= 0;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 子定位在 scope 的后代里求值(has / notHas 复合限定用) */
|
|
120
|
+
function resolveWithin(scope, ref) {
|
|
121
|
+
var hops = hopsOf(ref);
|
|
122
|
+
if (!hops.length) return [];
|
|
123
|
+
return resolveChain(hops, [scope]).els;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function applyHop(scopeEls, hop) {
|
|
127
|
+
var type = String(hop.type || "");
|
|
128
|
+
var value = hop.value;
|
|
129
|
+
var idx = Number(hop.index);
|
|
130
|
+
var useIndex = isFinite(idx) && idx > 0;
|
|
131
|
+
var picked = [];
|
|
132
|
+
|
|
133
|
+
for (var s = 0; s < scopeEls.length; s += 1) {
|
|
134
|
+
var scope = scopeEls[s];
|
|
135
|
+
if (type === "includes" || type === "text") {
|
|
136
|
+
var needle = String(value).trim();
|
|
137
|
+
if (!needle) continue;
|
|
138
|
+
var all = queryAllDeep(scope, "*");
|
|
139
|
+
var hits = [];
|
|
140
|
+
for (var i = 0; i < all.length; i += 1) {
|
|
141
|
+
if (String(all[i].textContent || "").indexOf(needle) >= 0) hits.push(all[i]);
|
|
142
|
+
}
|
|
143
|
+
if (!hits.length) continue;
|
|
144
|
+
// 「最小元素」= 命中里最深的一个(与 Playwright text= 命中叶子节点一致)
|
|
145
|
+
var best = hits[0], bestDepth = depthOf(best);
|
|
146
|
+
for (var h = 1; h < hits.length; h += 1) {
|
|
147
|
+
var d = depthOf(hits[h]);
|
|
148
|
+
if (d > bestDepth) { bestDepth = d; best = hits[h]; }
|
|
149
|
+
}
|
|
150
|
+
picked.push(best);
|
|
151
|
+
} else if (type === "label") {
|
|
152
|
+
var ctl = byLabel(scope, value);
|
|
153
|
+
if (!ctl) continue;
|
|
154
|
+
picked.push(ctl);
|
|
155
|
+
} else if (type === "role") {
|
|
156
|
+
var spec = value && typeof value === "object" ? value : { role: value };
|
|
157
|
+
picked = picked.concat(resolveByRoleIn([scope], spec.role, spec.name));
|
|
158
|
+
} else if (type === "data-ref") {
|
|
159
|
+
var refHits = queryAllDeep(scope, "[data-ref]").filter(function (el) {
|
|
160
|
+
return el.getAttribute("data-ref") === String(value);
|
|
161
|
+
});
|
|
162
|
+
picked = picked.concat(refHits);
|
|
163
|
+
} else if (type === "has-text") {
|
|
164
|
+
if (String(scope.textContent || "").indexOf(String(value)) >= 0) picked.push(scope);
|
|
165
|
+
} else if (type === "has-sub") {
|
|
166
|
+
if (resolveWithin(scope, value).length) picked.push(scope);
|
|
167
|
+
} else if (type === "not-has-sub") {
|
|
168
|
+
if (!resolveWithin(scope, value).length) picked.push(scope);
|
|
169
|
+
} else {
|
|
170
|
+
picked = picked.concat(queryAllDeep(scope, hopToCss(type, value)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!picked.length) return { els: [], status: "no-match" };
|
|
175
|
+
if (useIndex) {
|
|
176
|
+
var one = picked[idx - 1];
|
|
177
|
+
return one ? { els: [one], status: "ok" } : { els: [], status: "index-out-of-range" };
|
|
178
|
+
}
|
|
179
|
+
// 去重(多 scope 可能命中同一元素)
|
|
180
|
+
var seen = [];
|
|
181
|
+
for (var p = 0; p < picked.length; p += 1) {
|
|
182
|
+
if (seen.indexOf(picked[p]) < 0) seen.push(picked[p]);
|
|
183
|
+
}
|
|
184
|
+
return { els: seen, status: status };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 定位链求值。roots 为起始文档/元素集合。
|
|
189
|
+
* 返回 { els, status }:els 是「末端匹配集合」(未收敛),
|
|
190
|
+
* 调用方按需取 .nth / .first。
|
|
191
|
+
*/
|
|
192
|
+
function resolveChain(hops, roots) {
|
|
193
|
+
var scopeEls = roots && roots.length ? roots : [document];
|
|
194
|
+
var list = Array.isArray(hops) ? hops : [];
|
|
195
|
+
var sawIframe = false;
|
|
196
|
+
|
|
197
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
198
|
+
var hop = list[i] || {};
|
|
199
|
+
var type = String(hop.type || "");
|
|
200
|
+
if (type === "javascript" || type === "source" || type === "") continue;
|
|
201
|
+
|
|
202
|
+
if (type === "iframe-url") {
|
|
203
|
+
var frames = [];
|
|
204
|
+
for (var s = 0; s < scopeEls.length; s += 1) {
|
|
205
|
+
frames = frames.concat(queryAllDeep(scopeEls[s], "iframe"));
|
|
206
|
+
}
|
|
207
|
+
var match = null;
|
|
208
|
+
for (var f = 0; f < frames.length; f += 1) {
|
|
209
|
+
if (String(frames[f].getAttribute("src") || "").indexOf(String(hop.value)) >= 0) { match = frames[f]; break; }
|
|
210
|
+
}
|
|
211
|
+
if (!match) return { els: [], status: "no-iframe" };
|
|
212
|
+
var doc = null;
|
|
213
|
+
try { doc = match.contentDocument; } catch (e) { doc = null; }
|
|
214
|
+
if (!doc) return { els: [], status: "cross-origin-iframe" };
|
|
215
|
+
scopeEls = [doc];
|
|
216
|
+
sawIframe = true;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var res = applyHop(scopeEls, hop);
|
|
221
|
+
if (!res.els.length) return res;
|
|
222
|
+
scopeEls = res.els;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// iframe 进入后无后续定位跳:兜底取 frame 的 body(与 Playwright 实现一致)
|
|
226
|
+
if (sawIframe && scopeEls.length === 1 && scopeEls[0].nodeType === 9) {
|
|
227
|
+
scopeEls = queryAll(scopeEls[0], "body");
|
|
228
|
+
}
|
|
229
|
+
return { els: scopeEls, status: "ok" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* ElementRef → hop 序列(与 elementResolver.resolveElement 同构)
|
|
234
|
+
* 基础定位:chain 优先 → css → role(+name) → ref
|
|
235
|
+
* 复合限定:hasText / has / notHas 转成追加 hop,在前一步的结果集上过滤
|
|
236
|
+
*/
|
|
237
|
+
function hopsOf(ref) {
|
|
238
|
+
if (!ref || typeof ref !== "object") return [];
|
|
239
|
+
var hops = [];
|
|
240
|
+
if (ref.chain && ref.chain.length) {
|
|
241
|
+
hops = hops.concat(ref.chain);
|
|
242
|
+
} else if (ref.css) {
|
|
243
|
+
hops.push({ type: "selector", value: ref.css });
|
|
244
|
+
} else if (ref.role) {
|
|
245
|
+
hops.push({ type: "role", value: { role: ref.role, name: ref.name == null ? null : ref.name } });
|
|
246
|
+
} else if (ref.ref) {
|
|
247
|
+
hops.push({ type: "data-ref", value: ref.ref });
|
|
248
|
+
}
|
|
249
|
+
if (ref.hasText != null) hops.push({ type: "has-text", value: ref.hasText });
|
|
250
|
+
if (ref.has) hops.push({ type: "has-sub", value: ref.has });
|
|
251
|
+
if (ref.notHas) hops.push({ type: "not-has-sub", value: ref.notHas });
|
|
252
|
+
return hops;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 单个 ElementRef → 末端匹配集合 */
|
|
256
|
+
function resolveRef(ref) {
|
|
257
|
+
var hops = hopsOf(ref);
|
|
258
|
+
if (!hops.length) return { els: [], status: "invalid-ref" };
|
|
259
|
+
return resolveChain(hops, [document]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* ElementRef 链(engine.resolveChain(page, a, b, c) 的浏览器等价物):
|
|
264
|
+
* 第一个 ref 从 document 求值,后续 ref 在前一步的结果集范围内继续求值。
|
|
265
|
+
*/
|
|
266
|
+
function resolveRefChain(refs) {
|
|
267
|
+
if (!refs || !refs.length) return { els: [], status: "empty-ref" };
|
|
268
|
+
var res = resolveRef(refs[0]);
|
|
269
|
+
for (var i = 1; i < refs.length; i += 1) {
|
|
270
|
+
if (!res.els.length) return res;
|
|
271
|
+
var hops = hopsOf(refs[i]);
|
|
272
|
+
if (!hops.length) continue;
|
|
273
|
+
res = resolveChain(hops, res.els);
|
|
274
|
+
}
|
|
275
|
+
return res;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function implicitRole(el) {
|
|
279
|
+
var tag = String(el.tagName || "").toLowerCase();
|
|
280
|
+
if (tag === "button") return "button";
|
|
281
|
+
if (tag === "a" && el.hasAttribute("href")) return "link";
|
|
282
|
+
if (tag === "input") {
|
|
283
|
+
var t = String(el.getAttribute("type") || "text").toLowerCase();
|
|
284
|
+
if (t === "checkbox") return "checkbox";
|
|
285
|
+
if (t === "radio") return "radio";
|
|
286
|
+
if (t === "submit" || t === "button") return "button";
|
|
287
|
+
return "textbox";
|
|
288
|
+
}
|
|
289
|
+
if (tag === "select") return "combobox";
|
|
290
|
+
if (tag === "textarea") return "textbox";
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function accessibleName(el) {
|
|
295
|
+
var labelled = "";
|
|
296
|
+
var ids = String(el.getAttribute("aria-labelledby") || "").split(/\\s+/).filter(Boolean);
|
|
297
|
+
for (var i = 0; i < ids.length; i += 1) {
|
|
298
|
+
var node = document.getElementById(ids[i]);
|
|
299
|
+
if (node) labelled += " " + String(node.textContent || "");
|
|
300
|
+
}
|
|
301
|
+
return String(
|
|
302
|
+
el.getAttribute("aria-label")
|
|
303
|
+
|| labelled
|
|
304
|
+
|| (el.labels && el.labels[0] ? el.labels[0].textContent : "")
|
|
305
|
+
|| el.textContent
|
|
306
|
+
|| el.getAttribute("placeholder")
|
|
307
|
+
|| ""
|
|
308
|
+
).trim();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
window.__keveLocator = {
|
|
312
|
+
version: "v1",
|
|
313
|
+
resolveRef: resolveRef,
|
|
314
|
+
resolveRefChain: resolveRefChain,
|
|
315
|
+
resolveChain: resolveChain,
|
|
316
|
+
queryAllDeep: queryAllDeep,
|
|
317
|
+
accessibleName: accessibleName,
|
|
318
|
+
implicitRole: implicitRole,
|
|
319
|
+
};
|
|
320
|
+
window.__keveLocatorSource = "v1";
|
|
321
|
+
return "ok";
|
|
322
|
+
})()`;
|
|
323
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ops — 平台 DSL 算子 / 断言算子语义表
|
|
3
|
+
*
|
|
4
|
+
* 这张表的唯一用途是「可诊断」:生成器遇到不认识的算子时,报错信息能说清
|
|
5
|
+
* 是「平台用了新算子」还是「这个算子本来就不该出现在可执行用例里」。
|
|
6
|
+
*
|
|
7
|
+
* 约束:本模块必须保持零依赖、零副作用 —— dsl/ 会同时被打进 Playwright 与
|
|
8
|
+
* Cypress 的执行产物,不能引入 Node/框架依赖。
|
|
9
|
+
*/
|
|
10
|
+
/** 平台 stepDsl.operation 取值(来自 stepDsl 表 operation 列的真实分布) */
|
|
11
|
+
export declare const STEP_OPERATIONS: readonly ["OPEN_PAGE", "REDIRECT_TO", "REFRESH_PAGE", "CLICK_ELEMENT", "DB_CLICK_ELEMENT", "FORCE_CLICK_ELEMENT", "CHECK_TEXT", "INPUT_ELEMENT", "MOVE_ELEMENT", "SCROLL", "DRAGGING", "CLEAN", "WAIT_EVENTS", "WAIT_RESPONSE", "WRITE_GLOBAL", "SCREEN_SHOT", "SCREENSHOT", "TAKE_SCREENSHOT"];
|
|
12
|
+
export type StepOperation = (typeof STEP_OPERATIONS)[number];
|
|
13
|
+
/** 是否为已知步骤算子(未知算子一律确定性失败,不静默跳过) */
|
|
14
|
+
export declare function isKnownOperation(op: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 断言算子(stepDsl.expectation[].type)。
|
|
17
|
+
*
|
|
18
|
+
* 语义分成两类,这个区分很关键:
|
|
19
|
+
* - 可执行断言:必须渲染成 engine 断言,失败即步骤失败
|
|
20
|
+
* - 纯文案载体:只有 text 没有可执行语义,仅喂给 keveGoal 的 expected,
|
|
21
|
+
* 不产生任何断言(渲染成注释级信息)
|
|
22
|
+
*/
|
|
23
|
+
/** 可执行断言算子 */
|
|
24
|
+
export declare const ASSERT_OPERATIONS: readonly ["ELEMENT_EXIST", "ELEMENT_NOT_EXIST", "ELEMENT_VISIBLE", "ELEMENT_NOT_VISIBLE", "TEXT_EXIST", "TEXT_NOT_EXIST", "TEXT_EQUAL", "EXPECTED_VALUE", "EXPECTED__PAGET_VALUE", "SCREEN_SHOT_COMPARE", "SCREENSHOT_COMPARE", "IMAGE_COMPARE"];
|
|
25
|
+
/**
|
|
26
|
+
* 纯文案载体算子:不渲染断言。
|
|
27
|
+
*
|
|
28
|
+
* `TEXT` 是平台里占比最高的 expectation 类型(500+ 条),它只承载一句人话
|
|
29
|
+
* (「列表提示无匹配」),没有任何可执行的定位/期望值。把它当未知算子抛错会
|
|
30
|
+
* 让大批存量用例在生成期就失败 —— 它的语义本来就已经由 keveGoal 的 expected
|
|
31
|
+
* 承载了。
|
|
32
|
+
*/
|
|
33
|
+
export declare const TEXT_ONLY_ASSERT_OPERATIONS: readonly ["TEXT"];
|
|
34
|
+
export declare function isKnownAssert(type: string): boolean;
|
|
35
|
+
export declare function isTextOnlyAssert(type: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* EXPECTED_VALUE 的受支持比较维度。
|
|
38
|
+
* ctype 是「比什么」,cexpression 是「怎么比」(chai 风格,历史字段)。
|
|
39
|
+
*/
|
|
40
|
+
export declare const EXPECTED_VALUE_CTYPES: readonly ["text", "class", "placeholder", "style", "index"];
|
|
41
|
+
export declare const COMPARISON_EXPRESSIONS: readonly ["include", "eq", "not.include", "not.eq"];
|
|
42
|
+
/** 历史字段 cexpression → 语义方向 */
|
|
43
|
+
export declare function parseComparison(cexpression: string): {
|
|
44
|
+
mode: 'include' | 'eq';
|
|
45
|
+
negated: boolean;
|
|
46
|
+
};
|
package/dist/dsl/ops.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ops — 平台 DSL 算子 / 断言算子语义表
|
|
4
|
+
*
|
|
5
|
+
* 这张表的唯一用途是「可诊断」:生成器遇到不认识的算子时,报错信息能说清
|
|
6
|
+
* 是「平台用了新算子」还是「这个算子本来就不该出现在可执行用例里」。
|
|
7
|
+
*
|
|
8
|
+
* 约束:本模块必须保持零依赖、零副作用 —— dsl/ 会同时被打进 Playwright 与
|
|
9
|
+
* Cypress 的执行产物,不能引入 Node/框架依赖。
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.COMPARISON_EXPRESSIONS = exports.EXPECTED_VALUE_CTYPES = exports.TEXT_ONLY_ASSERT_OPERATIONS = exports.ASSERT_OPERATIONS = exports.STEP_OPERATIONS = void 0;
|
|
13
|
+
exports.isKnownOperation = isKnownOperation;
|
|
14
|
+
exports.isKnownAssert = isKnownAssert;
|
|
15
|
+
exports.isTextOnlyAssert = isTextOnlyAssert;
|
|
16
|
+
exports.parseComparison = parseComparison;
|
|
17
|
+
/** 平台 stepDsl.operation 取值(来自 stepDsl 表 operation 列的真实分布) */
|
|
18
|
+
exports.STEP_OPERATIONS = [
|
|
19
|
+
'OPEN_PAGE',
|
|
20
|
+
'REDIRECT_TO',
|
|
21
|
+
'REFRESH_PAGE',
|
|
22
|
+
'CLICK_ELEMENT',
|
|
23
|
+
'DB_CLICK_ELEMENT',
|
|
24
|
+
'FORCE_CLICK_ELEMENT',
|
|
25
|
+
'CHECK_TEXT',
|
|
26
|
+
'INPUT_ELEMENT',
|
|
27
|
+
'MOVE_ELEMENT',
|
|
28
|
+
'SCROLL',
|
|
29
|
+
'DRAGGING',
|
|
30
|
+
'CLEAN',
|
|
31
|
+
'WAIT_EVENTS',
|
|
32
|
+
'WAIT_RESPONSE',
|
|
33
|
+
'WRITE_GLOBAL',
|
|
34
|
+
'SCREEN_SHOT',
|
|
35
|
+
'SCREENSHOT',
|
|
36
|
+
'TAKE_SCREENSHOT',
|
|
37
|
+
];
|
|
38
|
+
const STEP_OPERATION_SET = new Set(exports.STEP_OPERATIONS);
|
|
39
|
+
/** 是否为已知步骤算子(未知算子一律确定性失败,不静默跳过) */
|
|
40
|
+
function isKnownOperation(op) {
|
|
41
|
+
return STEP_OPERATION_SET.has(String(op || ''));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 断言算子(stepDsl.expectation[].type)。
|
|
45
|
+
*
|
|
46
|
+
* 语义分成两类,这个区分很关键:
|
|
47
|
+
* - 可执行断言:必须渲染成 engine 断言,失败即步骤失败
|
|
48
|
+
* - 纯文案载体:只有 text 没有可执行语义,仅喂给 keveGoal 的 expected,
|
|
49
|
+
* 不产生任何断言(渲染成注释级信息)
|
|
50
|
+
*/
|
|
51
|
+
/** 可执行断言算子 */
|
|
52
|
+
exports.ASSERT_OPERATIONS = [
|
|
53
|
+
'ELEMENT_EXIST',
|
|
54
|
+
'ELEMENT_NOT_EXIST',
|
|
55
|
+
'ELEMENT_VISIBLE',
|
|
56
|
+
'ELEMENT_NOT_VISIBLE',
|
|
57
|
+
'TEXT_EXIST',
|
|
58
|
+
'TEXT_NOT_EXIST',
|
|
59
|
+
'TEXT_EQUAL',
|
|
60
|
+
'EXPECTED_VALUE',
|
|
61
|
+
'EXPECTED__PAGET_VALUE',
|
|
62
|
+
'SCREEN_SHOT_COMPARE',
|
|
63
|
+
'SCREENSHOT_COMPARE',
|
|
64
|
+
'IMAGE_COMPARE',
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* 纯文案载体算子:不渲染断言。
|
|
68
|
+
*
|
|
69
|
+
* `TEXT` 是平台里占比最高的 expectation 类型(500+ 条),它只承载一句人话
|
|
70
|
+
* (「列表提示无匹配」),没有任何可执行的定位/期望值。把它当未知算子抛错会
|
|
71
|
+
* 让大批存量用例在生成期就失败 —— 它的语义本来就已经由 keveGoal 的 expected
|
|
72
|
+
* 承载了。
|
|
73
|
+
*/
|
|
74
|
+
exports.TEXT_ONLY_ASSERT_OPERATIONS = ['TEXT'];
|
|
75
|
+
const ASSERT_OPERATION_SET = new Set(exports.ASSERT_OPERATIONS);
|
|
76
|
+
const TEXT_ONLY_SET = new Set(exports.TEXT_ONLY_ASSERT_OPERATIONS);
|
|
77
|
+
function isKnownAssert(type) {
|
|
78
|
+
return ASSERT_OPERATION_SET.has(String(type || ''));
|
|
79
|
+
}
|
|
80
|
+
function isTextOnlyAssert(type) {
|
|
81
|
+
return TEXT_ONLY_SET.has(String(type || ''));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* EXPECTED_VALUE 的受支持比较维度。
|
|
85
|
+
* ctype 是「比什么」,cexpression 是「怎么比」(chai 风格,历史字段)。
|
|
86
|
+
*/
|
|
87
|
+
exports.EXPECTED_VALUE_CTYPES = ['text', 'class', 'placeholder', 'style', 'index'];
|
|
88
|
+
exports.COMPARISON_EXPRESSIONS = ['include', 'eq', 'not.include', 'not.eq'];
|
|
89
|
+
/** 历史字段 cexpression → 语义方向 */
|
|
90
|
+
function parseComparison(cexpression) {
|
|
91
|
+
const raw = String(cexpression || 'include').trim().toLowerCase();
|
|
92
|
+
const negated = raw.startsWith('not.');
|
|
93
|
+
const mode = raw.replace(/^not\./, '') === 'eq' ? 'eq' : 'include';
|
|
94
|
+
return { mode, negated };
|
|
95
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL → @kkeve/suite 装饰器脚本(纯投影,零 DB / 零框架依赖)
|
|
3
|
+
*
|
|
4
|
+
* 输入:**已展开、已解析**的 stepDsl 清单(内嵌场景已递归展开、source 元素库
|
|
5
|
+
* 引用已解析成 locator 跳、变量已可查表)。输出:可被 Playwright 与 Cypress
|
|
6
|
+
* 两个引擎同时执行的装饰器脚本源码。
|
|
7
|
+
*
|
|
8
|
+
* 为什么生成器放在 suite 包而不是调用方:
|
|
9
|
+
* 1. 同一份 DSL 必须只投影出一种脚本 —— 两端报告一致的前提是执行体一致
|
|
10
|
+
* 2. 探索期由 LLM 生成的 @kkeve/suite 脚本,后续要能回填成 DSL;正反变换
|
|
11
|
+
* 放在同一个包里才闭得上环,否则两边算子语义会各自漂移
|
|
12
|
+
*
|
|
13
|
+
* 分层约定:本模块**不做任何 IO**。DB 装载、内嵌展开、元素库解析、变量表
|
|
14
|
+
* 查询全部留在调用方的 service 层(见 keve 侧 dslLoader)。这样本模块可以被
|
|
15
|
+
* Cypress 浏览器 bundle 直接引用。
|
|
16
|
+
*
|
|
17
|
+
* 关于「不支持就报错」:未知算子渲染成确定性 throw,而不是静默跳过 —— 静默
|
|
18
|
+
* 跳过会让报告出现「验证通过」的假绿。但**已知的纯文案载体**(TEXT 断言、
|
|
19
|
+
* 无 type 的 expectation)不是未知算子,它们只喂 expected 文案,不产生断言。
|
|
20
|
+
*/
|
|
21
|
+
/** 平台定位跳(context / expectation.context 的元素) */
|
|
22
|
+
export interface DslHop {
|
|
23
|
+
type?: string;
|
|
24
|
+
value?: any;
|
|
25
|
+
/** 历史字段:部分步骤把定位写在 selector 上 */
|
|
26
|
+
selector?: string;
|
|
27
|
+
/** 1-based;-1 / undefined 表示不取第几个 */
|
|
28
|
+
index?: number;
|
|
29
|
+
}
|
|
30
|
+
/** 平台 DSL 值:字面量或 { type, value } 包装 */
|
|
31
|
+
export type DslValue = string | number | null | undefined | {
|
|
32
|
+
type?: string;
|
|
33
|
+
value?: any;
|
|
34
|
+
variable?: Array<{
|
|
35
|
+
key: string;
|
|
36
|
+
value: any;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
/** 一条断言 / 预期 */
|
|
40
|
+
export interface DslExpectation {
|
|
41
|
+
text?: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
/** 比较维度:text | class | placeholder | style | index | url | title */
|
|
44
|
+
ctype?: string;
|
|
45
|
+
/** 比较方式:include | eq | not.include(chai 风格历史字段) */
|
|
46
|
+
cexpression?: string;
|
|
47
|
+
value?: DslValue;
|
|
48
|
+
context?: DslHop[];
|
|
49
|
+
baseImgUrl?: string;
|
|
50
|
+
}
|
|
51
|
+
/** 一条步骤 */
|
|
52
|
+
export interface DslStep {
|
|
53
|
+
id?: number;
|
|
54
|
+
operation?: string | null;
|
|
55
|
+
text?: string;
|
|
56
|
+
value?: DslValue;
|
|
57
|
+
context?: DslHop[];
|
|
58
|
+
wait?: Array<{
|
|
59
|
+
waitType?: string;
|
|
60
|
+
value?: any;
|
|
61
|
+
}>;
|
|
62
|
+
expectation?: DslExpectation[];
|
|
63
|
+
/** 拖拽目标 / 嵌套上下文 */
|
|
64
|
+
child?: any;
|
|
65
|
+
urlPart?: string;
|
|
66
|
+
}
|
|
67
|
+
/** 变量表行(按环境列取值) */
|
|
68
|
+
export interface DslVariableRow {
|
|
69
|
+
id?: number;
|
|
70
|
+
name?: string;
|
|
71
|
+
variable?: Array<{
|
|
72
|
+
key: string;
|
|
73
|
+
value: any;
|
|
74
|
+
}>;
|
|
75
|
+
[env: string]: any;
|
|
76
|
+
}
|
|
77
|
+
export interface ToDecoratorOptions {
|
|
78
|
+
/** 平台 stepGroup id(用于生成稳定类名 / 场景 id) */
|
|
79
|
+
stepGroupId: number;
|
|
80
|
+
caseName: string;
|
|
81
|
+
/** 已展开的 DSL 步骤清单 */
|
|
82
|
+
dslList: DslStep[];
|
|
83
|
+
/** 变量取值环境列名(test / rc / pre / online) */
|
|
84
|
+
env: string;
|
|
85
|
+
/** 变量表(按 id 或 name 引用) */
|
|
86
|
+
variableList?: DslVariableRow[];
|
|
87
|
+
/** 账号占位插值:${loginName} / ${pwd} */
|
|
88
|
+
account?: {
|
|
89
|
+
name: string;
|
|
90
|
+
password: string;
|
|
91
|
+
};
|
|
92
|
+
/** 是否已注入 SSO cookie(仅写进注释,供执行现场排查) */
|
|
93
|
+
ssoInject?: boolean;
|
|
94
|
+
/** import 来源,默认 @kkeve/suite/keve-test */
|
|
95
|
+
importFrom?: string;
|
|
96
|
+
}
|
|
97
|
+
/** 生成装饰器脚本源码(UTF-8 文本) */
|
|
98
|
+
export declare function toDecorator(options: ToDecoratorOptions): string;
|