@kkeve/suite 0.0.15 → 0.0.18
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 +12 -11
- 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 +49 -15
- 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 +204 -200
- 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 +82 -7
- package/dist/page-agent/agent.js +252 -88
- 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/tools.d.ts +1 -1
- package/dist/page-agent/tools.js +144 -8
- package/package.json +24 -11
- package/scripts/gen-system-prompt.mjs +36 -0
- package/scripts/run-dsl-task.mjs +652 -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 -123
- 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,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* elementResolver — 将 ElementRef 解析为 Playwright Locator
|
|
4
|
+
*
|
|
5
|
+
* 支持单元素解析和多级嵌套链式解析(resolveChain)。
|
|
6
|
+
* 引擎无关层:仅依赖 Playwright Locator API。
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.resolveElement = resolveElement;
|
|
10
|
+
exports.resolveChain = resolveChain;
|
|
11
|
+
exports.resolveFromPage = resolveFromPage;
|
|
12
|
+
const definePage_js_1 = require("./definePage.js");
|
|
13
|
+
// ─── Platform Hop Resolution (keve-wiki chain) ────────────────────────
|
|
14
|
+
/** class/shadow-dom hop value → css(支持空格分隔多 class,容忍 value 自带前导点) */
|
|
15
|
+
function classToCss(v) {
|
|
16
|
+
return String(v).trim().split(/\s+/).map((c) => `.${c.replace(/^\.+/, '')}`).join('');
|
|
17
|
+
}
|
|
18
|
+
/** hop → css 片段:selector 原样;id 补 #;shadow-dom 宿主按 class 定位(Playwright css 自动穿透 open shadow root) */
|
|
19
|
+
function hopToCss(t, v) {
|
|
20
|
+
if (t === 'selector')
|
|
21
|
+
return v;
|
|
22
|
+
if (t === 'id')
|
|
23
|
+
return `#${v.replace(/^#/, '')}`;
|
|
24
|
+
if (t === 'shadow-dom')
|
|
25
|
+
return /^[.#[]/.test(v) ? v : classToCss(v);
|
|
26
|
+
return classToCss(v);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 平台定位链解析:按 hop 序列逐级收窄
|
|
30
|
+
*
|
|
31
|
+
* - selector → locator(css)
|
|
32
|
+
* - class → locator('.a.b')
|
|
33
|
+
* - includes/text → locator('text=...')(在当前范围内找包含该文本的元素)
|
|
34
|
+
* - label → getByLabel(表单控件按可见标签定位,平台 label hop 37 处)
|
|
35
|
+
* - iframe-url → frameLocator(进入 iframe 上下文,src 部分匹配)
|
|
36
|
+
* - javascript/source → 跳过(javascript 仅 WRITE_GLOBAL 场景由 engine.evaluate 处理)
|
|
37
|
+
*
|
|
38
|
+
* hop.index:1-based,-1/缺省表示不取序号。取序号时用 .nth(index-1) —— 平台侧
|
|
39
|
+
* 91 处带 index 的跳,此前被整体忽略,导致命中第一个元素而非期望的第 N 个。
|
|
40
|
+
*/
|
|
41
|
+
function applyHops(page, scope, hops) {
|
|
42
|
+
let cur = scope;
|
|
43
|
+
let lastHop = null;
|
|
44
|
+
for (const hop of hops) {
|
|
45
|
+
const t = String(hop?.type || '');
|
|
46
|
+
const v = hop?.value;
|
|
47
|
+
if (t === 'javascript' || t === 'source' || t === '')
|
|
48
|
+
continue;
|
|
49
|
+
if (t === 'iframe-url') {
|
|
50
|
+
const css = `iframe[src*="${String(v).replace(/"/g, '\\"')}"]`;
|
|
51
|
+
cur = cur.frameLocator(css);
|
|
52
|
+
lastHop = null; // iframe 本身不产生最终定位,重置
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (t === 'selector' || t === 'id' || t === 'shadow-dom') {
|
|
56
|
+
cur = cur.locator(hopToCss(t, String(v)));
|
|
57
|
+
}
|
|
58
|
+
else if (t === 'class') {
|
|
59
|
+
cur = cur.locator(classToCss(String(v)));
|
|
60
|
+
}
|
|
61
|
+
else if (t === 'includes' || t === 'text') {
|
|
62
|
+
const text = String(v).trim();
|
|
63
|
+
// 「包含文本」语义:用 text= 引擎匹配「包含该文本的最小元素」。
|
|
64
|
+
// 老实现 locator('*').filter({ hasText }) 只搜后代 —— 当文本是当前元素的
|
|
65
|
+
// 直接文本节点(如 .tab-text「我创建的」)时后代为空,链路永久落空(点击等到超时)。
|
|
66
|
+
// text= 引擎对「自身直挂文本」与「文本在后代元素」两类 DOM 都命中,
|
|
67
|
+
// 且与老行为(点中文本所在叶子)兼容。
|
|
68
|
+
cur = cur.locator('text=' + text.replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
|
|
69
|
+
}
|
|
70
|
+
else if (t === 'label') {
|
|
71
|
+
// 表单控件按可见标签定位(平台 label hop)。Playwright getByLabel 在
|
|
72
|
+
// Locator/FrameLocator 上同样可用,语义与「找 label 文本对应的输入控件」一致。
|
|
73
|
+
const label = String(v).trim();
|
|
74
|
+
cur = cur.getByLabel(label, { exact: false });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
throw new Error(`Unknown hop type '${t}' in chain: ${JSON.stringify(hop)}`);
|
|
78
|
+
}
|
|
79
|
+
// 序号收敛:1-based → .nth(index-1);-1 / 0 / 非数字表示不取序号
|
|
80
|
+
const idx = Number(hop?.index);
|
|
81
|
+
if (Number.isFinite(idx) && idx > 0 && typeof cur?.nth === 'function') {
|
|
82
|
+
cur = cur.nth(idx - 1);
|
|
83
|
+
}
|
|
84
|
+
lastHop = { type: t, value: v };
|
|
85
|
+
}
|
|
86
|
+
// iframe 进入后无后续定位跳:兜底取 frame 的 body
|
|
87
|
+
if (!lastHop && hops.some((h) => String(h?.type || '') === 'iframe-url')) {
|
|
88
|
+
cur = cur.locator('body');
|
|
89
|
+
}
|
|
90
|
+
if (!cur || typeof cur.locator !== 'function') {
|
|
91
|
+
throw new Error(`Chain resolution produced a non-Locator: ${JSON.stringify(hops).slice(0, 200)}`);
|
|
92
|
+
}
|
|
93
|
+
return cur;
|
|
94
|
+
}
|
|
95
|
+
// ─── Single Element Resolution ────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* 将单个 ElementRef 解析为 Playwright Locator
|
|
98
|
+
*
|
|
99
|
+
* 优先级:ref > role+name > css
|
|
100
|
+
* 复合定位:hasText, has, notHas 在基础定位上叠加
|
|
101
|
+
*/
|
|
102
|
+
function resolveElement(page, ref) {
|
|
103
|
+
// 平台多跳定位链优先(keve-wiki 导出代码的原生形态)
|
|
104
|
+
if (ref.chain && ref.chain.length > 0) {
|
|
105
|
+
return applyHops(page, page, ref.chain);
|
|
106
|
+
}
|
|
107
|
+
let locator;
|
|
108
|
+
if (ref.ref) {
|
|
109
|
+
// AI 探索临时 ref(Playwright MCP 风格的 a11y ref)
|
|
110
|
+
// 实际实现中需要通过 ariaSnapshot 映射,这里用 css 兜底
|
|
111
|
+
locator = page.locator(`[data-ref="${ref.ref}"]`);
|
|
112
|
+
}
|
|
113
|
+
else if (ref.role) {
|
|
114
|
+
// role + name 语义定位
|
|
115
|
+
if (ref.name) {
|
|
116
|
+
locator = page.getByRole(ref.role, { name: ref.name, exact: false });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
locator = page.getByRole(ref.role);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (ref.css) {
|
|
123
|
+
locator = page.locator(ref.css);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
throw new Error(`Invalid ElementRef: must have role, css, or ref. Got: ${JSON.stringify(ref)}`);
|
|
127
|
+
}
|
|
128
|
+
// 复合定位:hasText
|
|
129
|
+
if (ref.hasText) {
|
|
130
|
+
locator = locator.filter({ hasText: ref.hasText });
|
|
131
|
+
}
|
|
132
|
+
// 复合定位:has(包含子元素)
|
|
133
|
+
if (ref.has) {
|
|
134
|
+
const childLocator = resolveElement(page, ref.has);
|
|
135
|
+
locator = locator.filter({ has: childLocator });
|
|
136
|
+
}
|
|
137
|
+
// 复合定位:notHas(不包含子元素)
|
|
138
|
+
if (ref.notHas) {
|
|
139
|
+
const childLocator = resolveElement(page, ref.notHas);
|
|
140
|
+
locator = locator.filter({ hasNot: childLocator });
|
|
141
|
+
}
|
|
142
|
+
return locator;
|
|
143
|
+
}
|
|
144
|
+
// ─── Chain Resolution (Multi-level Nesting) ──────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* 多级嵌套解析:按顺序在父元素范围内查找子元素
|
|
147
|
+
*
|
|
148
|
+
* resolveChain(page, parent, child) → parent.locator(child)
|
|
149
|
+
* resolveChain(page, a, b, c, d) → a.locator(b).locator(c).locator(d)
|
|
150
|
+
*
|
|
151
|
+
* 支持 ElementRef 和 ElementEntry(工厂函数需先调用)
|
|
152
|
+
*/
|
|
153
|
+
function resolveChain(page, ...refs) {
|
|
154
|
+
if (refs.length === 0) {
|
|
155
|
+
throw new Error('resolveChain requires at least one element ref');
|
|
156
|
+
}
|
|
157
|
+
// 将所有 entry 规范化为 ElementRef
|
|
158
|
+
const elements = refs.map((entry, _idx) => {
|
|
159
|
+
if ((0, definePage_js_1.isElementFactory)(entry)) {
|
|
160
|
+
throw new Error('Cannot use factory function in chain without calling it first. Call factory(item) before passing to resolveChain.');
|
|
161
|
+
}
|
|
162
|
+
return entry;
|
|
163
|
+
});
|
|
164
|
+
// 第一个元素:从 page 级定位
|
|
165
|
+
let locator = resolveElement(page, elements[0]);
|
|
166
|
+
// 后续元素:在父 locator 范围内定位
|
|
167
|
+
for (let i = 1; i < elements.length; i++) {
|
|
168
|
+
const childRef = elements[i];
|
|
169
|
+
// 平台多跳定位链:在当前范围内继续 hop 解析
|
|
170
|
+
if (childRef.chain && childRef.chain.length > 0) {
|
|
171
|
+
locator = applyHops(page, locator, childRef.chain);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// 子元素如果只有 hasText,用 filter;否则用 locator 链
|
|
175
|
+
if (childRef.hasText && !childRef.role && !childRef.css && !childRef.ref) {
|
|
176
|
+
locator = locator.filter({ hasText: childRef.hasText });
|
|
177
|
+
}
|
|
178
|
+
else if (childRef.role) {
|
|
179
|
+
if (childRef.name) {
|
|
180
|
+
locator = locator.getByRole(childRef.role, { name: childRef.name, exact: false });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
locator = locator.getByRole(childRef.role);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (childRef.css) {
|
|
187
|
+
locator = locator.locator(childRef.css);
|
|
188
|
+
}
|
|
189
|
+
else if (childRef.ref) {
|
|
190
|
+
locator = locator.locator(`[data-ref="${childRef.ref}"]`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
throw new Error(`Invalid chain element at position ${i}: ${JSON.stringify(childRef)}`);
|
|
194
|
+
}
|
|
195
|
+
// 复合定位叠加
|
|
196
|
+
if (childRef.hasText && (childRef.role || childRef.css || childRef.ref)) {
|
|
197
|
+
locator = locator.filter({ hasText: childRef.hasText });
|
|
198
|
+
}
|
|
199
|
+
if (childRef.has) {
|
|
200
|
+
locator = locator.filter({ has: resolveElement(page, childRef.has) });
|
|
201
|
+
}
|
|
202
|
+
if (childRef.notHas) {
|
|
203
|
+
locator = locator.filter({ hasNot: resolveElement(page, childRef.notHas) });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return locator;
|
|
207
|
+
}
|
|
208
|
+
// ─── Convenience: Resolve from Page definition ────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* 从 DefinedPage 中解析元素(自动处理工厂函数)
|
|
211
|
+
*
|
|
212
|
+
* resolveFromPage(page, 'saveButton') → Locator
|
|
213
|
+
* resolveFromPage(page, 'itemByName', '时间') → Locator(工厂调用)
|
|
214
|
+
* resolveFromPage(page, 'dimensionList', ['itemByName', '时间']) → 链式解析
|
|
215
|
+
*/
|
|
216
|
+
function resolveFromPage(pwPage, defPage, ...keys) {
|
|
217
|
+
const refs = [];
|
|
218
|
+
for (const key of keys) {
|
|
219
|
+
if (typeof key === 'string') {
|
|
220
|
+
const entry = defPage.elements[key];
|
|
221
|
+
if ((0, definePage_js_1.isElementFactory)(entry)) {
|
|
222
|
+
throw new Error(`Element '${key}' is a factory, needs args. Use [key, ...args] tuple.`);
|
|
223
|
+
}
|
|
224
|
+
refs.push(entry);
|
|
225
|
+
}
|
|
226
|
+
else if (Array.isArray(key)) {
|
|
227
|
+
const [k, ...args] = key;
|
|
228
|
+
const entry = defPage.elements[k];
|
|
229
|
+
if (!(0, definePage_js_1.isElementFactory)(entry)) {
|
|
230
|
+
throw new Error(`Element '${k}' is not a factory, but args were provided.`);
|
|
231
|
+
}
|
|
232
|
+
refs.push(entry(...args));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return resolveChain(pwPage, ...refs);
|
|
236
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine-types — EngineAdapter 契约(零依赖,浏览器安全)
|
|
3
|
+
*
|
|
4
|
+
* 所有测试脚本通过 `engine.*` 调用操作,不直接使用 page.locator / cy.get。
|
|
5
|
+
* 契约单独成文件的原因:Cypress 侧要在浏览器里 import 它,而
|
|
6
|
+
* engineAdapter.ts 里躺着 PlaywrightEngine —— 只要 Cypress 入口碰到那个模块,
|
|
7
|
+
* esbuild 就会把 playwright-core → node:fs 整条链拉进浏览器 bundle 而构建失败。
|
|
8
|
+
*
|
|
9
|
+
* 约束:本模块只允许 `import type`,不得引入任何运行时依赖。
|
|
10
|
+
*/
|
|
11
|
+
import type { ElementRef, ElementEntry, DefinedPage } from './definePage.js';
|
|
12
|
+
import type { ExpectScreenshotOptions, ShotDiffResult } from '../judge/screenshotDiff.js';
|
|
13
|
+
/** waitForResponse 选项:就绪门语义(软等待 = 超时不阻塞,与 waitForReady.api 一致) */
|
|
14
|
+
export interface WaitForResponseOptions {
|
|
15
|
+
/** 等待超时(默认 15000ms) */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/** 匹配的 HTTP 方法(如 'POST';不传则不限) */
|
|
18
|
+
method?: string;
|
|
19
|
+
/** 期望的 HTTP 状态码列表(如 [200];不传则不限)。给定时不匹配则继续等 */
|
|
20
|
+
status?: number[];
|
|
21
|
+
/** 超时是否视为失败。默认 false(软等待:打日志继续),true 则抛错(硬门) */
|
|
22
|
+
required?: boolean;
|
|
23
|
+
/** 命中后额外等待,给前端渲染留时间(默认 0) */
|
|
24
|
+
afterMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/** waitForResponse 结果:命中的响应摘要;软等待超时时 hit=false */
|
|
27
|
+
export interface WaitForResponseResult {
|
|
28
|
+
hit: boolean;
|
|
29
|
+
url: string;
|
|
30
|
+
status: number;
|
|
31
|
+
/** 响应体(JSON 可解析时为对象,否则为文本;体过大或非文本为 null) */
|
|
32
|
+
body: any;
|
|
33
|
+
elapsedMs: number;
|
|
34
|
+
}
|
|
35
|
+
export interface EngineAdapter {
|
|
36
|
+
/** 导航到 URL */
|
|
37
|
+
navigate(url: string): Promise<void>;
|
|
38
|
+
/** 打开 PageObject 页面入口:按 KEVE_ENV 解析四环境 URL 并填充 ${param} 占位(overrides 优先于 page.url.params) */
|
|
39
|
+
openPage(page: DefinedPage, overrides?: Record<string, string>): Promise<void>;
|
|
40
|
+
/** 调用其他场景(用例组合原语):按 @keveScene 注册的 id 查找并执行其方法体,无需 spec 互相 import */
|
|
41
|
+
callScene(sceneId: string, keveGoal?: unknown): Promise<void>;
|
|
42
|
+
/** 等待页面就绪(API + UI 信号) */
|
|
43
|
+
waitForReady(page: DefinedPage): Promise<void>;
|
|
44
|
+
/** 等待指定响应(就绪门,平台 WAIT_RESPONSE 转译落地用)。
|
|
45
|
+
* 配置型场景没有 DefinedPage,只能按 URL 片段/正则等待 —— 语义与 waitForReady.api 一致 */
|
|
46
|
+
waitForResponse(urlPart: string | RegExp, opts?: WaitForResponseOptions): Promise<WaitForResponseResult>;
|
|
47
|
+
/** 点击元素 */
|
|
48
|
+
click(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
49
|
+
/** 双击元素(平台 DB_CLICK_ELEMENT) */
|
|
50
|
+
dblClick(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
51
|
+
/** 强制点击元素(平台 FORCE_CLICK_ELEMENT,跳过 actionability 检查) */
|
|
52
|
+
forceClick(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
53
|
+
/** 输入文本 */
|
|
54
|
+
type(text: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
55
|
+
/** 清空输入内容(平台 CLEAN) */
|
|
56
|
+
clear(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
57
|
+
/** 悬停元素 */
|
|
58
|
+
hover(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
59
|
+
/** 拖拽元素到目标(平台 DRAGGING:source 拖到 target) */
|
|
60
|
+
drag(source: ElementRef | ElementEntry, target: ElementRef | ElementEntry): Promise<void>;
|
|
61
|
+
/** 滚动使元素进入视口(平台 SCROLL) */
|
|
62
|
+
scroll(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
63
|
+
/** 等待指定毫秒(平台 WAIT_EVENTS) */
|
|
64
|
+
wait(ms: number): Promise<void>;
|
|
65
|
+
/** 等待元素就绪(平台元素门的真检测:按 selector 等 visible/hidden,超时返回 found=false 不抛错;
|
|
66
|
+
* 替代「固定延时兜底」——只有元素真的就绪才算就绪,防丢响应/防假就绪) */
|
|
67
|
+
waitForElement(selector: string, opts?: {
|
|
68
|
+
timeout?: number;
|
|
69
|
+
visible?: boolean;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
found: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
/** 刷新页面(平台 REFRESH_PAGE) */
|
|
74
|
+
refresh(): Promise<void>;
|
|
75
|
+
/** 在页面上下文执行 JS 表达式(平台 WRITE_GLOBAL 的 javascript hop) */
|
|
76
|
+
evaluate(expression: string): Promise<any>;
|
|
77
|
+
/** 截图并附加到测试报告(平台 SCREEN_SHOT_COMPARE)。⚠ 语义已升级为基线对比断言:
|
|
78
|
+
* 无基线 → 自动建候选基线(pass);差异率超阈值 → 抛 ScreenshotDiffError(确定性 fail);
|
|
79
|
+
* 对比结果(差异率/区块/证据图路径)进 keveGoalResult attachment(screenshotDiff 字段) */
|
|
80
|
+
screenshot(name: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
81
|
+
/** 截图基线对比断言(expectScreenshot 的显式形态,支持阈值/掩码/全页选项) */
|
|
82
|
+
expectScreenshot(key: string, opts?: ExpectScreenshotOptions, ...refs: (ElementRef | ElementEntry)[]): Promise<ShotDiffResult>;
|
|
83
|
+
/** 获取元素文本 */
|
|
84
|
+
text(...refs: (ElementRef | ElementEntry)[]): Promise<string>;
|
|
85
|
+
/** 断言:元素可见 */
|
|
86
|
+
expectVisible(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
87
|
+
/** 断言:元素不可见 */
|
|
88
|
+
expectHidden(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
89
|
+
/** 断言:元素文本包含 */
|
|
90
|
+
expectText(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
91
|
+
/** 断言:元素文本不包含(平台 TEXT_NOT_EXIST) */
|
|
92
|
+
expectNotText(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
93
|
+
/** 断言:元素文本等于(平台 TEXT_EQUAL / EXPECTED_VALUE ctype=text cexpression=eq) */
|
|
94
|
+
expectTextEquals(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
95
|
+
/** 断言:元素文本不等于(平台 EXPECTED_VALUE ctype=text cexpression=not.eq) */
|
|
96
|
+
expectTextNotEquals(expected: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
97
|
+
/** 断言:元素数量 */
|
|
98
|
+
expectCount(count: number, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
99
|
+
/** 断言:元素集合中「文本非空」的个数(平台 EXPECTED_VALUE ctype=index,历史语义为计数) */
|
|
100
|
+
expectNonEmptyCount(count: number, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
101
|
+
/** 断言:元素存在 */
|
|
102
|
+
expectExists(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
103
|
+
/** 断言:元素不存在 */
|
|
104
|
+
expectNotExists(...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
105
|
+
/** 断言:元素包含指定 class */
|
|
106
|
+
expectClass(className: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
107
|
+
/** 断言:元素不包含指定 class(平台 EXPECTED_VALUE ctype=class cexpression=not.include) */
|
|
108
|
+
expectNotClass(className: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
109
|
+
/** 断言:元素属性值 */
|
|
110
|
+
expectAttribute(attr: string, value: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
111
|
+
/** 断言:元素属性值包含(平台 EXPECTED_VALUE ctype=style cexpression=include) */
|
|
112
|
+
expectAttributeContains(attr: string, value: string, ...refs: (ElementRef | ElementEntry)[]): Promise<void>;
|
|
113
|
+
/** 断言:当前 URL 包含指定片段(平台 EXPECTED__PAGET_VALUE ctype=url) */
|
|
114
|
+
expectUrlContains(part: string): Promise<void>;
|
|
115
|
+
/** 断言:当前 URL 不包含指定片段 */
|
|
116
|
+
expectUrlNotContains(part: string): Promise<void>;
|
|
117
|
+
/** 断言:当前 URL 等于(平台 EXPECTED__PAGET_VALUE ctype=url cexpression=eq) */
|
|
118
|
+
expectUrlEquals(url: string): Promise<void>;
|
|
119
|
+
/** 断言:当前 URL 不等于 */
|
|
120
|
+
expectUrlNotEquals(url: string): Promise<void>;
|
|
121
|
+
/** 断言:页面标题包含(平台 EXPECTED__PAGET_VALUE ctype=title cexpression=include) */
|
|
122
|
+
expectTitle(part: string): Promise<void>;
|
|
123
|
+
/** 断言:页面标题等于 */
|
|
124
|
+
expectTitleEquals(title: string): Promise<void>;
|
|
125
|
+
/** 获取底层引擎 page(仅探索场景使用;Cypress 下返回 CDP driver,无 Playwright Page) */
|
|
126
|
+
rawPage(): any;
|
|
127
|
+
/** aria snapshot(AI 探索用) */
|
|
128
|
+
ariaSnapshot(options?: {
|
|
129
|
+
mode?: string;
|
|
130
|
+
}): Promise<string>;
|
|
131
|
+
/** 注入 cookies 到当前浏览器上下文(登录态透传) */
|
|
132
|
+
injectCookies(cookies: any[]): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
/** 当前执行环境(调度层 KEVE_ENV 注入;与 keve-wiki test-data.ts 同源同序) */
|
|
135
|
+
export declare const KEVE_ENV: string;
|
|
136
|
+
/**
|
|
137
|
+
* 解析 PageObject 入口 URL:KEVE_ENV 环境值 → ${param} 占位填充(overrides 优先于默认 params)
|
|
138
|
+
* 环境值非法(非 http,如平台脏数据 "1")时回退 test → online → 任意 http 值
|
|
139
|
+
*/
|
|
140
|
+
export declare function resolvePageUrl(page: DefinedPage, overrides?: Record<string, string>): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* engine-types — EngineAdapter 契约(零依赖,浏览器安全)
|
|
4
|
+
*
|
|
5
|
+
* 所有测试脚本通过 `engine.*` 调用操作,不直接使用 page.locator / cy.get。
|
|
6
|
+
* 契约单独成文件的原因:Cypress 侧要在浏览器里 import 它,而
|
|
7
|
+
* engineAdapter.ts 里躺着 PlaywrightEngine —— 只要 Cypress 入口碰到那个模块,
|
|
8
|
+
* esbuild 就会把 playwright-core → node:fs 整条链拉进浏览器 bundle 而构建失败。
|
|
9
|
+
*
|
|
10
|
+
* 约束:本模块只允许 `import type`,不得引入任何运行时依赖。
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.KEVE_ENV = void 0;
|
|
14
|
+
exports.resolvePageUrl = resolvePageUrl;
|
|
15
|
+
/** 当前执行环境(调度层 KEVE_ENV 注入;与 keve-wiki test-data.ts 同源同序) */
|
|
16
|
+
exports.KEVE_ENV = (['online', 'pre', 'rc', 'test'].includes(process.env.KEVE_ENV || '') ? process.env.KEVE_ENV : 'test');
|
|
17
|
+
/**
|
|
18
|
+
* 解析 PageObject 入口 URL:KEVE_ENV 环境值 → ${param} 占位填充(overrides 优先于默认 params)
|
|
19
|
+
* 环境值非法(非 http,如平台脏数据 "1")时回退 test → online → 任意 http 值
|
|
20
|
+
*/
|
|
21
|
+
function resolvePageUrl(page, overrides) {
|
|
22
|
+
const u = page.url;
|
|
23
|
+
if (!u)
|
|
24
|
+
throw new Error(`openPage: 页面 ${page.__pageId} 未定义 url 入口`);
|
|
25
|
+
const candidates = [u[String(exports.KEVE_ENV)], u.test, u.online, ...Object.values(u)].filter((v) => typeof v === 'string' && /^https?:\/\//.test(v));
|
|
26
|
+
let url = String(candidates[0] || '');
|
|
27
|
+
if (!url)
|
|
28
|
+
throw new Error(`openPage: 页面 ${page.__pageId} 的 url 无有效 http 值: ${JSON.stringify(u)}`);
|
|
29
|
+
const params = { ...(u.params || {}), ...(overrides || {}) };
|
|
30
|
+
for (const [k, v] of Object.entries(params)) {
|
|
31
|
+
url = url.split('${' + k + '}').join(encodeURIComponent(String(v)));
|
|
32
|
+
}
|
|
33
|
+
return url.replace(/\$\{\w+\}/g, ''); // 未覆盖的占位符清空(保底不把 ${x} 发给服务端)
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSO Cookie 类型定义(仅类型,不做登录/缓存)
|
|
3
|
+
*
|
|
4
|
+
* SSO 登录态的唯一缓存与登录入口都在 eve-backend
|
|
5
|
+
* (src/services/ai/sso-cookies.ts,Redis key sso:{username})。
|
|
6
|
+
* suite 侧只负责接收服务端下发的 cookie 并注入浏览器,不再持有任何
|
|
7
|
+
* Redis/进程内缓存,避免多套缓存各自过期、互相覆盖造成「坏登录态被复用」。
|
|
8
|
+
*/
|
|
9
|
+
export interface SsoCookie {
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
domain: string;
|
|
13
|
+
path: string;
|
|
14
|
+
secure: boolean;
|
|
15
|
+
httpOnly?: boolean;
|
|
16
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
17
|
+
expires?: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SSO Cookie 类型定义(仅类型,不做登录/缓存)
|
|
4
|
+
*
|
|
5
|
+
* SSO 登录态的唯一缓存与登录入口都在 eve-backend
|
|
6
|
+
* (src/services/ai/sso-cookies.ts,Redis key sso:{username})。
|
|
7
|
+
* suite 侧只负责接收服务端下发的 cookie 并注入浏览器,不再持有任何
|
|
8
|
+
* Redis/进程内缓存,避免多套缓存各自过期、互相覆盖造成「坏登录态被复用」。
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsl — 平台 DSL ↔ @kkeve/suite 装饰器脚本的双向投影入口
|
|
3
|
+
*
|
|
4
|
+
* 本目录保持**零 IO、零框架依赖**,因此可以同时被打进:
|
|
5
|
+
* - Playwright 执行产物(@kkeve/suite/keve-test)
|
|
6
|
+
* - Cypress 浏览器 bundle(@kkeve/engine-cypress)
|
|
7
|
+
* - eve-backend 的生成器 service 层(Node 侧)
|
|
8
|
+
*
|
|
9
|
+
* 分层:DB 装载 / 内嵌场景展开 / 元素库 source 跳解析 / 变量表查询全部留在
|
|
10
|
+
* 调用方(见 eve-backend 的 dslLoader)。这里只做纯函数投影。
|
|
11
|
+
*/
|
|
12
|
+
export { toDecorator, type DslHop, type DslValue, type DslExpectation, type DslStep, type DslVariableRow, type ToDecoratorOptions, } from './toDecorator.js';
|
|
13
|
+
/** 语义别名:调用方按「生成一份装饰器 spec」的语义引用时更直观 */
|
|
14
|
+
export { toDecorator as generateDecoratorSpec } from './toDecorator.js';
|
|
15
|
+
export { STEP_OPERATIONS, ASSERT_OPERATIONS, TEXT_ONLY_ASSERT_OPERATIONS, EXPECTED_VALUE_CTYPES, COMPARISON_EXPRESSIONS, isKnownOperation, isKnownAssert, isTextOnlyAssert, parseComparison, type StepOperation, } from './ops.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dsl — 平台 DSL ↔ @kkeve/suite 装饰器脚本的双向投影入口
|
|
4
|
+
*
|
|
5
|
+
* 本目录保持**零 IO、零框架依赖**,因此可以同时被打进:
|
|
6
|
+
* - Playwright 执行产物(@kkeve/suite/keve-test)
|
|
7
|
+
* - Cypress 浏览器 bundle(@kkeve/engine-cypress)
|
|
8
|
+
* - eve-backend 的生成器 service 层(Node 侧)
|
|
9
|
+
*
|
|
10
|
+
* 分层:DB 装载 / 内嵌场景展开 / 元素库 source 跳解析 / 变量表查询全部留在
|
|
11
|
+
* 调用方(见 eve-backend 的 dslLoader)。这里只做纯函数投影。
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.parseComparison = exports.isTextOnlyAssert = exports.isKnownAssert = exports.isKnownOperation = exports.COMPARISON_EXPRESSIONS = exports.EXPECTED_VALUE_CTYPES = exports.TEXT_ONLY_ASSERT_OPERATIONS = exports.ASSERT_OPERATIONS = exports.STEP_OPERATIONS = exports.generateDecoratorSpec = exports.toDecorator = void 0;
|
|
15
|
+
var toDecorator_js_1 = require("./toDecorator.js");
|
|
16
|
+
Object.defineProperty(exports, "toDecorator", { enumerable: true, get: function () { return toDecorator_js_1.toDecorator; } });
|
|
17
|
+
/** 语义别名:调用方按「生成一份装饰器 spec」的语义引用时更直观 */
|
|
18
|
+
var toDecorator_js_2 = require("./toDecorator.js");
|
|
19
|
+
Object.defineProperty(exports, "generateDecoratorSpec", { enumerable: true, get: function () { return toDecorator_js_2.toDecorator; } });
|
|
20
|
+
var ops_js_1 = require("./ops.js");
|
|
21
|
+
Object.defineProperty(exports, "STEP_OPERATIONS", { enumerable: true, get: function () { return ops_js_1.STEP_OPERATIONS; } });
|
|
22
|
+
Object.defineProperty(exports, "ASSERT_OPERATIONS", { enumerable: true, get: function () { return ops_js_1.ASSERT_OPERATIONS; } });
|
|
23
|
+
Object.defineProperty(exports, "TEXT_ONLY_ASSERT_OPERATIONS", { enumerable: true, get: function () { return ops_js_1.TEXT_ONLY_ASSERT_OPERATIONS; } });
|
|
24
|
+
Object.defineProperty(exports, "EXPECTED_VALUE_CTYPES", { enumerable: true, get: function () { return ops_js_1.EXPECTED_VALUE_CTYPES; } });
|
|
25
|
+
Object.defineProperty(exports, "COMPARISON_EXPRESSIONS", { enumerable: true, get: function () { return ops_js_1.COMPARISON_EXPRESSIONS; } });
|
|
26
|
+
Object.defineProperty(exports, "isKnownOperation", { enumerable: true, get: function () { return ops_js_1.isKnownOperation; } });
|
|
27
|
+
Object.defineProperty(exports, "isKnownAssert", { enumerable: true, get: function () { return ops_js_1.isKnownAssert; } });
|
|
28
|
+
Object.defineProperty(exports, "isTextOnlyAssert", { enumerable: true, get: function () { return ops_js_1.isTextOnlyAssert; } });
|
|
29
|
+
Object.defineProperty(exports, "parseComparison", { enumerable: true, get: function () { return ops_js_1.parseComparison; } });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locatorSource — 浏览器侧元素定位(hop 链)源码生成
|
|
3
|
+
*
|
|
4
|
+
* Cypress 侧没有 Playwright Locator,元素定位必须在 AUT 的 DOM 里自己实现。
|
|
5
|
+
* 本模块不直接操作 DOM,只**生成一段可在页面上下文求值的自包含源码字符串**,
|
|
6
|
+
* 由 engine-cypress 通过 CDP `Runtime.evaluate` 注入一次,之后复用。
|
|
7
|
+
*
|
|
8
|
+
* 之所以生成源码而不是导出普通函数:注入 isolate world 的函数无法闭包引用
|
|
9
|
+
* bundle 里的变量,只能整段作为源码传过去。因此生成物必须零外部引用。
|
|
10
|
+
*
|
|
11
|
+
* 语义严格对齐 elementResolver.ts::applyHops —— 同一套 hop 只有一种解释:
|
|
12
|
+
* selector/id/shadow-dom → CSS
|
|
13
|
+
* class → .a.b(空格分隔多 class)
|
|
14
|
+
* includes/text → 文本包含,取「包含该文本的最深(最小)元素」
|
|
15
|
+
* label → 可见 label 关联的表单控件
|
|
16
|
+
* iframe-url → 同源 iframe 内继续;跨源标记 cross-origin-iframe
|
|
17
|
+
* javascript/source/'' → 跳过
|
|
18
|
+
* hop.index → 1-based;-1/0/非数字表示不取序号
|
|
19
|
+
*
|
|
20
|
+
* 关键差异(相对 Playwright):Playwright 的 locator 链是「集合 → 集合」,
|
|
21
|
+
* 中间不收敛到单元素;只有末端 .first()/.nth(i) 才收敛。这里同样保持
|
|
22
|
+
* 「集合传递」直到末端,保证 `selector` + `includes` 这类链的结果集正确。
|
|
23
|
+
*/
|
|
24
|
+
/** 生成页内定位器源码(幂等:重复注入只覆盖同名函数,无副作用) */
|
|
25
|
+
export declare function locatorSource(): string;
|