@kkeve/suite 0.0.21 → 0.0.23
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/core/page-object/elementResolver.js +20 -4
- package/dist/core/page-object/engine-types.d.ts +3 -0
- package/dist/dsl/locatorSource.d.ts +1 -1
- package/dist/dsl/locatorSource.js +72 -20
- package/dist/dsl/toDecorator.d.ts +2 -0
- package/dist/dsl/toDecorator.js +318 -69
- package/dist/engine-cypress/adapter.d.ts +8 -0
- package/dist/engine-cypress/adapter.js +23 -3
- package/dist/engine-cypress/agent-page.js +1 -1
- package/dist/engine-cypress/agentDom.js +86 -6
- package/dist/engine-cypress/cdp.js +1 -1
- package/dist/engine-cypress/host.js +40 -4
- package/dist/engine-cypress/keveGoal.d.ts +9 -0
- package/dist/engine-cypress/keveGoal.js +20 -0
- package/dist/engine-cypress/pageOps.js +79 -3
- package/dist/engine-playwright/engineAdapter.d.ts +7 -0
- package/dist/engine-playwright/engineAdapter.js +10 -0
- package/dist/engine-playwright/keve-test.d.ts +8 -0
- package/dist/engine-playwright/keve-test.js +23 -0
- package/dist/engine-playwright/pw-config.js +2 -0
- package/dist/runner/index.d.ts +85 -0
- package/dist/runner/index.js +220 -0
- package/package.json +5 -1
- package/scripts/run-dsl-task.mjs +91 -171
- package/scripts/run-probe.mjs +35 -0
- package/scripts/runtime/cypress.config.mjs +2 -0
- package/scripts/runtime/playwright.config.mjs +1 -0
|
@@ -31,7 +31,8 @@ function hopToCss(t, v) {
|
|
|
31
31
|
* - selector → locator(css)
|
|
32
32
|
* - class → locator('.a.b')
|
|
33
33
|
* - includes/text → locator('text=...')(在当前范围内找包含该文本的元素)
|
|
34
|
-
* - label →
|
|
34
|
+
* - label → 平台历史 DSL 的 label 是 HTML 标签选择器(tbody/tr/div 等),
|
|
35
|
+
* 现代表单「按可见 label 找控件」语义只在取值看起来不是标签名时兜底
|
|
35
36
|
* - iframe-url → frameLocator(进入 iframe 上下文,src 部分匹配)
|
|
36
37
|
* - javascript/source → 跳过(javascript 仅 WRITE_GLOBAL 场景由 engine.evaluate 处理)
|
|
37
38
|
*
|
|
@@ -68,10 +69,25 @@ function applyHops(page, scope, hops) {
|
|
|
68
69
|
cur = cur.locator('text=' + text.replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
|
|
69
70
|
}
|
|
70
71
|
else if (t === 'label') {
|
|
71
|
-
//
|
|
72
|
-
//
|
|
72
|
+
// 平台历史 DSL 的 label 是 HTML 标签选择器:tbody → locator('tbody')。
|
|
73
|
+
// 若值不是标签名(如“用户名/确定”),保留按可见 label 找控件语义,避免
|
|
74
|
+
// 历史或新 DSL 里依赖 label 文本的定位直接失效。
|
|
73
75
|
const label = String(v).trim();
|
|
74
|
-
|
|
76
|
+
const tagRe = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
77
|
+
const knownTag = tagRe.test(label) && [
|
|
78
|
+
'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo',
|
|
79
|
+
'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col',
|
|
80
|
+
'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl',
|
|
81
|
+
'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1',
|
|
82
|
+
'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe',
|
|
83
|
+
'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark',
|
|
84
|
+
'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
|
85
|
+
'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp',
|
|
86
|
+
'script', 'section', 'select', 'slot', 'small', 'source', 'span', 'strong', 'style',
|
|
87
|
+
'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th',
|
|
88
|
+
'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr',
|
|
89
|
+
].includes(label.toLowerCase());
|
|
90
|
+
cur = knownTag ? cur.locator(label) : cur.getByLabel(label, { exact: false });
|
|
75
91
|
}
|
|
76
92
|
else {
|
|
77
93
|
throw new Error(`Unknown hop type '${t}' in chain: ${JSON.stringify(hop)}`);
|
|
@@ -94,6 +94,9 @@ export interface EngineAdapter {
|
|
|
94
94
|
expectTextEquals(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
95
95
|
/** 断言:元素文本不等于(平台 EXPECTED_VALUE ctype=text cexpression=not.eq) */
|
|
96
96
|
expectTextNotEquals(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
97
|
+
/** 断言:表单元素当前值等于(input/textarea/select 的 value,而非文本内容)。
|
|
98
|
+
* 与 expectTextEquals 的区别:后者读的是元素文本,对 input 取不到用户输入的值。 */
|
|
99
|
+
expectValue(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
97
100
|
/** 断言:元素数量 */
|
|
98
101
|
expectCount(count: number, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
99
102
|
/** 断言:元素集合中「文本非空」的个数(平台 EXPECTED_VALUE ctype=index,历史语义为计数) */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* selector/id/shadow-dom → CSS
|
|
13
13
|
* class → .a.b(空格分隔多 class)
|
|
14
14
|
* includes/text → 文本包含,取「包含该文本的最深(最小)元素」
|
|
15
|
-
* label →
|
|
15
|
+
* label → HTML 标签名按标签选择器;非标签名回退「可见 label 关联控件」
|
|
16
16
|
* iframe-url → 同源 iframe 内继续;跨源标记 cross-origin-iframe
|
|
17
17
|
* javascript/source/'' → 跳过
|
|
18
18
|
* hop.index → 1-based;-1/0/非数字表示不取序号
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* selector/id/shadow-dom → CSS
|
|
14
14
|
* class → .a.b(空格分隔多 class)
|
|
15
15
|
* includes/text → 文本包含,取「包含该文本的最深(最小)元素」
|
|
16
|
-
* label →
|
|
16
|
+
* label → HTML 标签名按标签选择器;非标签名回退「可见 label 关联控件」
|
|
17
17
|
* iframe-url → 同源 iframe 内继续;跨源标记 cross-origin-iframe
|
|
18
18
|
* javascript/source/'' → 跳过
|
|
19
19
|
* hop.index → 1-based;-1/0/非数字表示不取序号
|
|
@@ -28,7 +28,7 @@ exports.locatorSource = locatorSource;
|
|
|
28
28
|
function locatorSource() {
|
|
29
29
|
return `(() => {
|
|
30
30
|
"use strict";
|
|
31
|
-
if (window.__keveLocatorSource === "
|
|
31
|
+
if (window.__keveLocatorSource === "v5") return "cached";
|
|
32
32
|
|
|
33
33
|
function classToCss(v) {
|
|
34
34
|
return String(v).trim().split(/\\s+/).filter(Boolean).map(function (c) {
|
|
@@ -69,9 +69,45 @@ function locatorSource() {
|
|
|
69
69
|
return d;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
72
|
+
/** 归一化用于文本定位的句子:统一空白与半/全角标点,并忽略成对的中文引号装饰 */
|
|
73
|
+
function normalizeText(str) {
|
|
74
|
+
return String(str == null ? "" : str)
|
|
75
|
+
.normalize("NFKC")
|
|
76
|
+
.replace(/[「」『』【】《》“”‘’]/g, "")
|
|
77
|
+
.replace(/\\s+/g, " ")
|
|
78
|
+
.trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var HTML_TAGS = {
|
|
82
|
+
"a": 1, "abbr": 1, "address": 1, "article": 1, "aside": 1, "audio": 1, "b": 1, "base": 1,
|
|
83
|
+
"bdi": 1, "bdo": 1, "blockquote": 1, "body": 1, "br": 1, "button": 1, "canvas": 1, "caption": 1,
|
|
84
|
+
"cite": 1, "code": 1, "col": 1, "colgroup": 1, "data": 1, "datalist": 1, "dd": 1, "del": 1,
|
|
85
|
+
"details": 1, "dfn": 1, "dialog": 1, "div": 1, "dl": 1, "dt": 1, "em": 1, "embed": 1,
|
|
86
|
+
"fieldset": 1, "figcaption": 1, "figure": 1, "footer": 1, "form": 1, "h1": 1, "h2": 1,
|
|
87
|
+
"h3": 1, "h4": 1, "h5": 1, "h6": 1, "head": 1, "header": 1, "hgroup": 1, "hr": 1, "html": 1,
|
|
88
|
+
"i": 1, "iframe": 1, "img": 1, "input": 1, "ins": 1, "kbd": 1, "label": 1, "legend": 1,
|
|
89
|
+
"li": 1, "link": 1, "main": 1, "map": 1, "mark": 1, "menu": 1, "meta": 1, "meter": 1,
|
|
90
|
+
"nav": 1, "noscript": 1, "object": 1, "ol": 1, "optgroup": 1, "option": 1, "output": 1,
|
|
91
|
+
"p": 1, "picture": 1, "pre": 1, "progress": 1, "q": 1, "rp": 1, "rt": 1, "ruby": 1, "s": 1,
|
|
92
|
+
"samp": 1, "script": 1, "section": 1, "select": 1, "slot": 1, "small": 1, "source": 1,
|
|
93
|
+
"span": 1, "strong": 1, "style": 1, "sub": 1, "summary": 1, "sup": 1, "table": 1, "tbody": 1,
|
|
94
|
+
"td": 1, "template": 1, "textarea": 1, "tfoot": 1, "th": 1, "thead": 1, "time": 1, "title": 1,
|
|
95
|
+
"tr": 1, "track": 1, "u": 1, "ul": 1, "var": 1, "video": 1, "wbr": 1,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** label 语义:HTML 标签名 → 标签选择器(含最近祖先回退);否则按 label 文本找关联控件 */
|
|
73
99
|
function byLabel(root, text) {
|
|
74
|
-
var
|
|
100
|
+
var raw = String(text == null ? "" : text).trim();
|
|
101
|
+
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(raw) && HTML_TAGS[raw.toLowerCase()]) {
|
|
102
|
+
var tagOut = queryAllDeep(root, raw);
|
|
103
|
+
// includes 常收敛到行内最深文本节点;
|
|
104
|
+
// 若目标是祖先标签,直接按后代查会落空,补充最近同名祖先回退。
|
|
105
|
+
var ancestor = null;
|
|
106
|
+
if (root && root.nodeType === 1 && root.closest) ancestor = root.closest(raw);
|
|
107
|
+
if (ancestor && tagOut.indexOf(ancestor) < 0) tagOut.unshift(ancestor);
|
|
108
|
+
return tagOut;
|
|
109
|
+
}
|
|
110
|
+
var needle = raw;
|
|
75
111
|
var labels = queryAllDeep(root, "label");
|
|
76
112
|
for (var i = 0; i < labels.length; i += 1) {
|
|
77
113
|
var lab = labels[i];
|
|
@@ -85,16 +121,16 @@ function locatorSource() {
|
|
|
85
121
|
if (candidates[c].id === forId) { byId = candidates[c]; break; }
|
|
86
122
|
}
|
|
87
123
|
}
|
|
88
|
-
if (byId) return byId;
|
|
124
|
+
if (byId) return [byId];
|
|
89
125
|
}
|
|
90
126
|
var nested = queryAll(lab, "input,textarea,select,button,[role]")[0];
|
|
91
|
-
if (nested) return nested;
|
|
127
|
+
if (nested) return [nested];
|
|
92
128
|
}
|
|
93
129
|
var aria = queryAllDeep(root, "[aria-label]");
|
|
94
130
|
for (var a = 0; a < aria.length; a += 1) {
|
|
95
|
-
if (String(aria[a].getAttribute("aria-label") || "").indexOf(needle) >= 0) return aria[a];
|
|
131
|
+
if (String(aria[a].getAttribute("aria-label") || "").indexOf(needle) >= 0) return [aria[a]];
|
|
96
132
|
}
|
|
97
|
-
return
|
|
133
|
+
return [];
|
|
98
134
|
}
|
|
99
135
|
|
|
100
136
|
/**
|
|
@@ -133,25 +169,41 @@ function locatorSource() {
|
|
|
133
169
|
for (var s = 0; s < scopeEls.length; s += 1) {
|
|
134
170
|
var scope = scopeEls[s];
|
|
135
171
|
if (type === "includes" || type === "text") {
|
|
136
|
-
var needle =
|
|
172
|
+
var needle = normalizeText(value);
|
|
137
173
|
if (!needle) continue;
|
|
174
|
+
// Playwright text= 会让「元素自身直挂文本」也能命中;这里除后代外把
|
|
175
|
+
// scope 本身也作为候选,避免 selector → text 链把直挂文本落空。
|
|
138
176
|
var all = queryAllDeep(scope, "*");
|
|
139
177
|
var hits = [];
|
|
178
|
+
if (normalizeText(scope.textContent).indexOf(needle) >= 0) hits.push(scope);
|
|
140
179
|
for (var i = 0; i < all.length; i += 1) {
|
|
141
|
-
if (
|
|
180
|
+
if (normalizeText(all[i].textContent).indexOf(needle) >= 0) hits.push(all[i]);
|
|
142
181
|
}
|
|
143
182
|
if (!hits.length) continue;
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
183
|
+
// 文本匹配默认取命中里最深的一个(与 Playwright text= 命中叶子节点一致),
|
|
184
|
+
// 但页面可能残留隐藏弹窗里的禁用按钮,这里优先从「可见且未禁用」的命中里选,
|
|
185
|
+
// 没有可见候选时才回退到全部命中,避免 text=确定 点到隐藏禁用按钮。
|
|
186
|
+
var candidates = hits.filter(function (el) {
|
|
187
|
+
if (el.disabled) return false;
|
|
188
|
+
try {
|
|
189
|
+
var style = window.getComputedStyle(el);
|
|
190
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false;
|
|
191
|
+
var rect = el.getBoundingClientRect();
|
|
192
|
+
if (rect.width === 0 && rect.height === 0) return false;
|
|
193
|
+
} catch (e) { /* default to enabled candidate */ }
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
if (!candidates.length) candidates = hits;
|
|
197
|
+
var best = candidates[0], bestDepth = depthOf(best);
|
|
198
|
+
for (var h = 1; h < candidates.length; h += 1) {
|
|
199
|
+
var d = depthOf(candidates[h]);
|
|
200
|
+
if (d > bestDepth) { bestDepth = d; best = candidates[h]; }
|
|
149
201
|
}
|
|
150
202
|
picked.push(best);
|
|
151
203
|
} else if (type === "label") {
|
|
152
|
-
var
|
|
153
|
-
if (!
|
|
154
|
-
picked.
|
|
204
|
+
var ctlMatches = byLabel(scope, value);
|
|
205
|
+
if (!ctlMatches.length) continue;
|
|
206
|
+
picked = picked.concat(ctlMatches);
|
|
155
207
|
} else if (type === "role") {
|
|
156
208
|
var spec = value && typeof value === "object" ? value : { role: value };
|
|
157
209
|
picked = picked.concat(resolveByRoleIn([scope], spec.role, spec.name));
|
|
@@ -161,7 +213,7 @@ function locatorSource() {
|
|
|
161
213
|
});
|
|
162
214
|
picked = picked.concat(refHits);
|
|
163
215
|
} else if (type === "has-text") {
|
|
164
|
-
if (
|
|
216
|
+
if (normalizeText(scope.textContent).indexOf(normalizeText(value)) >= 0) picked.push(scope);
|
|
165
217
|
} else if (type === "has-sub") {
|
|
166
218
|
if (resolveWithin(scope, value).length) picked.push(scope);
|
|
167
219
|
} else if (type === "not-has-sub") {
|
|
@@ -317,7 +369,7 @@ function locatorSource() {
|
|
|
317
369
|
accessibleName: accessibleName,
|
|
318
370
|
implicitRole: implicitRole,
|
|
319
371
|
};
|
|
320
|
-
window.__keveLocatorSource = "
|
|
372
|
+
window.__keveLocatorSource = "v5";
|
|
321
373
|
return "ok";
|
|
322
374
|
})()`;
|
|
323
375
|
}
|