@qontinui/ui-bridge 0.2.0 → 0.3.1
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/ai/index.d.mts +312 -155
- package/dist/ai/index.d.ts +312 -155
- package/dist/ai/index.js +2363 -67
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/index.mjs +2328 -68
- package/dist/ai/index.mjs.map +1 -1
- package/dist/annotations/index.d.mts +218 -0
- package/dist/annotations/index.d.ts +218 -0
- package/dist/annotations/index.js +246 -0
- package/dist/annotations/index.js.map +1 -0
- package/dist/annotations/index.mjs +241 -0
- package/dist/annotations/index.mjs.map +1 -0
- package/dist/assertions-BSR3afVr.d.ts +161 -0
- package/dist/assertions-CTw1hfOx.d.mts +161 -0
- package/dist/babel-plugin/index.js +504 -0
- package/dist/babel-plugin/index.js.map +1 -0
- package/dist/babel-plugin/index.mjs +488 -0
- package/dist/babel-plugin/index.mjs.map +1 -0
- package/dist/browser-capture-Bms60T6f.d.mts +47 -0
- package/dist/browser-capture-CsTU29mb.d.ts +47 -0
- package/dist/control/index.d.mts +26 -7
- package/dist/control/index.d.ts +26 -7
- package/dist/control/index.js +276 -48
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +276 -48
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.d.mts +115 -44
- package/dist/core/index.d.ts +115 -44
- package/dist/core/index.js +0 -1560
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +1 -1549
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.d.mts +5 -3
- package/dist/debug/index.d.ts +5 -3
- package/dist/debug/index.js +925 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +924 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.mts +13 -9
- package/dist/index.d.ts +13 -9
- package/dist/index.js +8310 -3777
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8246 -3766
- package/dist/index.mjs.map +1 -1
- package/dist/{metrics-NC3csD0R.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
- package/dist/{metrics-C9XRi_mL.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
- package/dist/native/control/index.js +448 -0
- package/dist/native/control/index.js.map +1 -0
- package/dist/native/control/index.mjs +445 -0
- package/dist/native/control/index.mjs.map +1 -0
- package/dist/native/core/index.js +486 -0
- package/dist/native/core/index.js.map +1 -0
- package/dist/native/core/index.mjs +475 -0
- package/dist/native/core/index.mjs.map +1 -0
- package/dist/native/debug/index.js +408 -0
- package/dist/native/debug/index.js.map +1 -0
- package/dist/native/debug/index.mjs +406 -0
- package/dist/native/debug/index.mjs.map +1 -0
- package/dist/native/index.js +2232 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/index.mjs +2204 -0
- package/dist/native/index.mjs.map +1 -0
- package/dist/native/react/index.js +1377 -0
- package/dist/native/react/index.js.map +1 -0
- package/dist/native/react/index.mjs +1365 -0
- package/dist/native/react/index.mjs.map +1 -0
- package/dist/native/server/index.js +440 -0
- package/dist/native/server/index.js.map +1 -0
- package/dist/native/server/index.mjs +435 -0
- package/dist/native/server/index.mjs.map +1 -0
- package/dist/react/index.d.mts +121 -9
- package/dist/react/index.d.ts +121 -9
- package/dist/react/index.js +2239 -91
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +2239 -92
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-CIEDjbQ9.d.ts → registry-C6dDtn1v.d.ts} +34 -15
- package/dist/{registry-SsSDq46X.d.mts → registry-POtcxnal.d.mts} +34 -15
- package/dist/render-log/index.d.mts +1 -1
- package/dist/render-log/index.d.ts +1 -1
- package/dist/server/express.d.mts +37 -0
- package/dist/server/express.d.ts +37 -0
- package/dist/server/express.js +298 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/express.mjs +294 -0
- package/dist/server/express.mjs.map +1 -0
- package/dist/server/handlers.d.mts +124 -0
- package/dist/server/handlers.d.ts +124 -0
- package/dist/server/handlers.js +7183 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/handlers.mjs +7180 -0
- package/dist/server/handlers.mjs.map +1 -0
- package/dist/server/index.d.mts +12 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +8384 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +8369 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/server/nextjs.d.mts +128 -0
- package/dist/server/nextjs.d.ts +128 -0
- package/dist/server/nextjs.js +390 -0
- package/dist/server/nextjs.js.map +1 -0
- package/dist/server/nextjs.mjs +385 -0
- package/dist/server/nextjs.mjs.map +1 -0
- package/dist/server/standalone.d.mts +7 -0
- package/dist/server/standalone.d.ts +7 -0
- package/dist/server/standalone.js +845 -0
- package/dist/server/standalone.js.map +1 -0
- package/dist/server/standalone.mjs +841 -0
- package/dist/server/standalone.mjs.map +1 -0
- package/dist/specs/index.d.mts +365 -0
- package/dist/specs/index.d.ts +365 -0
- package/dist/specs/index.js +2809 -0
- package/dist/specs/index.js.map +1 -0
- package/dist/specs/index.mjs +2786 -0
- package/dist/specs/index.mjs.map +1 -0
- package/dist/standalone-B6GLIEmR.d.ts +216 -0
- package/dist/standalone-CjdYqj3P.d.mts +216 -0
- package/dist/swc-plugin/index.d.mts +79 -0
- package/dist/swc-plugin/index.d.ts +79 -0
- package/dist/swc-plugin/index.js +15 -0
- package/dist/swc-plugin/index.js.map +1 -0
- package/dist/swc-plugin/index.mjs +9 -0
- package/dist/swc-plugin/index.mjs.map +1 -0
- package/dist/types-B2EfvEaq.d.ts +236 -0
- package/dist/{types-Dr6tH-bm.d.mts → types-C7gVYRnF.d.ts} +72 -2
- package/dist/{types-oCTrRxSw.d.ts → types-CJGrBEhC.d.mts} +72 -2
- package/dist/types-CebMQj76.d.ts +1275 -0
- package/dist/types-D_ypYl3T.d.mts +1275 -0
- package/dist/types-UBtp7R0u.d.mts +132 -0
- package/dist/types-UBtp7R0u.d.ts +132 -0
- package/dist/types-gO696T_t.d.mts +236 -0
- package/dist/{types-CPMbN_Iw.d.mts → types-suaYwWWg.d.mts} +519 -152
- package/dist/{types-CPMbN_Iw.d.ts → types-suaYwWWg.d.ts} +519 -152
- package/package.json +123 -4
- package/swc-plugin-wasm/ui_bridge_swc_plugin.wasm +0 -0
- package/dist/types-BvCfFuEV.d.ts +0 -534
- package/dist/types-CFT3Dnx4.d.mts +0 -534
- package/dist/websocket-client-CX4QJesI.d.ts +0 -124
- package/dist/websocket-client-C_Na0OSp.d.mts +0 -124
|
@@ -0,0 +1,2809 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/specs/types.ts
|
|
4
|
+
var SPEC_CONFIG_VERSION = "1.0.0";
|
|
5
|
+
var SPEC_FILE_EXTENSION = ".spec.uibridge.json";
|
|
6
|
+
var VALID_ASSERTION_TYPES = [
|
|
7
|
+
"visible",
|
|
8
|
+
"hidden",
|
|
9
|
+
"enabled",
|
|
10
|
+
"disabled",
|
|
11
|
+
"focused",
|
|
12
|
+
"checked",
|
|
13
|
+
"unchecked",
|
|
14
|
+
"hasText",
|
|
15
|
+
"containsText",
|
|
16
|
+
"hasValue",
|
|
17
|
+
"hasClass",
|
|
18
|
+
"exists",
|
|
19
|
+
"notExists",
|
|
20
|
+
"count",
|
|
21
|
+
"attribute",
|
|
22
|
+
"cssProperty"
|
|
23
|
+
];
|
|
24
|
+
var VALID_SPEC_CATEGORIES = [
|
|
25
|
+
"element-presence",
|
|
26
|
+
"accessibility",
|
|
27
|
+
"form-validation",
|
|
28
|
+
"state-consistency",
|
|
29
|
+
"modal-dialog",
|
|
30
|
+
"navigation",
|
|
31
|
+
"cross-page-consistency",
|
|
32
|
+
"custom"
|
|
33
|
+
];
|
|
34
|
+
var VALID_SPEC_SEVERITIES = [
|
|
35
|
+
"critical",
|
|
36
|
+
"warning",
|
|
37
|
+
"info"
|
|
38
|
+
];
|
|
39
|
+
var VALID_SPEC_SOURCES = [
|
|
40
|
+
"auto",
|
|
41
|
+
"manual",
|
|
42
|
+
"ai-generated"
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// src/specs/validator.ts
|
|
46
|
+
function isValidAssertionType(value) {
|
|
47
|
+
return typeof value === "string" && VALID_ASSERTION_TYPES.includes(value);
|
|
48
|
+
}
|
|
49
|
+
function isValidSpecCategory(value) {
|
|
50
|
+
return typeof value === "string" && VALID_SPEC_CATEGORIES.includes(value);
|
|
51
|
+
}
|
|
52
|
+
function isValidSpecSeverity(value) {
|
|
53
|
+
return typeof value === "string" && VALID_SPEC_SEVERITIES.includes(value);
|
|
54
|
+
}
|
|
55
|
+
function isValidSpecSource(value) {
|
|
56
|
+
return typeof value === "string" && VALID_SPEC_SOURCES.includes(value);
|
|
57
|
+
}
|
|
58
|
+
function validateSpecAssertion(data, path = "assertion") {
|
|
59
|
+
const errors = [];
|
|
60
|
+
if (!data || typeof data !== "object") {
|
|
61
|
+
errors.push({ path, message: "must be an object" });
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
64
|
+
const obj = data;
|
|
65
|
+
if (typeof obj.id !== "string" || obj.id.length === 0) {
|
|
66
|
+
errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
|
|
67
|
+
}
|
|
68
|
+
if (typeof obj.description !== "string") {
|
|
69
|
+
errors.push({ path: `${path}.description`, message: "must be a string" });
|
|
70
|
+
}
|
|
71
|
+
if (!isValidSpecCategory(obj.category)) {
|
|
72
|
+
errors.push({
|
|
73
|
+
path: `${path}.category`,
|
|
74
|
+
message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!isValidSpecSeverity(obj.severity)) {
|
|
78
|
+
errors.push({
|
|
79
|
+
path: `${path}.severity`,
|
|
80
|
+
message: `must be one of: ${VALID_SPEC_SEVERITIES.join(", ")}`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!obj.target || typeof obj.target !== "object") {
|
|
84
|
+
errors.push({ path: `${path}.target`, message: "must be an object" });
|
|
85
|
+
} else {
|
|
86
|
+
const target = obj.target;
|
|
87
|
+
if (target.type === "elementId") {
|
|
88
|
+
if (typeof target.elementId !== "string" || target.elementId.length === 0) {
|
|
89
|
+
errors.push({ path: `${path}.target.elementId`, message: "must be a non-empty string" });
|
|
90
|
+
}
|
|
91
|
+
} else if (target.type === "search") {
|
|
92
|
+
if (!target.criteria || typeof target.criteria !== "object") {
|
|
93
|
+
errors.push({ path: `${path}.target.criteria`, message: "must be an object" });
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
errors.push({ path: `${path}.target.type`, message: 'must be "elementId" or "search"' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!isValidAssertionType(obj.assertionType)) {
|
|
100
|
+
errors.push({
|
|
101
|
+
path: `${path}.assertionType`,
|
|
102
|
+
message: `must be one of: ${VALID_ASSERTION_TYPES.join(", ")}`
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (!isValidSpecSource(obj.source)) {
|
|
106
|
+
errors.push({
|
|
107
|
+
path: `${path}.source`,
|
|
108
|
+
message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (typeof obj.reviewed !== "boolean") {
|
|
112
|
+
errors.push({ path: `${path}.reviewed`, message: "must be a boolean" });
|
|
113
|
+
}
|
|
114
|
+
if (typeof obj.enabled !== "boolean") {
|
|
115
|
+
errors.push({ path: `${path}.enabled`, message: "must be a boolean" });
|
|
116
|
+
}
|
|
117
|
+
if (obj.timeout !== void 0 && (typeof obj.timeout !== "number" || obj.timeout < 0)) {
|
|
118
|
+
errors.push({ path: `${path}.timeout`, message: "must be a non-negative number" });
|
|
119
|
+
}
|
|
120
|
+
return errors;
|
|
121
|
+
}
|
|
122
|
+
function validateSpecGroup(data, path = "group") {
|
|
123
|
+
const errors = [];
|
|
124
|
+
if (!data || typeof data !== "object") {
|
|
125
|
+
errors.push({ path, message: "must be an object" });
|
|
126
|
+
return errors;
|
|
127
|
+
}
|
|
128
|
+
const obj = data;
|
|
129
|
+
if (typeof obj.id !== "string" || obj.id.length === 0) {
|
|
130
|
+
errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
|
|
131
|
+
}
|
|
132
|
+
if (typeof obj.name !== "string") {
|
|
133
|
+
errors.push({ path: `${path}.name`, message: "must be a string" });
|
|
134
|
+
}
|
|
135
|
+
if (typeof obj.description !== "string") {
|
|
136
|
+
errors.push({ path: `${path}.description`, message: "must be a string" });
|
|
137
|
+
}
|
|
138
|
+
if (!isValidSpecCategory(obj.category)) {
|
|
139
|
+
errors.push({
|
|
140
|
+
path: `${path}.category`,
|
|
141
|
+
message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (!isValidSpecSource(obj.source)) {
|
|
145
|
+
errors.push({
|
|
146
|
+
path: `${path}.source`,
|
|
147
|
+
message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(obj.assertions)) {
|
|
151
|
+
errors.push({ path: `${path}.assertions`, message: "must be an array" });
|
|
152
|
+
} else {
|
|
153
|
+
for (let i = 0; i < obj.assertions.length; i++) {
|
|
154
|
+
errors.push(...validateSpecAssertion(obj.assertions[i], `${path}.assertions[${i}]`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return errors;
|
|
158
|
+
}
|
|
159
|
+
function validateSpecConfig(data) {
|
|
160
|
+
const errors = [];
|
|
161
|
+
if (!data || typeof data !== "object") {
|
|
162
|
+
return { valid: false, errors: [{ path: "", message: "must be an object" }] };
|
|
163
|
+
}
|
|
164
|
+
const obj = data;
|
|
165
|
+
if (obj.version !== SPEC_CONFIG_VERSION) {
|
|
166
|
+
errors.push({ path: "version", message: `must be "${SPEC_CONFIG_VERSION}"` });
|
|
167
|
+
}
|
|
168
|
+
if (obj.description !== void 0 && typeof obj.description !== "string") {
|
|
169
|
+
errors.push({ path: "description", message: "must be a string if provided" });
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(obj.groups)) {
|
|
172
|
+
errors.push({ path: "groups", message: "must be an array" });
|
|
173
|
+
} else {
|
|
174
|
+
for (let i = 0; i < obj.groups.length; i++) {
|
|
175
|
+
errors.push(...validateSpecGroup(obj.groups[i], `groups[${i}]`));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (obj.assertions !== void 0) {
|
|
179
|
+
if (!Array.isArray(obj.assertions)) {
|
|
180
|
+
errors.push({ path: "assertions", message: "must be an array if provided" });
|
|
181
|
+
} else {
|
|
182
|
+
for (let i = 0; i < obj.assertions.length; i++) {
|
|
183
|
+
errors.push(...validateSpecAssertion(obj.assertions[i], `assertions[${i}]`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (obj.metadata !== void 0 && (typeof obj.metadata !== "object" || obj.metadata === null)) {
|
|
188
|
+
errors.push({ path: "metadata", message: "must be an object if provided" });
|
|
189
|
+
}
|
|
190
|
+
return { valid: errors.length === 0, errors };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/specs/migration.ts
|
|
194
|
+
function coerceAssertionType(raw) {
|
|
195
|
+
if (VALID_ASSERTION_TYPES.includes(raw)) {
|
|
196
|
+
return raw;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function coerceCategory(raw) {
|
|
201
|
+
const valid = [
|
|
202
|
+
"element-presence",
|
|
203
|
+
"accessibility",
|
|
204
|
+
"form-validation",
|
|
205
|
+
"state-consistency",
|
|
206
|
+
"modal-dialog",
|
|
207
|
+
"navigation",
|
|
208
|
+
"cross-page-consistency",
|
|
209
|
+
"custom"
|
|
210
|
+
];
|
|
211
|
+
return valid.includes(raw) ? raw : "custom";
|
|
212
|
+
}
|
|
213
|
+
function coerceSeverity(raw) {
|
|
214
|
+
const valid = ["critical", "warning", "info"];
|
|
215
|
+
return valid.includes(raw) ? raw : "info";
|
|
216
|
+
}
|
|
217
|
+
function coerceSource(raw) {
|
|
218
|
+
if (raw === "auto" || raw === "manual" || raw === "ai-generated") return raw;
|
|
219
|
+
return "auto";
|
|
220
|
+
}
|
|
221
|
+
function migrateLegacyTarget(legacy) {
|
|
222
|
+
switch (legacy.type) {
|
|
223
|
+
case "elementId":
|
|
224
|
+
return {
|
|
225
|
+
type: "elementId",
|
|
226
|
+
elementId: legacy.elementId || "",
|
|
227
|
+
label: legacy.label
|
|
228
|
+
};
|
|
229
|
+
case "formId":
|
|
230
|
+
return {
|
|
231
|
+
type: "search",
|
|
232
|
+
criteria: {
|
|
233
|
+
idPattern: legacy.formId || "",
|
|
234
|
+
role: "form"
|
|
235
|
+
},
|
|
236
|
+
label: legacy.label
|
|
237
|
+
};
|
|
238
|
+
case "modalId":
|
|
239
|
+
return {
|
|
240
|
+
type: "search",
|
|
241
|
+
criteria: {
|
|
242
|
+
idPattern: legacy.modalId || "",
|
|
243
|
+
role: "dialog"
|
|
244
|
+
},
|
|
245
|
+
label: legacy.label
|
|
246
|
+
};
|
|
247
|
+
default:
|
|
248
|
+
return {
|
|
249
|
+
type: "elementId",
|
|
250
|
+
elementId: "",
|
|
251
|
+
label: legacy.label
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function migrateLegacyAssertion(legacy) {
|
|
256
|
+
const assertionType = coerceAssertionType(legacy.assertionType);
|
|
257
|
+
return {
|
|
258
|
+
id: legacy.id,
|
|
259
|
+
description: legacy.description,
|
|
260
|
+
category: coerceCategory(legacy.category),
|
|
261
|
+
severity: coerceSeverity(legacy.severity),
|
|
262
|
+
target: migrateLegacyTarget(legacy.target),
|
|
263
|
+
assertionType: assertionType ?? "exists",
|
|
264
|
+
expected: legacy.expected,
|
|
265
|
+
attributeName: legacy.attributeName,
|
|
266
|
+
source: coerceSource(legacy.source),
|
|
267
|
+
reviewed: legacy.reviewed,
|
|
268
|
+
enabled: legacy.enabled,
|
|
269
|
+
notes: legacy.notes
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function migrateFromTestGeneratorOutput(legacy) {
|
|
273
|
+
const groups = legacy.testSpecifications.map((spec) => ({
|
|
274
|
+
id: spec.id,
|
|
275
|
+
name: spec.name,
|
|
276
|
+
description: spec.description,
|
|
277
|
+
category: coerceCategory(spec.category),
|
|
278
|
+
assertions: spec.assertions.map(migrateLegacyAssertion),
|
|
279
|
+
stateId: spec.stateId,
|
|
280
|
+
transitionId: spec.transitionId,
|
|
281
|
+
source: coerceSource(spec.source)
|
|
282
|
+
}));
|
|
283
|
+
return {
|
|
284
|
+
version: "1.0.0",
|
|
285
|
+
description: legacy.generatorType ? `Migrated from ${legacy.generatorType} test generator output` : "Migrated from legacy test generator output",
|
|
286
|
+
groups,
|
|
287
|
+
metadata: {
|
|
288
|
+
createdAt: legacy.createdAt,
|
|
289
|
+
updatedAt: legacy.updatedAt,
|
|
290
|
+
...legacy.snapshotMetadata?.pageUrl ? { pageUrl: legacy.snapshotMetadata.pageUrl } : {},
|
|
291
|
+
...legacy.explorationMetadata?.targetUrl ? { pageUrl: legacy.explorationMetadata.targetUrl } : {}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/specs/store.ts
|
|
297
|
+
var SpecStore = class {
|
|
298
|
+
constructor() {
|
|
299
|
+
this.configs = /* @__PURE__ */ new Map();
|
|
300
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// CRUD — Config Level
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
load(specId, config) {
|
|
306
|
+
this.configs.set(specId, config);
|
|
307
|
+
this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
|
|
308
|
+
}
|
|
309
|
+
unload(specId) {
|
|
310
|
+
const existed = this.configs.delete(specId);
|
|
311
|
+
if (existed) {
|
|
312
|
+
this.emit({ type: "spec:unloaded", specId, timestamp: Date.now() });
|
|
313
|
+
}
|
|
314
|
+
return existed;
|
|
315
|
+
}
|
|
316
|
+
get(specId) {
|
|
317
|
+
return this.configs.get(specId);
|
|
318
|
+
}
|
|
319
|
+
has(specId) {
|
|
320
|
+
return this.configs.has(specId);
|
|
321
|
+
}
|
|
322
|
+
getIds() {
|
|
323
|
+
return Array.from(this.configs.keys());
|
|
324
|
+
}
|
|
325
|
+
getAll() {
|
|
326
|
+
return new Map(this.configs);
|
|
327
|
+
}
|
|
328
|
+
get count() {
|
|
329
|
+
return this.configs.size;
|
|
330
|
+
}
|
|
331
|
+
clear() {
|
|
332
|
+
this.configs.clear();
|
|
333
|
+
this.emit({ type: "spec:cleared", timestamp: Date.now() });
|
|
334
|
+
}
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// CRUD — Group Level
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
addGroup(specId, group) {
|
|
339
|
+
const config = this.configs.get(specId);
|
|
340
|
+
if (!config) return false;
|
|
341
|
+
config.groups.push(group);
|
|
342
|
+
this.emit({ type: "spec:group-added", specId, groupId: group.id, timestamp: Date.now() });
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
removeGroup(specId, groupId) {
|
|
346
|
+
const config = this.configs.get(specId);
|
|
347
|
+
if (!config) return false;
|
|
348
|
+
const idx = config.groups.findIndex((g) => g.id === groupId);
|
|
349
|
+
if (idx === -1) return false;
|
|
350
|
+
config.groups.splice(idx, 1);
|
|
351
|
+
this.emit({ type: "spec:group-removed", specId, groupId, timestamp: Date.now() });
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
getGroup(specId, groupId) {
|
|
355
|
+
const config = this.configs.get(specId);
|
|
356
|
+
if (!config) return void 0;
|
|
357
|
+
return config.groups.find((g) => g.id === groupId);
|
|
358
|
+
}
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// CRUD — Assertion Level
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
addAssertion(specId, groupId, assertion) {
|
|
363
|
+
const config = this.configs.get(specId);
|
|
364
|
+
if (!config) return false;
|
|
365
|
+
if (groupId) {
|
|
366
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
367
|
+
if (!group) return false;
|
|
368
|
+
group.assertions.push(assertion);
|
|
369
|
+
} else {
|
|
370
|
+
if (!config.assertions) config.assertions = [];
|
|
371
|
+
config.assertions.push(assertion);
|
|
372
|
+
}
|
|
373
|
+
this.emit({
|
|
374
|
+
type: "spec:assertion-added",
|
|
375
|
+
specId,
|
|
376
|
+
groupId: groupId ?? void 0,
|
|
377
|
+
assertionId: assertion.id,
|
|
378
|
+
timestamp: Date.now()
|
|
379
|
+
});
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
removeAssertion(specId, groupId, assertionId) {
|
|
383
|
+
const config = this.configs.get(specId);
|
|
384
|
+
if (!config) return false;
|
|
385
|
+
let removed = false;
|
|
386
|
+
if (groupId) {
|
|
387
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
388
|
+
if (group) {
|
|
389
|
+
const idx = group.assertions.findIndex((a) => a.id === assertionId);
|
|
390
|
+
if (idx !== -1) {
|
|
391
|
+
group.assertions.splice(idx, 1);
|
|
392
|
+
removed = true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} else if (config.assertions) {
|
|
396
|
+
const idx = config.assertions.findIndex((a) => a.id === assertionId);
|
|
397
|
+
if (idx !== -1) {
|
|
398
|
+
config.assertions.splice(idx, 1);
|
|
399
|
+
removed = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (removed) {
|
|
403
|
+
this.emit({
|
|
404
|
+
type: "spec:assertion-removed",
|
|
405
|
+
specId,
|
|
406
|
+
groupId: groupId ?? void 0,
|
|
407
|
+
assertionId,
|
|
408
|
+
timestamp: Date.now()
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return removed;
|
|
412
|
+
}
|
|
413
|
+
toggleAssertion(specId, groupId, assertionId) {
|
|
414
|
+
const assertion = this.findAssertion(specId, groupId, assertionId);
|
|
415
|
+
if (!assertion) return false;
|
|
416
|
+
assertion.enabled = !assertion.enabled;
|
|
417
|
+
this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
markReviewed(specId, groupId, assertionId) {
|
|
421
|
+
const assertion = this.findAssertion(specId, groupId, assertionId);
|
|
422
|
+
if (!assertion) return false;
|
|
423
|
+
assertion.reviewed = !assertion.reviewed;
|
|
424
|
+
this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Queries
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
getAllAssertions() {
|
|
431
|
+
const result = [];
|
|
432
|
+
for (const config of this.configs.values()) {
|
|
433
|
+
for (const group of config.groups) {
|
|
434
|
+
result.push(...group.assertions);
|
|
435
|
+
}
|
|
436
|
+
if (config.assertions) {
|
|
437
|
+
result.push(...config.assertions);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
filterAssertions(opts) {
|
|
443
|
+
return this.getAllAssertions().filter((a) => {
|
|
444
|
+
if (opts.categories && !opts.categories.includes(a.category)) return false;
|
|
445
|
+
if (opts.severities && !opts.severities.includes(a.severity)) return false;
|
|
446
|
+
if (opts.enabledOnly && !a.enabled) return false;
|
|
447
|
+
if (opts.reviewedOnly && !a.reviewed) return false;
|
|
448
|
+
return true;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Coverage
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
getCoverage(allElementIds) {
|
|
455
|
+
const specifiedIdSet = /* @__PURE__ */ new Set();
|
|
456
|
+
for (const assertion of this.getAllAssertions()) {
|
|
457
|
+
if (assertion.target.type === "elementId") {
|
|
458
|
+
specifiedIdSet.add(assertion.target.elementId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const specifiedIds = [];
|
|
462
|
+
const unspecifiedIds = [];
|
|
463
|
+
for (const id of allElementIds) {
|
|
464
|
+
if (specifiedIdSet.has(id)) {
|
|
465
|
+
specifiedIds.push(id);
|
|
466
|
+
} else {
|
|
467
|
+
unspecifiedIds.push(id);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const total = allElementIds.length;
|
|
471
|
+
return {
|
|
472
|
+
totalElements: total,
|
|
473
|
+
specifiedElements: specifiedIds.length,
|
|
474
|
+
coveragePercent: total > 0 ? specifiedIds.length / total * 100 : 0,
|
|
475
|
+
specifiedIds,
|
|
476
|
+
unspecifiedIds,
|
|
477
|
+
timestamp: Date.now()
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// Import / Export
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
importConfig(specId, config) {
|
|
484
|
+
const result = validateSpecConfig(config);
|
|
485
|
+
if (!result.valid) return false;
|
|
486
|
+
this.configs.set(specId, config);
|
|
487
|
+
this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
exportConfig(specId) {
|
|
491
|
+
const config = this.configs.get(specId);
|
|
492
|
+
if (!config) return void 0;
|
|
493
|
+
return {
|
|
494
|
+
...config,
|
|
495
|
+
version: SPEC_CONFIG_VERSION,
|
|
496
|
+
metadata: {
|
|
497
|
+
...config.metadata,
|
|
498
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// Events
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
on(listener) {
|
|
506
|
+
this.listeners.add(listener);
|
|
507
|
+
return () => {
|
|
508
|
+
this.listeners.delete(listener);
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
emit(event) {
|
|
512
|
+
for (const listener of this.listeners) {
|
|
513
|
+
try {
|
|
514
|
+
listener(event);
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// Private Helpers
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
findAssertion(specId, groupId, assertionId) {
|
|
523
|
+
const config = this.configs.get(specId);
|
|
524
|
+
if (!config) return void 0;
|
|
525
|
+
if (groupId) {
|
|
526
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
527
|
+
if (!group) return void 0;
|
|
528
|
+
return group.assertions.find((a) => a.id === assertionId);
|
|
529
|
+
}
|
|
530
|
+
return config.assertions?.find((a) => a.id === assertionId);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
var globalStore = null;
|
|
534
|
+
function getGlobalSpecStore() {
|
|
535
|
+
if (!globalStore) {
|
|
536
|
+
globalStore = new SpecStore();
|
|
537
|
+
}
|
|
538
|
+
return globalStore;
|
|
539
|
+
}
|
|
540
|
+
function resetGlobalSpecStore() {
|
|
541
|
+
globalStore = null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/ai/fuzzy-matcher.ts
|
|
545
|
+
var DEFAULT_FUZZY_CONFIG = {
|
|
546
|
+
threshold: 0.7,
|
|
547
|
+
levenshteinWeight: 0.3,
|
|
548
|
+
jaroWinklerWeight: 0.4,
|
|
549
|
+
ngramWeight: 0.3,
|
|
550
|
+
ngramSize: 2,
|
|
551
|
+
caseSensitive: false,
|
|
552
|
+
ignoreWhitespace: true
|
|
553
|
+
};
|
|
554
|
+
function levenshteinDistance(s1, s2) {
|
|
555
|
+
const len1 = s1.length;
|
|
556
|
+
const len2 = s2.length;
|
|
557
|
+
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
|
558
|
+
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
|
|
559
|
+
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
|
|
560
|
+
for (let i = 1; i <= len1; i++) {
|
|
561
|
+
for (let j = 1; j <= len2; j++) {
|
|
562
|
+
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
563
|
+
matrix[i][j] = Math.min(
|
|
564
|
+
matrix[i - 1][j] + 1,
|
|
565
|
+
// deletion
|
|
566
|
+
matrix[i][j - 1] + 1,
|
|
567
|
+
// insertion
|
|
568
|
+
matrix[i - 1][j - 1] + cost
|
|
569
|
+
// substitution
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return matrix[len1][len2];
|
|
574
|
+
}
|
|
575
|
+
function levenshteinSimilarity(s1, s2) {
|
|
576
|
+
if (s1.length === 0 && s2.length === 0) return 1;
|
|
577
|
+
if (s1.length === 0 || s2.length === 0) return 0;
|
|
578
|
+
const distance = levenshteinDistance(s1, s2);
|
|
579
|
+
const maxLength = Math.max(s1.length, s2.length);
|
|
580
|
+
return 1 - distance / maxLength;
|
|
581
|
+
}
|
|
582
|
+
function jaroSimilarity(s1, s2) {
|
|
583
|
+
if (s1.length === 0 && s2.length === 0) return 1;
|
|
584
|
+
if (s1.length === 0 || s2.length === 0) return 0;
|
|
585
|
+
const matchDistance = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
|
|
586
|
+
const s1Matches = new Array(s1.length).fill(false);
|
|
587
|
+
const s2Matches = new Array(s2.length).fill(false);
|
|
588
|
+
let matches = 0;
|
|
589
|
+
let transpositions = 0;
|
|
590
|
+
for (let i = 0; i < s1.length; i++) {
|
|
591
|
+
const start = Math.max(0, i - matchDistance);
|
|
592
|
+
const end = Math.min(i + matchDistance + 1, s2.length);
|
|
593
|
+
for (let j = start; j < end; j++) {
|
|
594
|
+
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
|
595
|
+
s1Matches[i] = true;
|
|
596
|
+
s2Matches[j] = true;
|
|
597
|
+
matches++;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (matches === 0) return 0;
|
|
602
|
+
let k = 0;
|
|
603
|
+
for (let i = 0; i < s1.length; i++) {
|
|
604
|
+
if (!s1Matches[i]) continue;
|
|
605
|
+
while (!s2Matches[k]) k++;
|
|
606
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
607
|
+
k++;
|
|
608
|
+
}
|
|
609
|
+
return (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
610
|
+
}
|
|
611
|
+
function jaroWinklerSimilarity(s1, s2, prefixScale = 0.1) {
|
|
612
|
+
const jaroSim = jaroSimilarity(s1, s2);
|
|
613
|
+
let prefixLength = 0;
|
|
614
|
+
const maxPrefix = Math.min(4, Math.min(s1.length, s2.length));
|
|
615
|
+
for (let i = 0; i < maxPrefix; i++) {
|
|
616
|
+
if (s1[i] === s2[i]) {
|
|
617
|
+
prefixLength++;
|
|
618
|
+
} else {
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return jaroSim + prefixLength * prefixScale * (1 - jaroSim);
|
|
623
|
+
}
|
|
624
|
+
function generateNgrams(s, n) {
|
|
625
|
+
const ngrams = /* @__PURE__ */ new Set();
|
|
626
|
+
if (s.length < n) {
|
|
627
|
+
ngrams.add(s);
|
|
628
|
+
return ngrams;
|
|
629
|
+
}
|
|
630
|
+
for (let i = 0; i <= s.length - n; i++) {
|
|
631
|
+
ngrams.add(s.substring(i, i + n));
|
|
632
|
+
}
|
|
633
|
+
return ngrams;
|
|
634
|
+
}
|
|
635
|
+
function ngramSimilarity(s1, s2, n = 2) {
|
|
636
|
+
if (s1.length === 0 && s2.length === 0) return 1;
|
|
637
|
+
if (s1.length === 0 || s2.length === 0) return 0;
|
|
638
|
+
const ngrams1 = generateNgrams(s1, n);
|
|
639
|
+
const ngrams2 = generateNgrams(s2, n);
|
|
640
|
+
let intersection = 0;
|
|
641
|
+
for (const ngram of ngrams1) {
|
|
642
|
+
if (ngrams2.has(ngram)) {
|
|
643
|
+
intersection++;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const union = ngrams1.size + ngrams2.size - intersection;
|
|
647
|
+
return union === 0 ? 0 : intersection / union;
|
|
648
|
+
}
|
|
649
|
+
function normalizeString(s, config = {}) {
|
|
650
|
+
let normalized = s;
|
|
651
|
+
if (!config.caseSensitive) {
|
|
652
|
+
normalized = normalized.toLowerCase();
|
|
653
|
+
}
|
|
654
|
+
if (config.ignoreWhitespace !== false) {
|
|
655
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
656
|
+
}
|
|
657
|
+
return normalized;
|
|
658
|
+
}
|
|
659
|
+
function fuzzyMatch(source, target, config = {}) {
|
|
660
|
+
const finalConfig = { ...DEFAULT_FUZZY_CONFIG, ...config };
|
|
661
|
+
const normalizedSource = normalizeString(source, finalConfig);
|
|
662
|
+
const normalizedTarget = normalizeString(target, finalConfig);
|
|
663
|
+
const levenshteinScore = levenshteinSimilarity(normalizedSource, normalizedTarget);
|
|
664
|
+
const jaroWinklerScore = jaroWinklerSimilarity(normalizedSource, normalizedTarget);
|
|
665
|
+
const ngramScore = ngramSimilarity(normalizedSource, normalizedTarget, finalConfig.ngramSize);
|
|
666
|
+
const similarity = levenshteinScore * finalConfig.levenshteinWeight + jaroWinklerScore * finalConfig.jaroWinklerWeight + ngramScore * finalConfig.ngramWeight;
|
|
667
|
+
return {
|
|
668
|
+
similarity,
|
|
669
|
+
isMatch: similarity >= finalConfig.threshold,
|
|
670
|
+
scores: {
|
|
671
|
+
levenshtein: levenshteinScore,
|
|
672
|
+
jaroWinkler: jaroWinklerScore,
|
|
673
|
+
ngram: ngramScore
|
|
674
|
+
},
|
|
675
|
+
normalizedSource,
|
|
676
|
+
normalizedTarget
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function fuzzyContains(source, target, config = {}) {
|
|
680
|
+
const finalConfig = { ...DEFAULT_FUZZY_CONFIG, ...config };
|
|
681
|
+
const normalizedSource = normalizeString(source, finalConfig);
|
|
682
|
+
const normalizedTarget = normalizeString(target, finalConfig);
|
|
683
|
+
if (normalizedSource.includes(normalizedTarget)) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
const sourceWords = normalizedSource.split(/\s+/);
|
|
687
|
+
const targetWords = normalizedTarget.split(/\s+/);
|
|
688
|
+
for (const targetWord of targetWords) {
|
|
689
|
+
const hasMatch = sourceWords.some((sourceWord) => {
|
|
690
|
+
const result = fuzzyMatch(sourceWord, targetWord, { ...finalConfig, threshold: 0.8 });
|
|
691
|
+
return result.isMatch;
|
|
692
|
+
});
|
|
693
|
+
if (!hasMatch) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
function wordSimilarity(s1, s2, config = {}) {
|
|
700
|
+
const finalConfig = { ...DEFAULT_FUZZY_CONFIG, ...config };
|
|
701
|
+
const words1 = normalizeString(s1, finalConfig).split(/\s+/);
|
|
702
|
+
const words2 = normalizeString(s2, finalConfig).split(/\s+/);
|
|
703
|
+
if (words1.length === 0 && words2.length === 0) return 1;
|
|
704
|
+
if (words1.length === 0 || words2.length === 0) return 0;
|
|
705
|
+
let totalSimilarity = 0;
|
|
706
|
+
let matchCount = 0;
|
|
707
|
+
for (const word1 of words1) {
|
|
708
|
+
let bestSim = 0;
|
|
709
|
+
for (const word2 of words2) {
|
|
710
|
+
const result = fuzzyMatch(word1, word2, finalConfig);
|
|
711
|
+
if (result.similarity > bestSim) {
|
|
712
|
+
bestSim = result.similarity;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
totalSimilarity += bestSim;
|
|
716
|
+
if (bestSim >= finalConfig.threshold) {
|
|
717
|
+
matchCount++;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const avgSimilarity = totalSimilarity / words1.length;
|
|
721
|
+
const matchRatio = matchCount / Math.max(words1.length, words2.length);
|
|
722
|
+
return avgSimilarity * 0.5 + matchRatio * 0.5;
|
|
723
|
+
}
|
|
724
|
+
function tokenize(s) {
|
|
725
|
+
return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").replace(/\s+/g, " ").trim().toLowerCase().split(" ").filter((token) => token.length > 0);
|
|
726
|
+
}
|
|
727
|
+
function tokenSimilarity(s1, s2) {
|
|
728
|
+
const tokens1 = tokenize(s1);
|
|
729
|
+
const tokens2 = tokenize(s2);
|
|
730
|
+
if (tokens1.length === 0 && tokens2.length === 0) return 1;
|
|
731
|
+
if (tokens1.length === 0 || tokens2.length === 0) return 0;
|
|
732
|
+
const set1 = new Set(tokens1);
|
|
733
|
+
const set2 = new Set(tokens2);
|
|
734
|
+
let intersection = 0;
|
|
735
|
+
for (const token of set1) {
|
|
736
|
+
if (set2.has(token)) {
|
|
737
|
+
intersection++;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const union = set1.size + set2.size - intersection;
|
|
741
|
+
return union === 0 ? 0 : intersection / union;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/ai/alias-generator.ts
|
|
745
|
+
var DEFAULT_ALIAS_CONFIG = {
|
|
746
|
+
includeText: true,
|
|
747
|
+
includeAriaLabel: true,
|
|
748
|
+
includePlaceholder: true,
|
|
749
|
+
includeTitle: true,
|
|
750
|
+
includeSynonyms: true,
|
|
751
|
+
maxAliases: 20,
|
|
752
|
+
minLength: 2,
|
|
753
|
+
maxLength: 50
|
|
754
|
+
};
|
|
755
|
+
var SYNONYMS = {
|
|
756
|
+
// Submit-related
|
|
757
|
+
submit: ["send", "go", "confirm", "ok", "apply", "save", "done", "finish"],
|
|
758
|
+
send: ["submit", "deliver", "post"],
|
|
759
|
+
save: ["submit", "store", "keep", "apply"],
|
|
760
|
+
cancel: ["close", "dismiss", "abort", "back", "exit", "quit", "nevermind"],
|
|
761
|
+
close: ["cancel", "dismiss", "exit", "x"],
|
|
762
|
+
delete: ["remove", "trash", "erase", "clear", "destroy"],
|
|
763
|
+
remove: ["delete", "clear", "discard"],
|
|
764
|
+
edit: ["modify", "change", "update", "alter"],
|
|
765
|
+
update: ["edit", "modify", "save", "refresh"],
|
|
766
|
+
add: ["create", "new", "plus", "insert"],
|
|
767
|
+
create: ["add", "new", "make"],
|
|
768
|
+
search: ["find", "lookup", "query", "filter"],
|
|
769
|
+
find: ["search", "locate", "lookup"],
|
|
770
|
+
login: ["signin", "sign in", "log in", "authenticate", "enter"],
|
|
771
|
+
logout: ["signout", "sign out", "log out", "exit"],
|
|
772
|
+
register: ["signup", "sign up", "join", "create account"],
|
|
773
|
+
next: ["continue", "forward", "proceed", "advance"],
|
|
774
|
+
previous: ["back", "backward", "return", "prior"],
|
|
775
|
+
back: ["previous", "return", "backward"],
|
|
776
|
+
start: ["begin", "launch", "initiate", "run", "execute"],
|
|
777
|
+
stop: ["end", "halt", "pause", "terminate"],
|
|
778
|
+
enable: ["activate", "turn on", "switch on"],
|
|
779
|
+
disable: ["deactivate", "turn off", "switch off"],
|
|
780
|
+
show: ["display", "reveal", "view", "open"],
|
|
781
|
+
hide: ["conceal", "collapse", "close"],
|
|
782
|
+
expand: ["open", "show", "unfold", "reveal"],
|
|
783
|
+
collapse: ["close", "hide", "fold", "minimize"],
|
|
784
|
+
yes: ["ok", "confirm", "agree", "accept"],
|
|
785
|
+
no: ["cancel", "decline", "reject", "deny"],
|
|
786
|
+
help: ["support", "assistance", "info", "information", "faq"],
|
|
787
|
+
settings: ["preferences", "options", "config", "configuration"],
|
|
788
|
+
profile: ["account", "user", "me"],
|
|
789
|
+
download: ["export", "save", "get"],
|
|
790
|
+
upload: ["import", "load", "attach"],
|
|
791
|
+
refresh: ["reload", "update", "sync"],
|
|
792
|
+
copy: ["duplicate", "clone"],
|
|
793
|
+
paste: ["insert"],
|
|
794
|
+
select: ["choose", "pick"],
|
|
795
|
+
toggle: ["switch", "flip"],
|
|
796
|
+
// Form fields
|
|
797
|
+
email: ["e-mail", "mail"],
|
|
798
|
+
password: ["pass", "pwd", "secret"],
|
|
799
|
+
username: ["user", "login", "account", "name"],
|
|
800
|
+
firstname: ["first name", "given name", "forename"],
|
|
801
|
+
lastname: ["last name", "surname", "family name"],
|
|
802
|
+
fullname: ["full name", "name", "complete name"],
|
|
803
|
+
phone: ["telephone", "tel", "mobile", "cell"],
|
|
804
|
+
address: ["location", "street"],
|
|
805
|
+
city: ["town"],
|
|
806
|
+
country: ["nation"],
|
|
807
|
+
zip: ["zipcode", "postal", "postal code", "postcode"],
|
|
808
|
+
// Navigation
|
|
809
|
+
home: ["main", "start", "dashboard"],
|
|
810
|
+
menu: ["navigation", "nav"],
|
|
811
|
+
sidebar: ["side bar", "side panel", "side menu"]
|
|
812
|
+
};
|
|
813
|
+
var ELEMENT_ACTION_WORDS = {
|
|
814
|
+
button: ["button", "btn", "click"],
|
|
815
|
+
input: ["input", "field", "textbox", "box"],
|
|
816
|
+
textarea: ["textarea", "text area", "text field", "multiline"],
|
|
817
|
+
select: ["select", "dropdown", "combo", "picker", "chooser"],
|
|
818
|
+
checkbox: ["checkbox", "check", "tick"],
|
|
819
|
+
radio: ["radio", "option", "choice"],
|
|
820
|
+
link: ["link", "anchor", "href"],
|
|
821
|
+
form: ["form"],
|
|
822
|
+
menu: ["menu"],
|
|
823
|
+
menuitem: ["menu item", "option"],
|
|
824
|
+
tab: ["tab"],
|
|
825
|
+
dialog: ["dialog", "modal", "popup"],
|
|
826
|
+
switch: ["switch", "toggle"],
|
|
827
|
+
slider: ["slider", "range"]
|
|
828
|
+
};
|
|
829
|
+
function normalizeAlias(text) {
|
|
830
|
+
return text.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
831
|
+
}
|
|
832
|
+
function extractWords(text) {
|
|
833
|
+
const tokens = tokenize(text);
|
|
834
|
+
return tokens.filter((t) => t.length >= 2);
|
|
835
|
+
}
|
|
836
|
+
function generateTextAliases(text, config) {
|
|
837
|
+
if (!text || !config.includeText) return [];
|
|
838
|
+
const aliases = [];
|
|
839
|
+
const normalized = normalizeAlias(text);
|
|
840
|
+
if (normalized.length >= config.minLength && normalized.length <= config.maxLength) {
|
|
841
|
+
aliases.push(normalized);
|
|
842
|
+
}
|
|
843
|
+
const words = extractWords(text);
|
|
844
|
+
for (const word of words) {
|
|
845
|
+
if (word.length >= config.minLength) {
|
|
846
|
+
aliases.push(word);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (words.length >= 2 && words.length <= 4) {
|
|
850
|
+
const twoWords = words.slice(0, 2).join(" ");
|
|
851
|
+
if (twoWords.length <= config.maxLength) {
|
|
852
|
+
aliases.push(twoWords);
|
|
853
|
+
}
|
|
854
|
+
if (words.length > 2) {
|
|
855
|
+
const lastTwo = words.slice(-2).join(" ");
|
|
856
|
+
if (lastTwo.length <= config.maxLength) {
|
|
857
|
+
aliases.push(lastTwo);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return aliases;
|
|
862
|
+
}
|
|
863
|
+
function generateSynonyms(aliases, config) {
|
|
864
|
+
if (!config.includeSynonyms) return [];
|
|
865
|
+
const synonyms = [];
|
|
866
|
+
for (const alias of aliases) {
|
|
867
|
+
const words = alias.toLowerCase().split(/\s+/);
|
|
868
|
+
for (const word of words) {
|
|
869
|
+
if (SYNONYMS[word]) {
|
|
870
|
+
for (const synonym of SYNONYMS[word]) {
|
|
871
|
+
const newAlias = alias.toLowerCase().replace(word, synonym);
|
|
872
|
+
if (newAlias !== alias.toLowerCase()) {
|
|
873
|
+
synonyms.push(newAlias);
|
|
874
|
+
}
|
|
875
|
+
if (synonym.length >= config.minLength) {
|
|
876
|
+
synonyms.push(synonym);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return synonyms;
|
|
883
|
+
}
|
|
884
|
+
function generateTypeAliases(elementType) {
|
|
885
|
+
const type = elementType.toLowerCase();
|
|
886
|
+
return ELEMENT_ACTION_WORDS[type] || [type];
|
|
887
|
+
}
|
|
888
|
+
function generateAliases(input, config = {}) {
|
|
889
|
+
const finalConfig = { ...DEFAULT_ALIAS_CONFIG, ...config };
|
|
890
|
+
const aliasSet = /* @__PURE__ */ new Set();
|
|
891
|
+
const addAlias = (alias) => {
|
|
892
|
+
const normalized = normalizeAlias(alias);
|
|
893
|
+
if (normalized.length >= finalConfig.minLength && normalized.length <= finalConfig.maxLength) {
|
|
894
|
+
aliasSet.add(normalized);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
const addAliases = (aliases2) => {
|
|
898
|
+
for (const alias of aliases2) {
|
|
899
|
+
addAlias(alias);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
if (finalConfig.includeText && input.textContent) {
|
|
903
|
+
addAliases(generateTextAliases(input.textContent, finalConfig));
|
|
904
|
+
}
|
|
905
|
+
if (finalConfig.includeAriaLabel && input.ariaLabel) {
|
|
906
|
+
addAliases(generateTextAliases(input.ariaLabel, finalConfig));
|
|
907
|
+
}
|
|
908
|
+
if (finalConfig.includeAriaLabel && input.ariaLabelledBy) {
|
|
909
|
+
addAliases(generateTextAliases(input.ariaLabelledBy, finalConfig));
|
|
910
|
+
}
|
|
911
|
+
if (finalConfig.includePlaceholder && input.placeholder) {
|
|
912
|
+
addAliases(generateTextAliases(input.placeholder, finalConfig));
|
|
913
|
+
}
|
|
914
|
+
if (finalConfig.includeTitle && input.title) {
|
|
915
|
+
addAliases(generateTextAliases(input.title, finalConfig));
|
|
916
|
+
}
|
|
917
|
+
if (input.labelText) {
|
|
918
|
+
addAliases(generateTextAliases(input.labelText, finalConfig));
|
|
919
|
+
}
|
|
920
|
+
if (input.id) {
|
|
921
|
+
addAliases(extractWords(input.id));
|
|
922
|
+
}
|
|
923
|
+
if (input.name) {
|
|
924
|
+
addAliases(extractWords(input.name));
|
|
925
|
+
}
|
|
926
|
+
if (input.value && (input.elementType === "button" || input.inputType === "submit" || input.inputType === "button")) {
|
|
927
|
+
addAliases(generateTextAliases(input.value, finalConfig));
|
|
928
|
+
}
|
|
929
|
+
if (input.elementType) {
|
|
930
|
+
addAliases(generateTypeAliases(input.elementType));
|
|
931
|
+
}
|
|
932
|
+
if (input.inputType) {
|
|
933
|
+
addAlias(input.inputType);
|
|
934
|
+
if (input.inputType === "email") {
|
|
935
|
+
addAliases(["email", "e-mail", "email address"]);
|
|
936
|
+
} else if (input.inputType === "password") {
|
|
937
|
+
addAliases(["password", "pass", "pwd"]);
|
|
938
|
+
} else if (input.inputType === "tel") {
|
|
939
|
+
addAliases(["phone", "telephone", "mobile"]);
|
|
940
|
+
} else if (input.inputType === "url") {
|
|
941
|
+
addAliases(["url", "website", "link", "address"]);
|
|
942
|
+
} else if (input.inputType === "search") {
|
|
943
|
+
addAliases(["search", "find", "query"]);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (finalConfig.includeSynonyms) {
|
|
947
|
+
const currentAliases = Array.from(aliasSet);
|
|
948
|
+
addAliases(generateSynonyms(currentAliases, finalConfig));
|
|
949
|
+
}
|
|
950
|
+
let aliases = Array.from(aliasSet);
|
|
951
|
+
aliases.sort((a, b) => a.length - b.length);
|
|
952
|
+
if (aliases.length > finalConfig.maxAliases) {
|
|
953
|
+
aliases = aliases.slice(0, finalConfig.maxAliases);
|
|
954
|
+
}
|
|
955
|
+
return aliases;
|
|
956
|
+
}
|
|
957
|
+
function generateDescription(input) {
|
|
958
|
+
const parts = [];
|
|
959
|
+
let name = input.ariaLabel || input.labelText || input.textContent || input.placeholder || input.title || input.id || input.name;
|
|
960
|
+
if (name) {
|
|
961
|
+
name = name.trim();
|
|
962
|
+
if (name.length > 30) {
|
|
963
|
+
name = name.substring(0, 27) + "...";
|
|
964
|
+
}
|
|
965
|
+
parts.push(`"${name}"`);
|
|
966
|
+
}
|
|
967
|
+
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
968
|
+
input.elementType || "element"
|
|
969
|
+
];
|
|
970
|
+
parts.push(typeWords[0]);
|
|
971
|
+
if (input.inputType && input.inputType !== "text") {
|
|
972
|
+
parts.push(`(${input.inputType})`);
|
|
973
|
+
}
|
|
974
|
+
return parts.join(" ");
|
|
975
|
+
}
|
|
976
|
+
var CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
977
|
+
"heading",
|
|
978
|
+
"paragraph",
|
|
979
|
+
"list-item",
|
|
980
|
+
"table-cell",
|
|
981
|
+
"table-header",
|
|
982
|
+
"label",
|
|
983
|
+
"caption",
|
|
984
|
+
"blockquote",
|
|
985
|
+
"code-block",
|
|
986
|
+
"badge",
|
|
987
|
+
"status-message",
|
|
988
|
+
"metric-value",
|
|
989
|
+
"description-text",
|
|
990
|
+
"nav-text",
|
|
991
|
+
"content-generic"
|
|
992
|
+
]);
|
|
993
|
+
function generatePurpose(input) {
|
|
994
|
+
const text = (input.textContent || input.ariaLabel || input.title || "").toLowerCase();
|
|
995
|
+
const type = input.elementType?.toLowerCase() || "";
|
|
996
|
+
const inputType = input.inputType?.toLowerCase() || "";
|
|
997
|
+
if (CONTENT_TYPES.has(type)) {
|
|
998
|
+
switch (type) {
|
|
999
|
+
case "heading":
|
|
1000
|
+
return "Section heading";
|
|
1001
|
+
case "paragraph":
|
|
1002
|
+
return "Body text content";
|
|
1003
|
+
case "list-item":
|
|
1004
|
+
return "List item";
|
|
1005
|
+
case "table-cell":
|
|
1006
|
+
return "Table data cell";
|
|
1007
|
+
case "table-header":
|
|
1008
|
+
return "Table column header";
|
|
1009
|
+
case "label":
|
|
1010
|
+
return "Field label or definition term";
|
|
1011
|
+
case "caption":
|
|
1012
|
+
return "Figure or table caption";
|
|
1013
|
+
case "blockquote":
|
|
1014
|
+
return "Quoted content";
|
|
1015
|
+
case "code-block":
|
|
1016
|
+
return "Code or preformatted text";
|
|
1017
|
+
case "badge":
|
|
1018
|
+
return "Status badge or tag";
|
|
1019
|
+
case "status-message":
|
|
1020
|
+
return "Dynamic status indicator";
|
|
1021
|
+
case "metric-value":
|
|
1022
|
+
return "Metric or statistic value";
|
|
1023
|
+
case "description-text":
|
|
1024
|
+
return "Description or definition";
|
|
1025
|
+
case "nav-text":
|
|
1026
|
+
return "Navigation label";
|
|
1027
|
+
case "content-generic":
|
|
1028
|
+
return "Text content";
|
|
1029
|
+
default:
|
|
1030
|
+
return "Static content";
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (type === "button" || inputType === "submit") {
|
|
1034
|
+
if (text.match(/submit|send|save|confirm|ok|done|finish|apply/)) {
|
|
1035
|
+
return "Submits the form";
|
|
1036
|
+
}
|
|
1037
|
+
if (text.match(/cancel|close|dismiss|back|exit/)) {
|
|
1038
|
+
return "Cancels or closes the current action";
|
|
1039
|
+
}
|
|
1040
|
+
if (text.match(/delete|remove|trash|clear/)) {
|
|
1041
|
+
return "Deletes or removes an item";
|
|
1042
|
+
}
|
|
1043
|
+
if (text.match(/edit|modify|change|update/)) {
|
|
1044
|
+
return "Edits or modifies an item";
|
|
1045
|
+
}
|
|
1046
|
+
if (text.match(/add|create|new|\+/)) {
|
|
1047
|
+
return "Creates or adds a new item";
|
|
1048
|
+
}
|
|
1049
|
+
if (text.match(/search|find|lookup/)) {
|
|
1050
|
+
return "Performs a search";
|
|
1051
|
+
}
|
|
1052
|
+
if (text.match(/login|sign.?in/)) {
|
|
1053
|
+
return "Signs the user in";
|
|
1054
|
+
}
|
|
1055
|
+
if (text.match(/logout|sign.?out/)) {
|
|
1056
|
+
return "Signs the user out";
|
|
1057
|
+
}
|
|
1058
|
+
if (text.match(/register|sign.?up|join/)) {
|
|
1059
|
+
return "Creates a new account";
|
|
1060
|
+
}
|
|
1061
|
+
if (text.match(/next|continue|proceed/)) {
|
|
1062
|
+
return "Proceeds to the next step";
|
|
1063
|
+
}
|
|
1064
|
+
if (text.match(/previous|back|return/)) {
|
|
1065
|
+
return "Returns to the previous step";
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (type === "input" || type === "textarea") {
|
|
1069
|
+
if (inputType === "email") return "Accepts email address input";
|
|
1070
|
+
if (inputType === "password") return "Accepts password input";
|
|
1071
|
+
if (inputType === "search") return "Accepts search query input";
|
|
1072
|
+
if (inputType === "tel") return "Accepts phone number input";
|
|
1073
|
+
if (inputType === "url") return "Accepts URL input";
|
|
1074
|
+
if (inputType === "number") return "Accepts numeric input";
|
|
1075
|
+
if (inputType === "date") return "Accepts date input";
|
|
1076
|
+
if (inputType === "file") return "Accepts file upload";
|
|
1077
|
+
}
|
|
1078
|
+
if (type === "checkbox") {
|
|
1079
|
+
return "Toggles an option on or off";
|
|
1080
|
+
}
|
|
1081
|
+
if (type === "radio") {
|
|
1082
|
+
return "Selects one option from a group";
|
|
1083
|
+
}
|
|
1084
|
+
if (type === "select") {
|
|
1085
|
+
return "Selects an option from a dropdown";
|
|
1086
|
+
}
|
|
1087
|
+
if (type === "link") {
|
|
1088
|
+
return "Navigates to another page";
|
|
1089
|
+
}
|
|
1090
|
+
return void 0;
|
|
1091
|
+
}
|
|
1092
|
+
function generateSuggestedActions(input) {
|
|
1093
|
+
const type = input.elementType?.toLowerCase() || "";
|
|
1094
|
+
const inputType = input.inputType?.toLowerCase() || "";
|
|
1095
|
+
const text = (input.textContent || input.ariaLabel || "").toLowerCase();
|
|
1096
|
+
const actions = [];
|
|
1097
|
+
if (CONTENT_TYPES.has(type)) {
|
|
1098
|
+
actions.push("read text content", "verify text matches expected");
|
|
1099
|
+
return actions;
|
|
1100
|
+
}
|
|
1101
|
+
switch (type) {
|
|
1102
|
+
case "button":
|
|
1103
|
+
actions.push(`click "${text || "this button"}"`);
|
|
1104
|
+
break;
|
|
1105
|
+
case "input":
|
|
1106
|
+
if (inputType === "checkbox") {
|
|
1107
|
+
actions.push("check to enable", "uncheck to disable");
|
|
1108
|
+
} else if (inputType === "radio") {
|
|
1109
|
+
actions.push("select this option");
|
|
1110
|
+
} else {
|
|
1111
|
+
actions.push(`type into "${text || "this field"}"`);
|
|
1112
|
+
actions.push("clear the field");
|
|
1113
|
+
}
|
|
1114
|
+
break;
|
|
1115
|
+
case "textarea":
|
|
1116
|
+
actions.push(`type into "${text || "this text area"}"`);
|
|
1117
|
+
actions.push("clear the content");
|
|
1118
|
+
break;
|
|
1119
|
+
case "select":
|
|
1120
|
+
actions.push(`select an option from "${text || "this dropdown"}"`);
|
|
1121
|
+
break;
|
|
1122
|
+
case "checkbox":
|
|
1123
|
+
actions.push("check to enable", "uncheck to disable");
|
|
1124
|
+
break;
|
|
1125
|
+
case "radio":
|
|
1126
|
+
actions.push("select this option");
|
|
1127
|
+
break;
|
|
1128
|
+
case "link":
|
|
1129
|
+
actions.push(`click to navigate to "${text || "the linked page"}"`);
|
|
1130
|
+
break;
|
|
1131
|
+
case "switch":
|
|
1132
|
+
actions.push("toggle on", "toggle off");
|
|
1133
|
+
break;
|
|
1134
|
+
default:
|
|
1135
|
+
actions.push("click");
|
|
1136
|
+
}
|
|
1137
|
+
return actions;
|
|
1138
|
+
}
|
|
1139
|
+
function areSynonyms(word1, word2) {
|
|
1140
|
+
const w1 = word1.toLowerCase().trim();
|
|
1141
|
+
const w2 = word2.toLowerCase().trim();
|
|
1142
|
+
if (w1 === w2) return true;
|
|
1143
|
+
const synonyms1 = SYNONYMS[w1] || [];
|
|
1144
|
+
const synonyms2 = SYNONYMS[w2] || [];
|
|
1145
|
+
return synonyms1.includes(w2) || synonyms2.includes(w1);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/annotations/types.ts
|
|
1149
|
+
var ANNOTATION_CONFIG_VERSION = "1.0.0";
|
|
1150
|
+
|
|
1151
|
+
// src/annotations/store.ts
|
|
1152
|
+
var AnnotationStore = class {
|
|
1153
|
+
constructor() {
|
|
1154
|
+
this.store = /* @__PURE__ */ new Map();
|
|
1155
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Get an annotation by element ID.
|
|
1159
|
+
*/
|
|
1160
|
+
get(elementId) {
|
|
1161
|
+
return this.store.get(elementId);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Get all annotations as a record.
|
|
1165
|
+
*/
|
|
1166
|
+
getAll() {
|
|
1167
|
+
const result = {};
|
|
1168
|
+
for (const [id, annotation] of this.store) {
|
|
1169
|
+
result[id] = annotation;
|
|
1170
|
+
}
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Set an annotation for an element. Auto-sets `updatedAt`.
|
|
1175
|
+
*/
|
|
1176
|
+
set(elementId, annotation) {
|
|
1177
|
+
const updated = {
|
|
1178
|
+
...annotation,
|
|
1179
|
+
updatedAt: Date.now()
|
|
1180
|
+
};
|
|
1181
|
+
this.store.set(elementId, updated);
|
|
1182
|
+
this.emit({
|
|
1183
|
+
type: "annotation:set",
|
|
1184
|
+
elementId,
|
|
1185
|
+
annotation: updated,
|
|
1186
|
+
timestamp: Date.now()
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Delete an annotation by element ID.
|
|
1191
|
+
*
|
|
1192
|
+
* @returns true if the annotation existed and was deleted
|
|
1193
|
+
*/
|
|
1194
|
+
delete(elementId) {
|
|
1195
|
+
const existed = this.store.delete(elementId);
|
|
1196
|
+
if (existed) {
|
|
1197
|
+
this.emit({
|
|
1198
|
+
type: "annotation:deleted",
|
|
1199
|
+
elementId,
|
|
1200
|
+
timestamp: Date.now()
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
return existed;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Check if an annotation exists for an element.
|
|
1207
|
+
*/
|
|
1208
|
+
has(elementId) {
|
|
1209
|
+
return this.store.has(elementId);
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Get the number of stored annotations.
|
|
1213
|
+
*/
|
|
1214
|
+
get count() {
|
|
1215
|
+
return this.store.size;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Clear all annotations.
|
|
1219
|
+
*/
|
|
1220
|
+
clear() {
|
|
1221
|
+
this.store.clear();
|
|
1222
|
+
this.emit({
|
|
1223
|
+
type: "annotation:cleared",
|
|
1224
|
+
timestamp: Date.now()
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Import annotations from a config object.
|
|
1229
|
+
*
|
|
1230
|
+
* Merges with existing annotations (new values overwrite per element ID).
|
|
1231
|
+
*
|
|
1232
|
+
* @returns Number of annotations imported
|
|
1233
|
+
*
|
|
1234
|
+
* @example
|
|
1235
|
+
* ```ts
|
|
1236
|
+
* const config: AnnotationConfig = {
|
|
1237
|
+
* version: '1.0.0',
|
|
1238
|
+
* annotations: {
|
|
1239
|
+
* 'btn-1': { description: 'Submit button', tags: ['form'] },
|
|
1240
|
+
* 'input-1': { description: 'Name field' },
|
|
1241
|
+
* },
|
|
1242
|
+
* };
|
|
1243
|
+
* const count = store.importConfig(config); // 2
|
|
1244
|
+
* ```
|
|
1245
|
+
*/
|
|
1246
|
+
importConfig(config) {
|
|
1247
|
+
let count = 0;
|
|
1248
|
+
for (const [id, annotation] of Object.entries(config.annotations)) {
|
|
1249
|
+
this.store.set(id, {
|
|
1250
|
+
...annotation,
|
|
1251
|
+
updatedAt: annotation.updatedAt ?? Date.now()
|
|
1252
|
+
});
|
|
1253
|
+
count++;
|
|
1254
|
+
}
|
|
1255
|
+
this.emit({
|
|
1256
|
+
type: "annotation:imported",
|
|
1257
|
+
count,
|
|
1258
|
+
timestamp: Date.now()
|
|
1259
|
+
});
|
|
1260
|
+
return count;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Export all annotations as a config object.
|
|
1264
|
+
*
|
|
1265
|
+
* The returned object can be serialized to JSON and saved to a file,
|
|
1266
|
+
* then later re-imported with {@link importConfig}.
|
|
1267
|
+
*
|
|
1268
|
+
* @param metadata - Optional metadata to include (appName, description, etc.)
|
|
1269
|
+
* @returns AnnotationConfig with all current annotations
|
|
1270
|
+
*
|
|
1271
|
+
* @example
|
|
1272
|
+
* ```ts
|
|
1273
|
+
* const config = store.exportConfig({ appName: 'MyApp' });
|
|
1274
|
+
* // config.version === '1.0.0'
|
|
1275
|
+
* // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
|
|
1276
|
+
* // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
|
|
1277
|
+
*
|
|
1278
|
+
* // Save to file
|
|
1279
|
+
* fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
|
|
1280
|
+
* ```
|
|
1281
|
+
*/
|
|
1282
|
+
exportConfig(metadata) {
|
|
1283
|
+
return {
|
|
1284
|
+
version: ANNOTATION_CONFIG_VERSION,
|
|
1285
|
+
annotations: this.getAll(),
|
|
1286
|
+
metadata: {
|
|
1287
|
+
...metadata,
|
|
1288
|
+
exportedAt: Date.now()
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Compute annotation coverage against a set of known element IDs.
|
|
1294
|
+
*
|
|
1295
|
+
* Compares the store's annotations against the provided list of element IDs
|
|
1296
|
+
* to determine what percentage of elements have been annotated.
|
|
1297
|
+
*
|
|
1298
|
+
* @param allElementIds - Array of all known element IDs in the UI
|
|
1299
|
+
* @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
|
|
1300
|
+
*
|
|
1301
|
+
* @example
|
|
1302
|
+
* ```ts
|
|
1303
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
1304
|
+
* store.set('input-1', { description: 'Name' });
|
|
1305
|
+
*
|
|
1306
|
+
* const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
|
|
1307
|
+
* // coverage.totalElements === 4
|
|
1308
|
+
* // coverage.annotatedElements === 2
|
|
1309
|
+
* // coverage.coveragePercent === 50
|
|
1310
|
+
* // coverage.annotatedIds === ['btn-1', 'input-1']
|
|
1311
|
+
* // coverage.unannotatedIds === ['input-2', 'link-1']
|
|
1312
|
+
* ```
|
|
1313
|
+
*/
|
|
1314
|
+
getCoverage(allElementIds) {
|
|
1315
|
+
const annotatedIds = [];
|
|
1316
|
+
const unannotatedIds = [];
|
|
1317
|
+
for (const id of allElementIds) {
|
|
1318
|
+
if (this.store.has(id)) {
|
|
1319
|
+
annotatedIds.push(id);
|
|
1320
|
+
} else {
|
|
1321
|
+
unannotatedIds.push(id);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const total = allElementIds.length;
|
|
1325
|
+
return {
|
|
1326
|
+
totalElements: total,
|
|
1327
|
+
annotatedElements: annotatedIds.length,
|
|
1328
|
+
coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
|
|
1329
|
+
annotatedIds,
|
|
1330
|
+
unannotatedIds,
|
|
1331
|
+
timestamp: Date.now()
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Subscribe to annotation events.
|
|
1336
|
+
*
|
|
1337
|
+
* The listener is called whenever annotations are set, deleted, imported,
|
|
1338
|
+
* or cleared. Returns an unsubscribe function to stop listening.
|
|
1339
|
+
*
|
|
1340
|
+
* @param listener - Callback function receiving {@link AnnotationEvent} objects
|
|
1341
|
+
* @returns Unsubscribe function - call it to remove the listener
|
|
1342
|
+
*
|
|
1343
|
+
* @example
|
|
1344
|
+
* ```ts
|
|
1345
|
+
* const unsubscribe = store.on((event) => {
|
|
1346
|
+
* if (event.type === 'annotation:set') {
|
|
1347
|
+
* console.log(`Element ${event.elementId} annotated:`, event.annotation);
|
|
1348
|
+
* }
|
|
1349
|
+
* });
|
|
1350
|
+
*
|
|
1351
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
1352
|
+
* // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
|
|
1353
|
+
*
|
|
1354
|
+
* unsubscribe(); // Stop listening
|
|
1355
|
+
* ```
|
|
1356
|
+
*/
|
|
1357
|
+
on(listener) {
|
|
1358
|
+
this.listeners.add(listener);
|
|
1359
|
+
return () => {
|
|
1360
|
+
this.listeners.delete(listener);
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Emit an event to all listeners.
|
|
1365
|
+
*/
|
|
1366
|
+
emit(event) {
|
|
1367
|
+
for (const listener of this.listeners) {
|
|
1368
|
+
try {
|
|
1369
|
+
listener(event);
|
|
1370
|
+
} catch {
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
var globalStore2 = null;
|
|
1376
|
+
function getGlobalAnnotationStore() {
|
|
1377
|
+
if (!globalStore2) {
|
|
1378
|
+
globalStore2 = new AnnotationStore();
|
|
1379
|
+
}
|
|
1380
|
+
return globalStore2;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/ai/search-engine.ts
|
|
1384
|
+
var DEFAULT_SEARCH_CONFIG = {
|
|
1385
|
+
fuzzyThreshold: 0.7,
|
|
1386
|
+
textWeight: 0.35,
|
|
1387
|
+
accessibilityWeight: 0.25,
|
|
1388
|
+
roleWeight: 0.15,
|
|
1389
|
+
spatialWeight: 0.1,
|
|
1390
|
+
aliasWeight: 0.15,
|
|
1391
|
+
maxResults: 20,
|
|
1392
|
+
includeHidden: false
|
|
1393
|
+
};
|
|
1394
|
+
var SearchEngine = class {
|
|
1395
|
+
// Cache valid for 100ms
|
|
1396
|
+
constructor(config = {}) {
|
|
1397
|
+
this.cachedElements = [];
|
|
1398
|
+
this.cacheTimestamp = 0;
|
|
1399
|
+
this.cacheValidityMs = 100;
|
|
1400
|
+
this.config = { ...DEFAULT_SEARCH_CONFIG, ...config };
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Update cached elements from various sources
|
|
1404
|
+
*/
|
|
1405
|
+
updateElements(elements, getState) {
|
|
1406
|
+
this.cachedElements = elements.map((el) => this.toSearchable(el, getState));
|
|
1407
|
+
this.cacheTimestamp = Date.now();
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Convert an element to searchable format
|
|
1411
|
+
*/
|
|
1412
|
+
toSearchable(element, getState) {
|
|
1413
|
+
let state;
|
|
1414
|
+
let textContent;
|
|
1415
|
+
let tagName;
|
|
1416
|
+
let role;
|
|
1417
|
+
let ariaLabel;
|
|
1418
|
+
let placeholder;
|
|
1419
|
+
let title;
|
|
1420
|
+
let labelText;
|
|
1421
|
+
let value;
|
|
1422
|
+
if ("getState" in element && typeof element.getState === "function") {
|
|
1423
|
+
state = getState ? getState(element) : element.getState();
|
|
1424
|
+
textContent = state.textContent || void 0;
|
|
1425
|
+
try {
|
|
1426
|
+
tagName = element.element.tagName.toLowerCase();
|
|
1427
|
+
} catch {
|
|
1428
|
+
tagName = element.type || "unknown";
|
|
1429
|
+
}
|
|
1430
|
+
try {
|
|
1431
|
+
role = element.element.getAttribute("role") || void 0;
|
|
1432
|
+
ariaLabel = element.element.getAttribute("aria-label") || void 0;
|
|
1433
|
+
placeholder = element.element.getAttribute("placeholder") || void 0;
|
|
1434
|
+
title = element.element.getAttribute("title") || void 0;
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
if (!ariaLabel && element.label) {
|
|
1438
|
+
ariaLabel = element.label;
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
if (element.element.id) {
|
|
1442
|
+
const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
|
|
1443
|
+
labelText = labelEl?.textContent?.trim() || void 0;
|
|
1444
|
+
}
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
if (!labelText && element.label) {
|
|
1448
|
+
labelText = element.label;
|
|
1449
|
+
}
|
|
1450
|
+
if (!textContent && element.label) {
|
|
1451
|
+
textContent = element.label;
|
|
1452
|
+
}
|
|
1453
|
+
try {
|
|
1454
|
+
if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
|
|
1455
|
+
value = element.element.value || void 0;
|
|
1456
|
+
}
|
|
1457
|
+
} catch {
|
|
1458
|
+
value = state.value || void 0;
|
|
1459
|
+
}
|
|
1460
|
+
} else {
|
|
1461
|
+
const discovered = element;
|
|
1462
|
+
state = discovered.state;
|
|
1463
|
+
textContent = state.textContent || void 0;
|
|
1464
|
+
tagName = discovered.tagName;
|
|
1465
|
+
role = discovered.role || void 0;
|
|
1466
|
+
ariaLabel = discovered.accessibleName || void 0;
|
|
1467
|
+
if (!labelText && element.label) {
|
|
1468
|
+
labelText = element.label;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
let aliases = generateAliases({
|
|
1472
|
+
textContent,
|
|
1473
|
+
ariaLabel,
|
|
1474
|
+
placeholder,
|
|
1475
|
+
title,
|
|
1476
|
+
elementType: element.type,
|
|
1477
|
+
id: element.id,
|
|
1478
|
+
labelText,
|
|
1479
|
+
value
|
|
1480
|
+
});
|
|
1481
|
+
if ("aliases" in element && Array.isArray(element.aliases) && element.aliases.length > 0) {
|
|
1482
|
+
const aliasSet = /* @__PURE__ */ new Set([
|
|
1483
|
+
...aliases,
|
|
1484
|
+
...element.aliases.map((a) => a.toLowerCase())
|
|
1485
|
+
]);
|
|
1486
|
+
aliases = [...aliasSet];
|
|
1487
|
+
}
|
|
1488
|
+
let description = generateDescription({
|
|
1489
|
+
textContent,
|
|
1490
|
+
ariaLabel,
|
|
1491
|
+
placeholder,
|
|
1492
|
+
title,
|
|
1493
|
+
elementType: element.type,
|
|
1494
|
+
id: element.id,
|
|
1495
|
+
labelText
|
|
1496
|
+
});
|
|
1497
|
+
if (!description && "description" in element && element.description) {
|
|
1498
|
+
description = element.description;
|
|
1499
|
+
}
|
|
1500
|
+
const annotation = getGlobalAnnotationStore().get(element.id);
|
|
1501
|
+
if (annotation) {
|
|
1502
|
+
if (annotation.description) {
|
|
1503
|
+
description = annotation.description;
|
|
1504
|
+
}
|
|
1505
|
+
if (annotation.tags && annotation.tags.length > 0) {
|
|
1506
|
+
const tagSet = /* @__PURE__ */ new Set([...aliases, ...annotation.tags.map((t) => t.toLowerCase())]);
|
|
1507
|
+
aliases = [...tagSet];
|
|
1508
|
+
}
|
|
1509
|
+
if (annotation.notes) {
|
|
1510
|
+
aliases.push(annotation.notes.toLowerCase());
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return {
|
|
1514
|
+
id: element.id,
|
|
1515
|
+
element,
|
|
1516
|
+
state,
|
|
1517
|
+
textContent,
|
|
1518
|
+
ariaLabel,
|
|
1519
|
+
placeholder,
|
|
1520
|
+
title,
|
|
1521
|
+
role,
|
|
1522
|
+
tagName,
|
|
1523
|
+
type: element.type,
|
|
1524
|
+
aliases,
|
|
1525
|
+
description,
|
|
1526
|
+
rect: state.rect,
|
|
1527
|
+
labelText,
|
|
1528
|
+
value
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Search for elements matching the criteria
|
|
1533
|
+
*/
|
|
1534
|
+
search(criteria, elements) {
|
|
1535
|
+
const startTime = performance.now();
|
|
1536
|
+
if (elements) {
|
|
1537
|
+
this.updateElements(elements);
|
|
1538
|
+
}
|
|
1539
|
+
let searchableElements = this.cachedElements;
|
|
1540
|
+
if (!this.config.includeHidden && !criteria.fuzzy) {
|
|
1541
|
+
searchableElements = searchableElements.filter((el) => el.state.visible);
|
|
1542
|
+
}
|
|
1543
|
+
const results = [];
|
|
1544
|
+
for (const searchable of searchableElements) {
|
|
1545
|
+
const result = this.scoreElement(searchable, criteria);
|
|
1546
|
+
if (result.confidence >= (criteria.fuzzyThreshold ?? this.config.fuzzyThreshold)) {
|
|
1547
|
+
results.push(result);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
1551
|
+
const limitedResults = results.slice(0, this.config.maxResults);
|
|
1552
|
+
return {
|
|
1553
|
+
results: limitedResults,
|
|
1554
|
+
bestMatch: limitedResults.length > 0 ? limitedResults[0] : null,
|
|
1555
|
+
scannedCount: searchableElements.length,
|
|
1556
|
+
durationMs: performance.now() - startTime,
|
|
1557
|
+
criteria,
|
|
1558
|
+
timestamp: Date.now()
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Find the best matching element
|
|
1563
|
+
*/
|
|
1564
|
+
findBest(criteria, elements) {
|
|
1565
|
+
const response = this.search(criteria, elements);
|
|
1566
|
+
return response.bestMatch;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Find elements by text content
|
|
1570
|
+
*/
|
|
1571
|
+
findByText(text, fuzzy = true, elements) {
|
|
1572
|
+
return this.search({ text, fuzzy }, elements).results;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Find elements by role
|
|
1576
|
+
*/
|
|
1577
|
+
findByRole(role, name, elements) {
|
|
1578
|
+
const criteria = { role };
|
|
1579
|
+
if (name) {
|
|
1580
|
+
criteria.accessibleName = name;
|
|
1581
|
+
}
|
|
1582
|
+
return this.search(criteria, elements).results;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Find elements by accessible name
|
|
1586
|
+
*/
|
|
1587
|
+
findByAccessibleName(name, elements) {
|
|
1588
|
+
return this.search({ accessibleName: name, fuzzy: true }, elements).results;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Find elements near another element
|
|
1592
|
+
*/
|
|
1593
|
+
findNear(referenceId, criteria, elements) {
|
|
1594
|
+
return this.search({ ...criteria, near: referenceId }, elements).results;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Find elements within a container
|
|
1598
|
+
*/
|
|
1599
|
+
findWithin(containerId, criteria, elements) {
|
|
1600
|
+
return this.search({ ...criteria, within: containerId }, elements).results;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Score an element against search criteria
|
|
1604
|
+
*/
|
|
1605
|
+
scoreElement(searchable, criteria) {
|
|
1606
|
+
const scores = {};
|
|
1607
|
+
const matchReasons = [];
|
|
1608
|
+
let totalWeight = 0;
|
|
1609
|
+
let weightedScore = 0;
|
|
1610
|
+
const fuzzyConfig = {
|
|
1611
|
+
...DEFAULT_FUZZY_CONFIG,
|
|
1612
|
+
threshold: criteria.fuzzyThreshold ?? this.config.fuzzyThreshold
|
|
1613
|
+
};
|
|
1614
|
+
if (criteria.text) {
|
|
1615
|
+
const textScore = this.scoreTextMatch(
|
|
1616
|
+
searchable,
|
|
1617
|
+
criteria.text,
|
|
1618
|
+
criteria.fuzzy !== false,
|
|
1619
|
+
fuzzyConfig.threshold
|
|
1620
|
+
);
|
|
1621
|
+
scores.text = textScore.score;
|
|
1622
|
+
if (textScore.score > 0) {
|
|
1623
|
+
matchReasons.push(...textScore.reasons);
|
|
1624
|
+
}
|
|
1625
|
+
weightedScore += textScore.score * this.config.textWeight;
|
|
1626
|
+
totalWeight += this.config.textWeight;
|
|
1627
|
+
}
|
|
1628
|
+
if (criteria.textContent && !criteria.text) {
|
|
1629
|
+
const alternatives = criteria.textContent.includes("|") ? criteria.textContent.split("|").map((s) => s.trim()).filter(Boolean) : [criteria.textContent];
|
|
1630
|
+
let bestScore = 0;
|
|
1631
|
+
let bestReasons = [];
|
|
1632
|
+
for (const alt of alternatives) {
|
|
1633
|
+
const exactScore = this.scoreTextMatch(
|
|
1634
|
+
searchable,
|
|
1635
|
+
alt,
|
|
1636
|
+
criteria.fuzzy !== false,
|
|
1637
|
+
fuzzyConfig.threshold
|
|
1638
|
+
);
|
|
1639
|
+
const containsScore = this.scoreContainsMatch(searchable, alt, criteria.fuzzy !== false);
|
|
1640
|
+
const altBest = Math.max(exactScore.score, containsScore.score);
|
|
1641
|
+
if (altBest > bestScore) {
|
|
1642
|
+
bestScore = altBest;
|
|
1643
|
+
bestReasons = exactScore.score >= containsScore.score ? exactScore.reasons : containsScore.reasons;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
scores.text = bestScore;
|
|
1647
|
+
if (bestScore > 0) {
|
|
1648
|
+
matchReasons.push(...bestReasons);
|
|
1649
|
+
}
|
|
1650
|
+
weightedScore += bestScore * this.config.textWeight;
|
|
1651
|
+
totalWeight += this.config.textWeight;
|
|
1652
|
+
}
|
|
1653
|
+
if (criteria.textContains) {
|
|
1654
|
+
const containsScore = this.scoreContainsMatch(
|
|
1655
|
+
searchable,
|
|
1656
|
+
criteria.textContains,
|
|
1657
|
+
criteria.fuzzy !== false
|
|
1658
|
+
);
|
|
1659
|
+
scores.text = Math.max(scores.text || 0, containsScore.score);
|
|
1660
|
+
if (containsScore.score > 0 && containsScore.reasons.length > 0) {
|
|
1661
|
+
matchReasons.push(...containsScore.reasons);
|
|
1662
|
+
}
|
|
1663
|
+
weightedScore += containsScore.score * this.config.textWeight;
|
|
1664
|
+
totalWeight += this.config.textWeight;
|
|
1665
|
+
}
|
|
1666
|
+
if (criteria.accessibleName) {
|
|
1667
|
+
const accessibilityScore = this.scoreAccessibilityMatch(
|
|
1668
|
+
searchable,
|
|
1669
|
+
criteria.accessibleName,
|
|
1670
|
+
criteria.fuzzy !== false,
|
|
1671
|
+
fuzzyConfig.threshold
|
|
1672
|
+
);
|
|
1673
|
+
scores.accessibility = accessibilityScore.score;
|
|
1674
|
+
if (accessibilityScore.score > 0) {
|
|
1675
|
+
matchReasons.push(...accessibilityScore.reasons);
|
|
1676
|
+
}
|
|
1677
|
+
weightedScore += accessibilityScore.score * this.config.accessibilityWeight;
|
|
1678
|
+
totalWeight += this.config.accessibilityWeight;
|
|
1679
|
+
}
|
|
1680
|
+
if (criteria.role) {
|
|
1681
|
+
const roleScore = this.scoreRoleMatch(searchable, criteria.role);
|
|
1682
|
+
scores.role = roleScore.score;
|
|
1683
|
+
if (roleScore.score > 0) {
|
|
1684
|
+
matchReasons.push(...roleScore.reasons);
|
|
1685
|
+
}
|
|
1686
|
+
weightedScore += roleScore.score * this.config.roleWeight;
|
|
1687
|
+
totalWeight += this.config.roleWeight;
|
|
1688
|
+
}
|
|
1689
|
+
if (criteria.type) {
|
|
1690
|
+
const typeMatch = searchable.type.toLowerCase() === criteria.type.toLowerCase();
|
|
1691
|
+
if (typeMatch) {
|
|
1692
|
+
matchReasons.push(`type: ${criteria.type}`);
|
|
1693
|
+
weightedScore += 1 * this.config.roleWeight;
|
|
1694
|
+
totalWeight += this.config.roleWeight;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (criteria.near) {
|
|
1698
|
+
const spatialScore = this.scoreSpatialMatch(searchable, criteria.near);
|
|
1699
|
+
scores.spatial = spatialScore.score;
|
|
1700
|
+
if (spatialScore.score > 0) {
|
|
1701
|
+
matchReasons.push(...spatialScore.reasons);
|
|
1702
|
+
}
|
|
1703
|
+
weightedScore += spatialScore.score * this.config.spatialWeight;
|
|
1704
|
+
totalWeight += this.config.spatialWeight;
|
|
1705
|
+
}
|
|
1706
|
+
if (criteria.placeholder && searchable.placeholder) {
|
|
1707
|
+
const placeholderResult = fuzzyMatch(
|
|
1708
|
+
searchable.placeholder,
|
|
1709
|
+
criteria.placeholder,
|
|
1710
|
+
fuzzyConfig
|
|
1711
|
+
);
|
|
1712
|
+
if (placeholderResult.isMatch) {
|
|
1713
|
+
matchReasons.push(`placeholder matches`);
|
|
1714
|
+
weightedScore += placeholderResult.similarity * this.config.textWeight;
|
|
1715
|
+
totalWeight += this.config.textWeight;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (criteria.title && searchable.title) {
|
|
1719
|
+
const titleResult = fuzzyMatch(searchable.title, criteria.title, fuzzyConfig);
|
|
1720
|
+
if (titleResult.isMatch) {
|
|
1721
|
+
matchReasons.push(`title matches`);
|
|
1722
|
+
weightedScore += titleResult.similarity * this.config.textWeight;
|
|
1723
|
+
totalWeight += this.config.textWeight;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (criteria.idPattern) {
|
|
1727
|
+
const idMatch = this.matchPattern(searchable.id, criteria.idPattern);
|
|
1728
|
+
if (idMatch) {
|
|
1729
|
+
matchReasons.push(`id matches pattern`);
|
|
1730
|
+
weightedScore += 1 * this.config.textWeight;
|
|
1731
|
+
totalWeight += this.config.textWeight;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const aliasScore = this.scoreAliasMatch(searchable, criteria, fuzzyConfig.threshold);
|
|
1735
|
+
if (aliasScore.score > 0) {
|
|
1736
|
+
scores.fuzzy = aliasScore.score;
|
|
1737
|
+
matchReasons.push(...aliasScore.reasons);
|
|
1738
|
+
weightedScore += aliasScore.score * this.config.aliasWeight;
|
|
1739
|
+
totalWeight += this.config.aliasWeight;
|
|
1740
|
+
}
|
|
1741
|
+
const confidence = totalWeight > 0 ? weightedScore / totalWeight : 0;
|
|
1742
|
+
const aiElement = this.toAIDiscoveredElement(searchable);
|
|
1743
|
+
return {
|
|
1744
|
+
element: aiElement,
|
|
1745
|
+
confidence,
|
|
1746
|
+
matchReasons,
|
|
1747
|
+
scores
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Score text match
|
|
1752
|
+
*/
|
|
1753
|
+
scoreTextMatch(searchable, text, fuzzy, threshold) {
|
|
1754
|
+
const reasons = [];
|
|
1755
|
+
let maxScore = 0;
|
|
1756
|
+
const textsToMatch = [searchable.textContent, searchable.labelText, searchable.value].filter(
|
|
1757
|
+
Boolean
|
|
1758
|
+
);
|
|
1759
|
+
for (const targetText of textsToMatch) {
|
|
1760
|
+
if (targetText.toLowerCase() === text.toLowerCase()) {
|
|
1761
|
+
maxScore = Math.max(maxScore, 1);
|
|
1762
|
+
reasons.push("exact text match");
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (fuzzy) {
|
|
1766
|
+
const result = fuzzyMatch(targetText, text, { threshold });
|
|
1767
|
+
if (result.isMatch && result.similarity > maxScore) {
|
|
1768
|
+
maxScore = result.similarity;
|
|
1769
|
+
reasons.push(`text similarity: ${(result.similarity * 100).toFixed(0)}%`);
|
|
1770
|
+
}
|
|
1771
|
+
const wordSim = wordSimilarity(targetText, text, { threshold });
|
|
1772
|
+
if (wordSim > maxScore && wordSim >= threshold) {
|
|
1773
|
+
maxScore = wordSim;
|
|
1774
|
+
reasons.push(`word match: ${(wordSim * 100).toFixed(0)}%`);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return { score: maxScore, reasons };
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Score contains match
|
|
1782
|
+
*/
|
|
1783
|
+
scoreContainsMatch(searchable, text, fuzzy) {
|
|
1784
|
+
const reasons = [];
|
|
1785
|
+
let maxScore = 0;
|
|
1786
|
+
const textsToMatch = [
|
|
1787
|
+
searchable.textContent,
|
|
1788
|
+
searchable.labelText,
|
|
1789
|
+
searchable.ariaLabel
|
|
1790
|
+
].filter(Boolean);
|
|
1791
|
+
for (const targetText of textsToMatch) {
|
|
1792
|
+
if (targetText.toLowerCase().includes(text.toLowerCase())) {
|
|
1793
|
+
maxScore = Math.max(maxScore, 0.9);
|
|
1794
|
+
reasons.push("text contains match");
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (fuzzy && fuzzyContains(targetText, text)) {
|
|
1798
|
+
maxScore = Math.max(maxScore, 0.7);
|
|
1799
|
+
reasons.push("fuzzy contains match");
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return { score: maxScore, reasons };
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Score accessibility match
|
|
1806
|
+
*/
|
|
1807
|
+
scoreAccessibilityMatch(searchable, name, fuzzy, threshold) {
|
|
1808
|
+
const reasons = [];
|
|
1809
|
+
let maxScore = 0;
|
|
1810
|
+
const accessibleNames = [
|
|
1811
|
+
searchable.ariaLabel,
|
|
1812
|
+
searchable.ariaLabelledBy,
|
|
1813
|
+
searchable.labelText,
|
|
1814
|
+
searchable.title
|
|
1815
|
+
].filter(Boolean);
|
|
1816
|
+
for (const accessibleName of accessibleNames) {
|
|
1817
|
+
if (accessibleName.toLowerCase() === name.toLowerCase()) {
|
|
1818
|
+
maxScore = Math.max(maxScore, 1);
|
|
1819
|
+
reasons.push("exact accessible name match");
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
if (fuzzy) {
|
|
1823
|
+
const result = fuzzyMatch(accessibleName, name, { threshold });
|
|
1824
|
+
if (result.isMatch && result.similarity > maxScore) {
|
|
1825
|
+
maxScore = result.similarity;
|
|
1826
|
+
reasons.push(`accessible name similarity: ${(result.similarity * 100).toFixed(0)}%`);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return { score: maxScore, reasons };
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Score role match
|
|
1834
|
+
*/
|
|
1835
|
+
scoreRoleMatch(searchable, role) {
|
|
1836
|
+
const reasons = [];
|
|
1837
|
+
const normalizedRole = role.toLowerCase();
|
|
1838
|
+
if (searchable.role?.toLowerCase() === normalizedRole) {
|
|
1839
|
+
return { score: 1, reasons: [`role: ${role}`] };
|
|
1840
|
+
}
|
|
1841
|
+
const tagRoleMap = {
|
|
1842
|
+
button: ["button", "input[type=button]", "input[type=submit]"],
|
|
1843
|
+
textbox: ["input", "textarea"],
|
|
1844
|
+
checkbox: ["input[type=checkbox]"],
|
|
1845
|
+
radio: ["input[type=radio]"],
|
|
1846
|
+
link: ["a"],
|
|
1847
|
+
listbox: ["select"],
|
|
1848
|
+
combobox: ["select", "input[list]"],
|
|
1849
|
+
navigation: ["nav"],
|
|
1850
|
+
main: ["main"],
|
|
1851
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
1852
|
+
};
|
|
1853
|
+
const inferredRoles = tagRoleMap[normalizedRole] || [];
|
|
1854
|
+
if (inferredRoles.some(
|
|
1855
|
+
(r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole
|
|
1856
|
+
)) {
|
|
1857
|
+
return { score: 0.8, reasons: [`inferred role: ${role}`] };
|
|
1858
|
+
}
|
|
1859
|
+
return { score: 0, reasons };
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Score spatial match (proximity to another element)
|
|
1863
|
+
*/
|
|
1864
|
+
scoreSpatialMatch(searchable, nearId) {
|
|
1865
|
+
const reference = this.cachedElements.find((el) => el.id === nearId);
|
|
1866
|
+
if (!reference) {
|
|
1867
|
+
return { score: 0, reasons: [] };
|
|
1868
|
+
}
|
|
1869
|
+
const distance = this.calculateDistance(searchable.rect, reference.rect);
|
|
1870
|
+
const nearThreshold = 200;
|
|
1871
|
+
if (distance > nearThreshold * 3) {
|
|
1872
|
+
return { score: 0, reasons: [] };
|
|
1873
|
+
}
|
|
1874
|
+
const score = Math.max(0, 1 - distance / (nearThreshold * 3));
|
|
1875
|
+
return {
|
|
1876
|
+
score,
|
|
1877
|
+
reasons: [`${distance.toFixed(0)}px from ${nearId}`]
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Calculate distance between two element rectangles
|
|
1882
|
+
*/
|
|
1883
|
+
calculateDistance(rect1, rect2) {
|
|
1884
|
+
const center1 = {
|
|
1885
|
+
x: rect1.x + rect1.width / 2,
|
|
1886
|
+
y: rect1.y + rect1.height / 2
|
|
1887
|
+
};
|
|
1888
|
+
const center2 = {
|
|
1889
|
+
x: rect2.x + rect2.width / 2,
|
|
1890
|
+
y: rect2.y + rect2.height / 2
|
|
1891
|
+
};
|
|
1892
|
+
return Math.sqrt(Math.pow(center1.x - center2.x, 2) + Math.pow(center1.y - center2.y, 2));
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Score alias match
|
|
1896
|
+
*/
|
|
1897
|
+
scoreAliasMatch(searchable, criteria, threshold) {
|
|
1898
|
+
const reasons = [];
|
|
1899
|
+
let maxScore = 0;
|
|
1900
|
+
const searchTerms = [];
|
|
1901
|
+
if (criteria.text) searchTerms.push(criteria.text);
|
|
1902
|
+
if (criteria.textContains) searchTerms.push(criteria.textContains);
|
|
1903
|
+
if (criteria.accessibleName) searchTerms.push(criteria.accessibleName);
|
|
1904
|
+
for (const searchTerm of searchTerms) {
|
|
1905
|
+
const termLower = searchTerm.toLowerCase();
|
|
1906
|
+
for (const alias of searchable.aliases) {
|
|
1907
|
+
if (alias === termLower) {
|
|
1908
|
+
maxScore = Math.max(maxScore, 1);
|
|
1909
|
+
reasons.push(`alias match: "${alias}"`);
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
const searchWords = termLower.split(/\s+/);
|
|
1913
|
+
const aliasWords = alias.split(/\s+/);
|
|
1914
|
+
for (const searchWord of searchWords) {
|
|
1915
|
+
for (const aliasWord of aliasWords) {
|
|
1916
|
+
if (areSynonyms(searchWord, aliasWord)) {
|
|
1917
|
+
maxScore = Math.max(maxScore, 0.85);
|
|
1918
|
+
reasons.push(`synonym match: "${searchWord}" ~ "${aliasWord}"`);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
const result = fuzzyMatch(alias, termLower, { threshold });
|
|
1923
|
+
if (result.isMatch && result.similarity > maxScore) {
|
|
1924
|
+
maxScore = result.similarity;
|
|
1925
|
+
reasons.push(`fuzzy alias: "${alias}" (${(result.similarity * 100).toFixed(0)}%)`);
|
|
1926
|
+
}
|
|
1927
|
+
const tokenSim = tokenSimilarity(alias, termLower);
|
|
1928
|
+
if (tokenSim > maxScore && tokenSim >= threshold) {
|
|
1929
|
+
maxScore = tokenSim;
|
|
1930
|
+
reasons.push(`token match: "${alias}"`);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
return { score: maxScore, reasons };
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Match a string against a pattern (supports * wildcard)
|
|
1938
|
+
*/
|
|
1939
|
+
matchPattern(str, pattern) {
|
|
1940
|
+
const regexPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
1941
|
+
return new RegExp(`^${regexPattern}$`, "i").test(str);
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Convert searchable element to AI discovered element
|
|
1945
|
+
*/
|
|
1946
|
+
toAIDiscoveredElement(searchable) {
|
|
1947
|
+
const discoveredBase = "getState" in searchable.element ? {
|
|
1948
|
+
id: searchable.id,
|
|
1949
|
+
type: searchable.type,
|
|
1950
|
+
label: searchable.element.label,
|
|
1951
|
+
tagName: searchable.tagName,
|
|
1952
|
+
role: searchable.role,
|
|
1953
|
+
accessibleName: searchable.ariaLabel,
|
|
1954
|
+
actions: searchable.element.actions,
|
|
1955
|
+
state: searchable.state,
|
|
1956
|
+
registered: true
|
|
1957
|
+
} : searchable.element;
|
|
1958
|
+
return {
|
|
1959
|
+
...discoveredBase,
|
|
1960
|
+
description: searchable.description,
|
|
1961
|
+
aliases: searchable.aliases,
|
|
1962
|
+
purpose: generatePurpose({
|
|
1963
|
+
textContent: searchable.textContent,
|
|
1964
|
+
ariaLabel: searchable.ariaLabel,
|
|
1965
|
+
elementType: searchable.type,
|
|
1966
|
+
tagName: searchable.tagName
|
|
1967
|
+
}),
|
|
1968
|
+
parentContext: void 0,
|
|
1969
|
+
// Would need DOM traversal
|
|
1970
|
+
suggestedActions: generateSuggestedActions({
|
|
1971
|
+
textContent: searchable.textContent,
|
|
1972
|
+
ariaLabel: searchable.ariaLabel,
|
|
1973
|
+
elementType: searchable.type,
|
|
1974
|
+
tagName: searchable.tagName
|
|
1975
|
+
}),
|
|
1976
|
+
semanticType: this.inferSemanticType(searchable),
|
|
1977
|
+
labelText: searchable.labelText,
|
|
1978
|
+
placeholder: searchable.placeholder,
|
|
1979
|
+
title: searchable.title
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Infer a semantic type for the element
|
|
1984
|
+
*/
|
|
1985
|
+
inferSemanticType(searchable) {
|
|
1986
|
+
const text = (searchable.textContent || searchable.ariaLabel || "").toLowerCase();
|
|
1987
|
+
const type = searchable.type.toLowerCase();
|
|
1988
|
+
if (type === "input" || type === "textarea") {
|
|
1989
|
+
if (searchable.placeholder?.toLowerCase().includes("email") || text.includes("email")) {
|
|
1990
|
+
return "email-input";
|
|
1991
|
+
}
|
|
1992
|
+
if (searchable.placeholder?.toLowerCase().includes("password") || text.includes("password")) {
|
|
1993
|
+
return "password-input";
|
|
1994
|
+
}
|
|
1995
|
+
if (searchable.placeholder?.toLowerCase().includes("search") || text.includes("search")) {
|
|
1996
|
+
return "search-input";
|
|
1997
|
+
}
|
|
1998
|
+
return "text-input";
|
|
1999
|
+
}
|
|
2000
|
+
if (type === "button") {
|
|
2001
|
+
if (text.match(/submit|save|confirm|ok|done|apply/)) return "submit-button";
|
|
2002
|
+
if (text.match(/cancel|close|dismiss/)) return "cancel-button";
|
|
2003
|
+
if (text.match(/delete|remove|trash/)) return "delete-button";
|
|
2004
|
+
if (text.match(/add|create|new|\+/)) return "add-button";
|
|
2005
|
+
if (text.match(/edit|modify/)) return "edit-button";
|
|
2006
|
+
if (text.match(/next|continue/)) return "next-button";
|
|
2007
|
+
if (text.match(/back|previous/)) return "back-button";
|
|
2008
|
+
return "action-button";
|
|
2009
|
+
}
|
|
2010
|
+
if (type === "link") {
|
|
2011
|
+
if (text.match(/home|dashboard/)) return "home-link";
|
|
2012
|
+
if (text.match(/login|sign.?in/)) return "login-link";
|
|
2013
|
+
if (text.match(/logout|sign.?out/)) return "logout-link";
|
|
2014
|
+
return "navigation-link";
|
|
2015
|
+
}
|
|
2016
|
+
return type;
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
// src/ai/assertions.ts
|
|
2021
|
+
var DEFAULT_ASSERTION_CONFIG = {
|
|
2022
|
+
defaultTimeout: 5e3,
|
|
2023
|
+
pollInterval: 100,
|
|
2024
|
+
fuzzyThreshold: 0.7,
|
|
2025
|
+
includeSuggestions: true
|
|
2026
|
+
};
|
|
2027
|
+
var AssertionExecutor = class {
|
|
2028
|
+
constructor(config = {}) {
|
|
2029
|
+
this.elements = [];
|
|
2030
|
+
this.config = { ...DEFAULT_ASSERTION_CONFIG, ...config };
|
|
2031
|
+
this.searchEngine = new SearchEngine({ fuzzyThreshold: this.config.fuzzyThreshold });
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Update available elements for assertions
|
|
2035
|
+
*/
|
|
2036
|
+
updateElements(elements) {
|
|
2037
|
+
this.elements = elements;
|
|
2038
|
+
this.searchEngine.updateElements(elements);
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Execute a single assertion
|
|
2042
|
+
*/
|
|
2043
|
+
async assert(request) {
|
|
2044
|
+
const startTime = performance.now();
|
|
2045
|
+
const timeout = request.timeout ?? this.config.defaultTimeout;
|
|
2046
|
+
const searchResult = this.findElementDetailed(request.target, request.fuzzy !== false);
|
|
2047
|
+
const element = searchResult?.element ?? null;
|
|
2048
|
+
const searchDetails = searchResult ? {
|
|
2049
|
+
confidence: searchResult.confidence,
|
|
2050
|
+
matchReasons: searchResult.matchReasons,
|
|
2051
|
+
candidateCount: this.elements.length
|
|
2052
|
+
} : void 0;
|
|
2053
|
+
if (!element && request.type !== "notExists") {
|
|
2054
|
+
const result2 = this.createResult(
|
|
2055
|
+
false,
|
|
2056
|
+
typeof request.target === "string" ? request.target : JSON.stringify(request.target),
|
|
2057
|
+
"element not found",
|
|
2058
|
+
request.type === "exists" ? true : request.expected,
|
|
2059
|
+
null,
|
|
2060
|
+
"Element could not be found",
|
|
2061
|
+
this.config.includeSuggestions ? "Check if the element exists and is properly labeled" : void 0,
|
|
2062
|
+
startTime
|
|
2063
|
+
);
|
|
2064
|
+
if (searchDetails) {
|
|
2065
|
+
result2.searchDetails = searchDetails;
|
|
2066
|
+
}
|
|
2067
|
+
return result2;
|
|
2068
|
+
}
|
|
2069
|
+
const result = await this.executeAssertion(request, element, timeout, startTime);
|
|
2070
|
+
if (searchDetails) {
|
|
2071
|
+
result.searchDetails = searchDetails;
|
|
2072
|
+
}
|
|
2073
|
+
return result;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Execute multiple assertions
|
|
2077
|
+
*/
|
|
2078
|
+
async assertBatch(request) {
|
|
2079
|
+
const startTime = performance.now();
|
|
2080
|
+
const results = [];
|
|
2081
|
+
let passedCount = 0;
|
|
2082
|
+
let failedCount = 0;
|
|
2083
|
+
for (const assertion of request.assertions) {
|
|
2084
|
+
const result = await this.assert(assertion);
|
|
2085
|
+
results.push(result);
|
|
2086
|
+
if (result.passed) {
|
|
2087
|
+
passedCount++;
|
|
2088
|
+
} else {
|
|
2089
|
+
failedCount++;
|
|
2090
|
+
if (request.stopOnFailure) {
|
|
2091
|
+
break;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
const passed = request.mode === "all" ? failedCount === 0 : passedCount > 0;
|
|
2096
|
+
return {
|
|
2097
|
+
passed,
|
|
2098
|
+
results,
|
|
2099
|
+
passedCount,
|
|
2100
|
+
failedCount,
|
|
2101
|
+
durationMs: performance.now() - startTime,
|
|
2102
|
+
timestamp: Date.now()
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Convenience method: assert element is visible
|
|
2107
|
+
*/
|
|
2108
|
+
async assertVisible(target, timeout) {
|
|
2109
|
+
return this.assert({ target, type: "visible", timeout });
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Convenience method: assert element is hidden
|
|
2113
|
+
*/
|
|
2114
|
+
async assertHidden(target, timeout) {
|
|
2115
|
+
return this.assert({ target, type: "hidden", timeout });
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Convenience method: assert element is enabled
|
|
2119
|
+
*/
|
|
2120
|
+
async assertEnabled(target, timeout) {
|
|
2121
|
+
return this.assert({ target, type: "enabled", timeout });
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Convenience method: assert element is disabled
|
|
2125
|
+
*/
|
|
2126
|
+
async assertDisabled(target, timeout) {
|
|
2127
|
+
return this.assert({ target, type: "disabled", timeout });
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Convenience method: assert element has text
|
|
2131
|
+
*/
|
|
2132
|
+
async assertHasText(target, text, timeout) {
|
|
2133
|
+
return this.assert({ target, type: "hasText", expected: text, timeout });
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Convenience method: assert element contains text
|
|
2137
|
+
*/
|
|
2138
|
+
async assertContainsText(target, text, timeout) {
|
|
2139
|
+
return this.assert({ target, type: "containsText", expected: text, timeout });
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Convenience method: assert element has value
|
|
2143
|
+
*/
|
|
2144
|
+
async assertHasValue(target, value, timeout) {
|
|
2145
|
+
return this.assert({ target, type: "hasValue", expected: value, timeout });
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Convenience method: assert element exists
|
|
2149
|
+
*/
|
|
2150
|
+
async assertExists(target, timeout) {
|
|
2151
|
+
return this.assert({ target, type: "exists", timeout });
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Convenience method: assert element does not exist
|
|
2155
|
+
*/
|
|
2156
|
+
async assertNotExists(target, timeout) {
|
|
2157
|
+
return this.assert({ target, type: "notExists", timeout });
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Convenience method: assert checkbox is checked
|
|
2161
|
+
*/
|
|
2162
|
+
async assertChecked(target, timeout) {
|
|
2163
|
+
return this.assert({ target, type: "checked", timeout });
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Convenience method: assert checkbox is unchecked
|
|
2167
|
+
*/
|
|
2168
|
+
async assertUnchecked(target, timeout) {
|
|
2169
|
+
return this.assert({ target, type: "unchecked", timeout });
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Convenience method: assert element count
|
|
2173
|
+
*/
|
|
2174
|
+
async assertCount(target, expectedCount, timeout) {
|
|
2175
|
+
return this.assert({ target, type: "count", expected: expectedCount, timeout });
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Find element by target with full search metadata.
|
|
2179
|
+
* Returns the SearchResult (including confidence, matchReasons, scores)
|
|
2180
|
+
* or null if no match above the fuzzy threshold.
|
|
2181
|
+
*/
|
|
2182
|
+
findElementDetailed(target, fuzzy = true) {
|
|
2183
|
+
const criteria = typeof target === "string" ? { text: target, fuzzy } : { ...target, fuzzy };
|
|
2184
|
+
const searchResult = this.searchEngine.findBest(criteria, this.elements);
|
|
2185
|
+
if (searchResult && searchResult.confidence >= this.config.fuzzyThreshold) {
|
|
2186
|
+
return searchResult;
|
|
2187
|
+
}
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Find element by target (string or criteria).
|
|
2192
|
+
* Public for use by condition evaluation in SpecExecutor.
|
|
2193
|
+
*/
|
|
2194
|
+
async findElement(target, fuzzy = true) {
|
|
2195
|
+
const result = this.findElementDetailed(target, fuzzy);
|
|
2196
|
+
return result?.element ?? null;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Execute the actual assertion
|
|
2200
|
+
*/
|
|
2201
|
+
async executeAssertion(request, element, timeout, startTime) {
|
|
2202
|
+
const targetStr = typeof request.target === "string" ? request.target : JSON.stringify(request.target);
|
|
2203
|
+
const elementDescription = element?.description || targetStr;
|
|
2204
|
+
switch (request.type) {
|
|
2205
|
+
case "visible":
|
|
2206
|
+
return this.assertVisibility(
|
|
2207
|
+
element,
|
|
2208
|
+
true,
|
|
2209
|
+
elementDescription,
|
|
2210
|
+
request.message,
|
|
2211
|
+
startTime
|
|
2212
|
+
);
|
|
2213
|
+
case "hidden":
|
|
2214
|
+
return this.assertVisibility(
|
|
2215
|
+
element,
|
|
2216
|
+
false,
|
|
2217
|
+
elementDescription,
|
|
2218
|
+
request.message,
|
|
2219
|
+
startTime
|
|
2220
|
+
);
|
|
2221
|
+
case "enabled":
|
|
2222
|
+
return this.assertEnabledState(
|
|
2223
|
+
element,
|
|
2224
|
+
true,
|
|
2225
|
+
elementDescription,
|
|
2226
|
+
request.message,
|
|
2227
|
+
startTime
|
|
2228
|
+
);
|
|
2229
|
+
case "disabled":
|
|
2230
|
+
return this.assertEnabledState(
|
|
2231
|
+
element,
|
|
2232
|
+
false,
|
|
2233
|
+
elementDescription,
|
|
2234
|
+
request.message,
|
|
2235
|
+
startTime
|
|
2236
|
+
);
|
|
2237
|
+
case "focused":
|
|
2238
|
+
return this.assertFocused(element, elementDescription, request.message, startTime);
|
|
2239
|
+
case "checked":
|
|
2240
|
+
return this.assertCheckedState(
|
|
2241
|
+
element,
|
|
2242
|
+
true,
|
|
2243
|
+
elementDescription,
|
|
2244
|
+
request.message,
|
|
2245
|
+
startTime
|
|
2246
|
+
);
|
|
2247
|
+
case "unchecked":
|
|
2248
|
+
return this.assertCheckedState(
|
|
2249
|
+
element,
|
|
2250
|
+
false,
|
|
2251
|
+
elementDescription,
|
|
2252
|
+
request.message,
|
|
2253
|
+
startTime
|
|
2254
|
+
);
|
|
2255
|
+
case "hasText":
|
|
2256
|
+
return this.assertTextMatch(
|
|
2257
|
+
element,
|
|
2258
|
+
request.expected,
|
|
2259
|
+
true,
|
|
2260
|
+
elementDescription,
|
|
2261
|
+
request.message,
|
|
2262
|
+
startTime
|
|
2263
|
+
);
|
|
2264
|
+
case "containsText":
|
|
2265
|
+
return this.assertTextMatch(
|
|
2266
|
+
element,
|
|
2267
|
+
request.expected,
|
|
2268
|
+
false,
|
|
2269
|
+
elementDescription,
|
|
2270
|
+
request.message,
|
|
2271
|
+
startTime
|
|
2272
|
+
);
|
|
2273
|
+
case "hasValue":
|
|
2274
|
+
return this.assertValue(
|
|
2275
|
+
element,
|
|
2276
|
+
request.expected,
|
|
2277
|
+
elementDescription,
|
|
2278
|
+
request.message,
|
|
2279
|
+
startTime
|
|
2280
|
+
);
|
|
2281
|
+
case "exists":
|
|
2282
|
+
return this.createResult(
|
|
2283
|
+
element !== null,
|
|
2284
|
+
targetStr,
|
|
2285
|
+
elementDescription,
|
|
2286
|
+
true,
|
|
2287
|
+
element !== null,
|
|
2288
|
+
element === null ? "Element does not exist" : void 0,
|
|
2289
|
+
void 0,
|
|
2290
|
+
startTime,
|
|
2291
|
+
element?.state
|
|
2292
|
+
);
|
|
2293
|
+
case "notExists":
|
|
2294
|
+
return this.createResult(
|
|
2295
|
+
element === null,
|
|
2296
|
+
targetStr,
|
|
2297
|
+
elementDescription,
|
|
2298
|
+
false,
|
|
2299
|
+
element === null,
|
|
2300
|
+
element !== null ? "Element exists but should not" : void 0,
|
|
2301
|
+
void 0,
|
|
2302
|
+
startTime,
|
|
2303
|
+
element?.state
|
|
2304
|
+
);
|
|
2305
|
+
case "count":
|
|
2306
|
+
return this.assertElementCount(
|
|
2307
|
+
request.target,
|
|
2308
|
+
request.expected,
|
|
2309
|
+
targetStr,
|
|
2310
|
+
request.message,
|
|
2311
|
+
startTime
|
|
2312
|
+
);
|
|
2313
|
+
case "attribute":
|
|
2314
|
+
return this.assertAttribute(
|
|
2315
|
+
element,
|
|
2316
|
+
request.attributeName,
|
|
2317
|
+
request.expected,
|
|
2318
|
+
elementDescription,
|
|
2319
|
+
request.message,
|
|
2320
|
+
startTime
|
|
2321
|
+
);
|
|
2322
|
+
case "hasClass":
|
|
2323
|
+
return this.assertHasClass(
|
|
2324
|
+
element,
|
|
2325
|
+
request.expected,
|
|
2326
|
+
elementDescription,
|
|
2327
|
+
request.message,
|
|
2328
|
+
startTime
|
|
2329
|
+
);
|
|
2330
|
+
case "cssProperty":
|
|
2331
|
+
return this.assertCssProperty(
|
|
2332
|
+
element,
|
|
2333
|
+
request.propertyName,
|
|
2334
|
+
request.expected,
|
|
2335
|
+
elementDescription,
|
|
2336
|
+
request.message,
|
|
2337
|
+
startTime
|
|
2338
|
+
);
|
|
2339
|
+
default:
|
|
2340
|
+
return this.createResult(
|
|
2341
|
+
false,
|
|
2342
|
+
targetStr,
|
|
2343
|
+
elementDescription,
|
|
2344
|
+
void 0,
|
|
2345
|
+
void 0,
|
|
2346
|
+
`Unknown assertion type: ${request.type}`,
|
|
2347
|
+
void 0,
|
|
2348
|
+
startTime
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Assert visibility state
|
|
2354
|
+
*/
|
|
2355
|
+
assertVisibility(element, expectedVisible, description, message, startTime = performance.now()) {
|
|
2356
|
+
const isVisible = element.state.visible;
|
|
2357
|
+
const passed = isVisible === expectedVisible;
|
|
2358
|
+
return this.createResult(
|
|
2359
|
+
passed,
|
|
2360
|
+
element.id,
|
|
2361
|
+
description,
|
|
2362
|
+
expectedVisible,
|
|
2363
|
+
isVisible,
|
|
2364
|
+
passed ? void 0 : message || `Element is ${isVisible ? "visible" : "hidden"} but expected ${expectedVisible ? "visible" : "hidden"}`,
|
|
2365
|
+
passed ? void 0 : "Check if element is covered by another element or has display:none",
|
|
2366
|
+
startTime,
|
|
2367
|
+
element.state
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Assert enabled state
|
|
2372
|
+
*/
|
|
2373
|
+
assertEnabledState(element, expectedEnabled, description, message, startTime = performance.now()) {
|
|
2374
|
+
const isEnabled = element.state.enabled;
|
|
2375
|
+
const passed = isEnabled === expectedEnabled;
|
|
2376
|
+
return this.createResult(
|
|
2377
|
+
passed,
|
|
2378
|
+
element.id,
|
|
2379
|
+
description,
|
|
2380
|
+
expectedEnabled,
|
|
2381
|
+
isEnabled,
|
|
2382
|
+
passed ? void 0 : message || `Element is ${isEnabled ? "enabled" : "disabled"} but expected ${expectedEnabled ? "enabled" : "disabled"}`,
|
|
2383
|
+
passed ? void 0 : "Check if the element has a disabled attribute or aria-disabled",
|
|
2384
|
+
startTime,
|
|
2385
|
+
element.state
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Assert focused state
|
|
2390
|
+
*/
|
|
2391
|
+
assertFocused(element, description, message, startTime = performance.now()) {
|
|
2392
|
+
const isFocused = element.state.focused;
|
|
2393
|
+
return this.createResult(
|
|
2394
|
+
isFocused,
|
|
2395
|
+
element.id,
|
|
2396
|
+
description,
|
|
2397
|
+
true,
|
|
2398
|
+
isFocused,
|
|
2399
|
+
isFocused ? void 0 : message || "Element is not focused",
|
|
2400
|
+
isFocused ? void 0 : "Click or focus the element first",
|
|
2401
|
+
startTime,
|
|
2402
|
+
element.state
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Assert checked state
|
|
2407
|
+
*/
|
|
2408
|
+
assertCheckedState(element, expectedChecked, description, message, startTime = performance.now()) {
|
|
2409
|
+
const isChecked = element.state.checked ?? false;
|
|
2410
|
+
const passed = isChecked === expectedChecked;
|
|
2411
|
+
return this.createResult(
|
|
2412
|
+
passed,
|
|
2413
|
+
element.id,
|
|
2414
|
+
description,
|
|
2415
|
+
expectedChecked,
|
|
2416
|
+
isChecked,
|
|
2417
|
+
passed ? void 0 : message || `Element is ${isChecked ? "checked" : "unchecked"} but expected ${expectedChecked ? "checked" : "unchecked"}`,
|
|
2418
|
+
passed ? void 0 : "Click the checkbox to change its state",
|
|
2419
|
+
startTime,
|
|
2420
|
+
element.state
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Assert text content
|
|
2425
|
+
*/
|
|
2426
|
+
assertTextMatch(element, expectedText, exact, description, message, startTime = performance.now()) {
|
|
2427
|
+
const actualText = element.state.textContent || "";
|
|
2428
|
+
const passed = exact ? actualText === expectedText : actualText.includes(expectedText);
|
|
2429
|
+
return this.createResult(
|
|
2430
|
+
passed,
|
|
2431
|
+
element.id,
|
|
2432
|
+
description,
|
|
2433
|
+
expectedText,
|
|
2434
|
+
actualText,
|
|
2435
|
+
passed ? void 0 : message || (exact ? `Text "${actualText}" does not match expected "${expectedText}"` : `Text "${actualText}" does not contain "${expectedText}"`),
|
|
2436
|
+
passed ? void 0 : "Verify the element contains the expected text",
|
|
2437
|
+
startTime,
|
|
2438
|
+
element.state
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Assert input value
|
|
2443
|
+
*/
|
|
2444
|
+
assertValue(element, expectedValue, description, message, startTime = performance.now()) {
|
|
2445
|
+
const actualValue = element.state.value || "";
|
|
2446
|
+
const passed = actualValue === expectedValue;
|
|
2447
|
+
return this.createResult(
|
|
2448
|
+
passed,
|
|
2449
|
+
element.id,
|
|
2450
|
+
description,
|
|
2451
|
+
expectedValue,
|
|
2452
|
+
actualValue,
|
|
2453
|
+
passed ? void 0 : message || `Value "${actualValue}" does not match expected "${expectedValue}"`,
|
|
2454
|
+
passed ? void 0 : "Type the expected value into the input",
|
|
2455
|
+
startTime,
|
|
2456
|
+
element.state
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Assert element count
|
|
2461
|
+
*/
|
|
2462
|
+
assertElementCount(criteria, expectedCount, targetStr, message, startTime = performance.now()) {
|
|
2463
|
+
const searchResponse = this.searchEngine.search(criteria);
|
|
2464
|
+
const actualCount = searchResponse.results.length;
|
|
2465
|
+
const passed = actualCount === expectedCount;
|
|
2466
|
+
return this.createResult(
|
|
2467
|
+
passed,
|
|
2468
|
+
targetStr,
|
|
2469
|
+
`${actualCount} elements matching criteria`,
|
|
2470
|
+
expectedCount,
|
|
2471
|
+
actualCount,
|
|
2472
|
+
passed ? void 0 : message || `Found ${actualCount} elements but expected ${expectedCount}`,
|
|
2473
|
+
passed ? void 0 : "Adjust search criteria or wait for elements to load",
|
|
2474
|
+
startTime
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Assert attribute value (placeholder for DOM attribute assertions)
|
|
2479
|
+
*/
|
|
2480
|
+
assertAttribute(element, attributeName, expectedValue, description, message, startTime = performance.now()) {
|
|
2481
|
+
let actualValue;
|
|
2482
|
+
switch (attributeName.toLowerCase()) {
|
|
2483
|
+
case "placeholder":
|
|
2484
|
+
actualValue = element.placeholder;
|
|
2485
|
+
break;
|
|
2486
|
+
case "title":
|
|
2487
|
+
actualValue = element.title;
|
|
2488
|
+
break;
|
|
2489
|
+
default:
|
|
2490
|
+
return this.createResult(
|
|
2491
|
+
false,
|
|
2492
|
+
element.id,
|
|
2493
|
+
description,
|
|
2494
|
+
expectedValue,
|
|
2495
|
+
void 0,
|
|
2496
|
+
`Cannot check attribute "${attributeName}" without DOM access`,
|
|
2497
|
+
"Use the server API to check element attributes",
|
|
2498
|
+
startTime,
|
|
2499
|
+
element.state
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
const passed = actualValue === expectedValue;
|
|
2503
|
+
return this.createResult(
|
|
2504
|
+
passed,
|
|
2505
|
+
element.id,
|
|
2506
|
+
description,
|
|
2507
|
+
expectedValue,
|
|
2508
|
+
actualValue,
|
|
2509
|
+
passed ? void 0 : message || `Attribute "${attributeName}" is "${actualValue}" but expected "${expectedValue}"`,
|
|
2510
|
+
void 0,
|
|
2511
|
+
startTime,
|
|
2512
|
+
element.state
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Assert element has CSS class
|
|
2517
|
+
*/
|
|
2518
|
+
assertHasClass(element, className, description, message, startTime = performance.now()) {
|
|
2519
|
+
return this.createResult(
|
|
2520
|
+
false,
|
|
2521
|
+
element.id,
|
|
2522
|
+
description,
|
|
2523
|
+
className,
|
|
2524
|
+
void 0,
|
|
2525
|
+
"Cannot check CSS classes without DOM access",
|
|
2526
|
+
"Use the server API to check element classes",
|
|
2527
|
+
startTime,
|
|
2528
|
+
element.state
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Assert CSS property value
|
|
2533
|
+
*/
|
|
2534
|
+
assertCssProperty(element, propertyName, expectedValue, description, message, startTime = performance.now()) {
|
|
2535
|
+
const computedStyles = element.state.computedStyles;
|
|
2536
|
+
if (!computedStyles) {
|
|
2537
|
+
return this.createResult(
|
|
2538
|
+
false,
|
|
2539
|
+
element.id,
|
|
2540
|
+
description,
|
|
2541
|
+
expectedValue,
|
|
2542
|
+
void 0,
|
|
2543
|
+
"Computed styles not available",
|
|
2544
|
+
"Request element state with computed styles",
|
|
2545
|
+
startTime,
|
|
2546
|
+
element.state
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
const styleKey = propertyName;
|
|
2550
|
+
const actualValue = computedStyles[styleKey];
|
|
2551
|
+
const passed = actualValue === expectedValue;
|
|
2552
|
+
return this.createResult(
|
|
2553
|
+
passed,
|
|
2554
|
+
element.id,
|
|
2555
|
+
description,
|
|
2556
|
+
expectedValue,
|
|
2557
|
+
actualValue,
|
|
2558
|
+
passed ? void 0 : message || `CSS property "${propertyName}" is "${actualValue}" but expected "${expectedValue}"`,
|
|
2559
|
+
void 0,
|
|
2560
|
+
startTime,
|
|
2561
|
+
element.state
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Create an assertion result
|
|
2566
|
+
*/
|
|
2567
|
+
createResult(passed, target, targetDescription, expected, actual, failureReason, suggestion, startTime = performance.now(), elementState) {
|
|
2568
|
+
return {
|
|
2569
|
+
passed,
|
|
2570
|
+
target,
|
|
2571
|
+
targetDescription,
|
|
2572
|
+
expected,
|
|
2573
|
+
actual,
|
|
2574
|
+
failureReason,
|
|
2575
|
+
suggestion: this.config.includeSuggestions ? suggestion : void 0,
|
|
2576
|
+
elementState,
|
|
2577
|
+
durationMs: performance.now() - startTime,
|
|
2578
|
+
timestamp: Date.now()
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
// src/specs/executor.ts
|
|
2584
|
+
function resolveTarget(target) {
|
|
2585
|
+
switch (target.type) {
|
|
2586
|
+
case "elementId":
|
|
2587
|
+
return { idPattern: target.elementId, fuzzy: false };
|
|
2588
|
+
case "search":
|
|
2589
|
+
return target.criteria;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
var SpecExecutor = class {
|
|
2593
|
+
constructor(config) {
|
|
2594
|
+
this.assertionExecutor = new AssertionExecutor(config);
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Update the element registry (pass-through to AssertionExecutor).
|
|
2598
|
+
*/
|
|
2599
|
+
updateElements(elements) {
|
|
2600
|
+
this.assertionExecutor.updateElements(elements);
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* Convert a SpecAssertion to an AssertionRequest.
|
|
2604
|
+
*/
|
|
2605
|
+
toAssertionRequest(assertion) {
|
|
2606
|
+
return {
|
|
2607
|
+
target: resolveTarget(assertion.target),
|
|
2608
|
+
type: assertion.assertionType,
|
|
2609
|
+
expected: assertion.expected,
|
|
2610
|
+
attributeName: assertion.attributeName,
|
|
2611
|
+
propertyName: assertion.propertyName,
|
|
2612
|
+
timeout: assertion.timeout,
|
|
2613
|
+
message: assertion.message
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Evaluate a condition to determine if an assertion should be executed.
|
|
2618
|
+
* Returns true if the condition is met (assertion should run),
|
|
2619
|
+
* false if condition is not met (assertion should skip/pass).
|
|
2620
|
+
*/
|
|
2621
|
+
async evaluateCondition(condition) {
|
|
2622
|
+
const target = resolveTarget(condition.target);
|
|
2623
|
+
const element = await this.assertionExecutor.findElement(target, false);
|
|
2624
|
+
switch (condition.type) {
|
|
2625
|
+
case "exists":
|
|
2626
|
+
return element !== null;
|
|
2627
|
+
case "notExists":
|
|
2628
|
+
return element === null;
|
|
2629
|
+
case "hasText": {
|
|
2630
|
+
if (!element) return false;
|
|
2631
|
+
const textContent = element.state?.textContent || element.accessibleName || element.label || element.description || "";
|
|
2632
|
+
return textContent.toLowerCase().includes(condition.text.toLowerCase());
|
|
2633
|
+
}
|
|
2634
|
+
default:
|
|
2635
|
+
return true;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Execute a single SpecAssertion.
|
|
2640
|
+
*/
|
|
2641
|
+
async executeAssertion(assertion) {
|
|
2642
|
+
if (!assertion.enabled) {
|
|
2643
|
+
return {
|
|
2644
|
+
assertionId: assertion.id,
|
|
2645
|
+
severity: assertion.severity,
|
|
2646
|
+
category: assertion.category,
|
|
2647
|
+
skipped: true,
|
|
2648
|
+
result: null
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
if (assertion.condition) {
|
|
2652
|
+
const conditionMet = await this.evaluateCondition(assertion.condition);
|
|
2653
|
+
if (!conditionMet) {
|
|
2654
|
+
return {
|
|
2655
|
+
assertionId: assertion.id,
|
|
2656
|
+
severity: assertion.severity,
|
|
2657
|
+
category: assertion.category,
|
|
2658
|
+
skipped: true,
|
|
2659
|
+
skipReason: "condition_not_met",
|
|
2660
|
+
result: null
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
const request = this.toAssertionRequest(assertion);
|
|
2665
|
+
const result = await this.assertionExecutor.assert(request);
|
|
2666
|
+
return {
|
|
2667
|
+
assertionId: assertion.id,
|
|
2668
|
+
severity: assertion.severity,
|
|
2669
|
+
category: assertion.category,
|
|
2670
|
+
skipped: false,
|
|
2671
|
+
result
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Execute all assertions in a SpecGroup.
|
|
2676
|
+
*/
|
|
2677
|
+
async executeGroup(group, options) {
|
|
2678
|
+
const startTime = Date.now();
|
|
2679
|
+
const assertionResults = [];
|
|
2680
|
+
let passedCount = 0;
|
|
2681
|
+
let failedCount = 0;
|
|
2682
|
+
let skippedCount = 0;
|
|
2683
|
+
for (const assertion of group.assertions) {
|
|
2684
|
+
if (shouldSkip(assertion, options)) {
|
|
2685
|
+
assertionResults.push({
|
|
2686
|
+
assertionId: assertion.id,
|
|
2687
|
+
groupId: group.id,
|
|
2688
|
+
severity: assertion.severity,
|
|
2689
|
+
category: assertion.category,
|
|
2690
|
+
skipped: true,
|
|
2691
|
+
result: null
|
|
2692
|
+
});
|
|
2693
|
+
skippedCount++;
|
|
2694
|
+
continue;
|
|
2695
|
+
}
|
|
2696
|
+
const result = await this.executeAssertion(assertion);
|
|
2697
|
+
result.groupId = group.id;
|
|
2698
|
+
assertionResults.push(result);
|
|
2699
|
+
if (result.skipped) {
|
|
2700
|
+
skippedCount++;
|
|
2701
|
+
} else if (result.result?.passed) {
|
|
2702
|
+
passedCount++;
|
|
2703
|
+
} else {
|
|
2704
|
+
failedCount++;
|
|
2705
|
+
if (options?.stopOnFailure) break;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
return {
|
|
2709
|
+
groupId: group.id,
|
|
2710
|
+
groupName: group.name,
|
|
2711
|
+
assertionResults,
|
|
2712
|
+
passedCount,
|
|
2713
|
+
failedCount,
|
|
2714
|
+
skippedCount,
|
|
2715
|
+
passed: failedCount === 0,
|
|
2716
|
+
durationMs: Date.now() - startTime,
|
|
2717
|
+
timestamp: Date.now()
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Execute a full SpecConfig.
|
|
2722
|
+
*/
|
|
2723
|
+
async execute(config, options) {
|
|
2724
|
+
const startTime = Date.now();
|
|
2725
|
+
const groupResults = [];
|
|
2726
|
+
for (const group of config.groups) {
|
|
2727
|
+
if (options?.groupIds && !options.groupIds.includes(group.id)) continue;
|
|
2728
|
+
const groupResult = await this.executeGroup(group, options);
|
|
2729
|
+
groupResults.push(groupResult);
|
|
2730
|
+
if (options?.stopOnFailure && !groupResult.passed) break;
|
|
2731
|
+
}
|
|
2732
|
+
const ungroupedResults = [];
|
|
2733
|
+
if (config.assertions) {
|
|
2734
|
+
for (const assertion of config.assertions) {
|
|
2735
|
+
if (shouldSkip(assertion, options)) {
|
|
2736
|
+
ungroupedResults.push({
|
|
2737
|
+
assertionId: assertion.id,
|
|
2738
|
+
severity: assertion.severity,
|
|
2739
|
+
category: assertion.category,
|
|
2740
|
+
skipped: true,
|
|
2741
|
+
result: null
|
|
2742
|
+
});
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
const result = await this.executeAssertion(assertion);
|
|
2746
|
+
ungroupedResults.push(result);
|
|
2747
|
+
if (options?.stopOnFailure && !result.skipped && !result.result?.passed) break;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
let passedCount = 0;
|
|
2751
|
+
let failedCount = 0;
|
|
2752
|
+
let skippedCount = 0;
|
|
2753
|
+
for (const gr of groupResults) {
|
|
2754
|
+
passedCount += gr.passedCount;
|
|
2755
|
+
failedCount += gr.failedCount;
|
|
2756
|
+
skippedCount += gr.skippedCount;
|
|
2757
|
+
}
|
|
2758
|
+
for (const ur of ungroupedResults) {
|
|
2759
|
+
if (ur.skipped) skippedCount++;
|
|
2760
|
+
else if (ur.result?.passed) passedCount++;
|
|
2761
|
+
else failedCount++;
|
|
2762
|
+
}
|
|
2763
|
+
return {
|
|
2764
|
+
specVersion: config.version ?? SPEC_CONFIG_VERSION,
|
|
2765
|
+
groupResults,
|
|
2766
|
+
ungroupedResults,
|
|
2767
|
+
totalAssertions: passedCount + failedCount + skippedCount,
|
|
2768
|
+
passedCount,
|
|
2769
|
+
failedCount,
|
|
2770
|
+
skippedCount,
|
|
2771
|
+
passed: failedCount === 0,
|
|
2772
|
+
durationMs: Date.now() - startTime,
|
|
2773
|
+
timestamp: Date.now()
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
};
|
|
2777
|
+
function shouldSkip(assertion, options) {
|
|
2778
|
+
if (!assertion.enabled) return true;
|
|
2779
|
+
if (options?.assertionIds && !options.assertionIds.includes(assertion.id)) return true;
|
|
2780
|
+
if (options?.categories && !options.categories.includes(assertion.category)) return true;
|
|
2781
|
+
if (options?.severities && !options.severities.includes(assertion.severity)) return true;
|
|
2782
|
+
if (options?.skipUnreviewed && !assertion.reviewed) return true;
|
|
2783
|
+
return false;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
exports.SPEC_CONFIG_VERSION = SPEC_CONFIG_VERSION;
|
|
2787
|
+
exports.SPEC_FILE_EXTENSION = SPEC_FILE_EXTENSION;
|
|
2788
|
+
exports.SpecExecutor = SpecExecutor;
|
|
2789
|
+
exports.SpecStore = SpecStore;
|
|
2790
|
+
exports.VALID_ASSERTION_TYPES = VALID_ASSERTION_TYPES;
|
|
2791
|
+
exports.VALID_SPEC_CATEGORIES = VALID_SPEC_CATEGORIES;
|
|
2792
|
+
exports.VALID_SPEC_SEVERITIES = VALID_SPEC_SEVERITIES;
|
|
2793
|
+
exports.VALID_SPEC_SOURCES = VALID_SPEC_SOURCES;
|
|
2794
|
+
exports.coerceAssertionType = coerceAssertionType;
|
|
2795
|
+
exports.getGlobalSpecStore = getGlobalSpecStore;
|
|
2796
|
+
exports.isValidAssertionType = isValidAssertionType;
|
|
2797
|
+
exports.isValidSpecCategory = isValidSpecCategory;
|
|
2798
|
+
exports.isValidSpecSeverity = isValidSpecSeverity;
|
|
2799
|
+
exports.isValidSpecSource = isValidSpecSource;
|
|
2800
|
+
exports.migrateFromTestGeneratorOutput = migrateFromTestGeneratorOutput;
|
|
2801
|
+
exports.migrateLegacyAssertion = migrateLegacyAssertion;
|
|
2802
|
+
exports.migrateLegacyTarget = migrateLegacyTarget;
|
|
2803
|
+
exports.resetGlobalSpecStore = resetGlobalSpecStore;
|
|
2804
|
+
exports.resolveTarget = resolveTarget;
|
|
2805
|
+
exports.validateSpecAssertion = validateSpecAssertion;
|
|
2806
|
+
exports.validateSpecConfig = validateSpecConfig;
|
|
2807
|
+
exports.validateSpecGroup = validateSpecGroup;
|
|
2808
|
+
//# sourceMappingURL=index.js.map
|
|
2809
|
+
//# sourceMappingURL=index.js.map
|