@dev-blinq/cucumber_client 1.0.1236-dev → 1.0.1236-stage
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/bin/assets/bundled_scripts/recorder.js +220 -0
- package/bin/assets/preload/recorderv3.js +65 -8
- package/bin/assets/preload/unique_locators.js +1 -1
- package/bin/assets/scripts/aria_snapshot.js +235 -0
- package/bin/assets/scripts/dom_attr.js +372 -0
- package/bin/assets/scripts/dom_element.js +0 -0
- package/bin/assets/scripts/dom_parent.js +185 -0
- package/bin/assets/scripts/event_utils.js +105 -0
- package/bin/assets/scripts/pw.js +7886 -0
- package/bin/assets/scripts/recorder.js +1147 -0
- package/bin/assets/scripts/snapshot_capturer.js +155 -0
- package/bin/assets/scripts/unique_locators.js +852 -0
- package/bin/assets/scripts/yaml.js +4770 -0
- package/bin/assets/templates/_hooks_template.txt +37 -0
- package/bin/assets/templates/page_template.txt +2 -16
- package/bin/assets/templates/utils_template.txt +44 -71
- package/bin/client/apiTest/apiTest.js +6 -0
- package/bin/client/cli_helpers.js +11 -13
- package/bin/client/code_cleanup/utils.js +36 -13
- package/bin/client/code_gen/code_inversion.js +68 -10
- package/bin/client/code_gen/page_reflection.js +12 -15
- package/bin/client/code_gen/playwright_codeget.js +163 -33
- package/bin/client/cucumber/feature.js +85 -27
- package/bin/client/cucumber/steps_definitions.js +84 -76
- package/bin/client/cucumber_selector.js +13 -1
- package/bin/client/local_agent.js +3 -3
- package/bin/client/project.js +7 -1
- package/bin/client/recorderv3/bvt_recorder.js +267 -87
- package/bin/client/recorderv3/implemented_steps.js +74 -12
- package/bin/client/recorderv3/index.js +58 -8
- package/bin/client/recorderv3/network.js +299 -0
- package/bin/client/recorderv3/step_runner.js +319 -67
- package/bin/client/recorderv3/step_utils.js +152 -5
- package/bin/client/recorderv3/update_feature.js +58 -30
- package/bin/client/recording.js +7 -0
- package/bin/client/run_cucumber.js +5 -1
- package/bin/client/scenario_report.js +0 -5
- package/bin/client/test_scenario.js +0 -1
- package/bin/index.js +1 -0
- package/package.json +17 -9
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
|
|
2
|
+
class DOM_Attr {
|
|
3
|
+
/**
|
|
4
|
+
* Finds attributes that are unique to the target element
|
|
5
|
+
* @param {Element} target - Target element
|
|
6
|
+
* @param {Element[]} similarElements - Array of similar elements to compare against
|
|
7
|
+
* @returns {Object} - Object with unique attribute names as keys and their values
|
|
8
|
+
*/
|
|
9
|
+
findUniqueAttributes(target, similarElements) {
|
|
10
|
+
if (!target || !Array.isArray(similarElements)) return {};
|
|
11
|
+
|
|
12
|
+
const targetAttrs = getElementAttributes(target);
|
|
13
|
+
const uniqueAttrs = {};
|
|
14
|
+
|
|
15
|
+
// Check each attribute of the target
|
|
16
|
+
for (const [attrName, attrValue] of Object.entries(targetAttrs)) {
|
|
17
|
+
let isUnique = true;
|
|
18
|
+
|
|
19
|
+
// Check if any similar element has the same attribute with the same value
|
|
20
|
+
for (const element of similarElements) {
|
|
21
|
+
if (element === target) continue; // Skip self
|
|
22
|
+
|
|
23
|
+
const elementAttrs = getElementAttributes(element);
|
|
24
|
+
if (elementAttrs[attrName] === attrValue) {
|
|
25
|
+
isUnique = false;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isUnique) {
|
|
31
|
+
uniqueAttrs[attrName] = attrValue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return uniqueAttrs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Finds attributes that exist on target but not on similar elements
|
|
40
|
+
* @param {Element} target - Target element
|
|
41
|
+
* @param {Element[]} similarElements - Array of similar elements to compare against
|
|
42
|
+
* @returns {Object} - Object with attribute names that only target has
|
|
43
|
+
*/
|
|
44
|
+
findExclusiveAttributes(target, similarElements) {
|
|
45
|
+
if (!target || !Array.isArray(similarElements)) return {};
|
|
46
|
+
|
|
47
|
+
const targetAttrs = getElementAttributes(target);
|
|
48
|
+
const exclusiveAttrs = {};
|
|
49
|
+
|
|
50
|
+
// Check each attribute of the target
|
|
51
|
+
for (const [attrName, attrValue] of Object.entries(targetAttrs)) {
|
|
52
|
+
let hasAttribute = false;
|
|
53
|
+
|
|
54
|
+
// Check if any similar element has this attribute (regardless of value)
|
|
55
|
+
for (const element of similarElements) {
|
|
56
|
+
if (element === target) continue; // Skip self
|
|
57
|
+
|
|
58
|
+
if (element.hasAttribute(attrName)) {
|
|
59
|
+
hasAttribute = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!hasAttribute) {
|
|
65
|
+
exclusiveAttrs[attrName] = attrValue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return exclusiveAttrs;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Finds classnames that are unique to the target element
|
|
73
|
+
* @param {Element} target - Target element
|
|
74
|
+
* @param {Element[]} similarElements - Array of similar elements to compare against
|
|
75
|
+
* @returns {Set<string>} - Set of unique classnames
|
|
76
|
+
*/
|
|
77
|
+
findUniqueClassNames(target, similarElements) {
|
|
78
|
+
if (!target || !Array.isArray(similarElements)) return new Set();
|
|
79
|
+
|
|
80
|
+
const targetClasses = getElementClassNames(target);
|
|
81
|
+
const uniqueClasses = new Set();
|
|
82
|
+
|
|
83
|
+
// Check each class of the target
|
|
84
|
+
for (const className of targetClasses) {
|
|
85
|
+
let isUnique = true;
|
|
86
|
+
|
|
87
|
+
// Check if any similar element has the same class
|
|
88
|
+
for (const element of similarElements) {
|
|
89
|
+
if (element === target) continue; // Skip self
|
|
90
|
+
|
|
91
|
+
const elementClasses = getElementClassNames(element);
|
|
92
|
+
if (elementClasses.has(className)) {
|
|
93
|
+
isUnique = false;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (isUnique) {
|
|
99
|
+
uniqueClasses.add(className);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return uniqueClasses;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Finds attributes with different values between target and similar elements
|
|
108
|
+
* @param {Element} target - Target element
|
|
109
|
+
* @param {Element[]} similarElements - Array of similar elements to compare against
|
|
110
|
+
* @returns {Object} - Object with attribute names as keys and comparison info as values
|
|
111
|
+
*/
|
|
112
|
+
findDifferentValueAttributes(target, similarElements) {
|
|
113
|
+
if (!target || !Array.isArray(similarElements)) return {};
|
|
114
|
+
|
|
115
|
+
const targetAttrs = getElementAttributes(target);
|
|
116
|
+
const differentAttrs = {};
|
|
117
|
+
|
|
118
|
+
// Get all attributes from all elements
|
|
119
|
+
const allAttributes = new Set(Object.keys(targetAttrs));
|
|
120
|
+
similarElements.forEach(element => {
|
|
121
|
+
if (element !== target) {
|
|
122
|
+
const attrs = getElementAttributes(element);
|
|
123
|
+
Object.keys(attrs).forEach(attr => allAttributes.add(attr));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Check each attribute
|
|
128
|
+
for (const attrName of allAttributes) {
|
|
129
|
+
const targetValue = targetAttrs[attrName];
|
|
130
|
+
const otherValues = [];
|
|
131
|
+
|
|
132
|
+
similarElements.forEach(element => {
|
|
133
|
+
if (element !== target) {
|
|
134
|
+
const attrs = getElementAttributes(element);
|
|
135
|
+
const value = attrs[attrName];
|
|
136
|
+
if (value !== undefined && value !== targetValue) {
|
|
137
|
+
otherValues.push(value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (otherValues.length > 0) {
|
|
143
|
+
differentAttrs[attrName] = {
|
|
144
|
+
targetValue: targetValue || null,
|
|
145
|
+
otherValues: [...new Set(otherValues)] // Remove duplicates
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return differentAttrs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Main function: Finds all differentiating attributes and classnames
|
|
155
|
+
* @param {Element} target - Target element to find differentiators for
|
|
156
|
+
* @param {Element[]} similarElements - Array of similar elements to compare against
|
|
157
|
+
* @param {Object} options - Options for the comparison
|
|
158
|
+
* @param {boolean} options.includeComputedStyles - Whether to include computed style differences
|
|
159
|
+
* @param {string[]} options.styleProperties - Array of CSS properties to check if includeComputedStyles is true
|
|
160
|
+
* @param {boolean} options.ignoreDataAttributes - Whether to ignore data-* attributes
|
|
161
|
+
* @param {boolean} options.ignoreAriaAttributes - Whether to ignore aria-* attributes
|
|
162
|
+
* @param {string[]} options.ignoreAttributes - Array of attribute names to ignore
|
|
163
|
+
* @param {boolean} options.caseSensitiveClasses - Whether class comparison should be case sensitive
|
|
164
|
+
* @returns {Object} - Comprehensive differentiating information
|
|
165
|
+
*/
|
|
166
|
+
findDifferentiatingAttributes(target, similarElements, options = {}) {
|
|
167
|
+
const {
|
|
168
|
+
includeComputedStyles = false,
|
|
169
|
+
styleProperties = ['color', 'background-color', 'font-size', 'display', 'position'],
|
|
170
|
+
ignoreDataAttributes = false,
|
|
171
|
+
ignoreAriaAttributes = false,
|
|
172
|
+
ignoreAttributes = [],
|
|
173
|
+
caseSensitiveClasses = true
|
|
174
|
+
} = options;
|
|
175
|
+
|
|
176
|
+
// Validate inputs
|
|
177
|
+
if (!target) {
|
|
178
|
+
throw new Error('Target element is required');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!Array.isArray(similarElements)) {
|
|
182
|
+
throw new Error('similarElements must be an array');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Filter out the target from similar elements if it exists
|
|
186
|
+
const filteredSimilarElements = similarElements.filter(el => el !== target);
|
|
187
|
+
|
|
188
|
+
if (filteredSimilarElements.length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
uniqueAttributes: getElementAttributes(target),
|
|
191
|
+
exclusiveAttributes: getElementAttributes(target),
|
|
192
|
+
uniqueClassNames: getElementClassNames(target),
|
|
193
|
+
differentValueAttributes: {},
|
|
194
|
+
computedStyleDifferences: {},
|
|
195
|
+
summary: {
|
|
196
|
+
hasUniqueAttributes: Object.keys(getElementAttributes(target)).length > 0,
|
|
197
|
+
hasUniqueClasses: getElementClassNames(target).size > 0,
|
|
198
|
+
totalDifferences: Object.keys(getElementAttributes(target)).length + getElementClassNames(target).size
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Helper function to filter attributes based on options
|
|
204
|
+
const shouldIgnoreAttribute = (attrName) => {
|
|
205
|
+
if (ignoreAttributes.includes(attrName)) return true;
|
|
206
|
+
if (ignoreDataAttributes && attrName.startsWith('data-')) return true;
|
|
207
|
+
if (ignoreAriaAttributes && attrName.startsWith('aria-')) return true;
|
|
208
|
+
return false;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Filter attributes for target and similar elements
|
|
212
|
+
const filterAttributes = (attrs) => {
|
|
213
|
+
const filtered = {};
|
|
214
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
215
|
+
if (!shouldIgnoreAttribute(name)) {
|
|
216
|
+
filtered[name] = value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return filtered;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Get filtered attributes
|
|
223
|
+
const targetAttrs = filterAttributes(getElementAttributes(target));
|
|
224
|
+
|
|
225
|
+
// Temporarily override attribute getting to use filtered attributes
|
|
226
|
+
const originalTarget = { ...target };
|
|
227
|
+
const originalGetElementAttributes = getElementAttributes;
|
|
228
|
+
|
|
229
|
+
const getFilteredAttributes = (element) => {
|
|
230
|
+
return filterAttributes(originalGetElementAttributes(element));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Find unique attributes (same name and value combination is unique)
|
|
234
|
+
const uniqueAttributes = {};
|
|
235
|
+
for (const [attrName, attrValue] of Object.entries(targetAttrs)) {
|
|
236
|
+
let isUnique = true;
|
|
237
|
+
|
|
238
|
+
for (const element of filteredSimilarElements) {
|
|
239
|
+
const elementAttrs = getFilteredAttributes(element);
|
|
240
|
+
if (elementAttrs[attrName] === attrValue) {
|
|
241
|
+
isUnique = false;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isUnique) {
|
|
247
|
+
uniqueAttributes[attrName] = attrValue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Find exclusive attributes (target has, others don't)
|
|
252
|
+
const exclusiveAttributes = {};
|
|
253
|
+
for (const [attrName, attrValue] of Object.entries(targetAttrs)) {
|
|
254
|
+
let hasAttribute = false;
|
|
255
|
+
|
|
256
|
+
for (const element of filteredSimilarElements) {
|
|
257
|
+
if (element.hasAttribute(attrName) && !shouldIgnoreAttribute(attrName)) {
|
|
258
|
+
hasAttribute = true;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!hasAttribute) {
|
|
264
|
+
exclusiveAttributes[attrName] = attrValue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Find unique classnames
|
|
269
|
+
const targetClasses = getElementClassNames(target);
|
|
270
|
+
const uniqueClassNames = new Set();
|
|
271
|
+
|
|
272
|
+
for (const className of targetClasses) {
|
|
273
|
+
let isUnique = true;
|
|
274
|
+
|
|
275
|
+
for (const element of filteredSimilarElements) {
|
|
276
|
+
const elementClasses = getElementClassNames(element);
|
|
277
|
+
const hasClass = caseSensitiveClasses
|
|
278
|
+
? elementClasses.has(className)
|
|
279
|
+
: Array.from(elementClasses).some(cls => cls.toLowerCase() === className.toLowerCase());
|
|
280
|
+
|
|
281
|
+
if (hasClass) {
|
|
282
|
+
isUnique = false;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (isUnique) {
|
|
288
|
+
uniqueClassNames.add(className);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Find different value attributes
|
|
293
|
+
const differentValueAttributes = {};
|
|
294
|
+
const allAttributes = new Set(Object.keys(targetAttrs));
|
|
295
|
+
|
|
296
|
+
filteredSimilarElements.forEach(element => {
|
|
297
|
+
const attrs = getFilteredAttributes(element);
|
|
298
|
+
Object.keys(attrs).forEach(attr => allAttributes.add(attr));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
for (const attrName of allAttributes) {
|
|
302
|
+
const targetValue = targetAttrs[attrName];
|
|
303
|
+
const otherValues = [];
|
|
304
|
+
|
|
305
|
+
filteredSimilarElements.forEach(element => {
|
|
306
|
+
const attrs = getFilteredAttributes(element);
|
|
307
|
+
const value = attrs[attrName];
|
|
308
|
+
if (value !== undefined && value !== targetValue) {
|
|
309
|
+
otherValues.push(value);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (otherValues.length > 0) {
|
|
314
|
+
differentValueAttributes[attrName] = {
|
|
315
|
+
targetValue: targetValue || null,
|
|
316
|
+
otherValues: [...new Set(otherValues)]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Computed style differences
|
|
322
|
+
let computedStyleDifferences = {};
|
|
323
|
+
if (includeComputedStyles) {
|
|
324
|
+
const targetStyles = getComputedStyles(target, styleProperties);
|
|
325
|
+
|
|
326
|
+
for (const property of styleProperties) {
|
|
327
|
+
const targetValue = targetStyles[property];
|
|
328
|
+
const otherValues = [];
|
|
329
|
+
|
|
330
|
+
filteredSimilarElements.forEach(element => {
|
|
331
|
+
const elementStyles = getComputedStyles(element, [property]);
|
|
332
|
+
const value = elementStyles[property];
|
|
333
|
+
if (value && value !== targetValue) {
|
|
334
|
+
otherValues.push(value);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (otherValues.length > 0) {
|
|
339
|
+
computedStyleDifferences[property] = {
|
|
340
|
+
targetValue,
|
|
341
|
+
otherValues: [...new Set(otherValues)]
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Generate summary
|
|
348
|
+
const summary = {
|
|
349
|
+
hasUniqueAttributes: Object.keys(uniqueAttributes).length > 0,
|
|
350
|
+
hasExclusiveAttributes: Object.keys(exclusiveAttributes).length > 0,
|
|
351
|
+
hasUniqueClasses: uniqueClassNames.size > 0,
|
|
352
|
+
hasDifferentValues: Object.keys(differentValueAttributes).length > 0,
|
|
353
|
+
hasStyleDifferences: Object.keys(computedStyleDifferences).length > 0,
|
|
354
|
+
totalDifferences: Object.keys(uniqueAttributes).length +
|
|
355
|
+
uniqueClassNames.size +
|
|
356
|
+
Object.keys(differentValueAttributes).length +
|
|
357
|
+
Object.keys(computedStyleDifferences).length,
|
|
358
|
+
comparedAgainst: filteredSimilarElements.length
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
uniqueAttributes,
|
|
363
|
+
exclusiveAttributes,
|
|
364
|
+
uniqueClassNames,
|
|
365
|
+
differentValueAttributes,
|
|
366
|
+
computedStyleDifferences,
|
|
367
|
+
summary
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export default DOM_Attr;
|
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
class DOM_Parent {
|
|
2
|
+
|
|
3
|
+
getActualParent(element) {
|
|
4
|
+
if (!element || !(element instanceof Element)) {
|
|
5
|
+
throw new Error('Invalid element provided');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// TODO: account for slotted elements
|
|
9
|
+
|
|
10
|
+
if (element.assignedSlot) {
|
|
11
|
+
return element.assignedSlot
|
|
12
|
+
}
|
|
13
|
+
// Get the actual parent element, skipping shadow DOM if necessary
|
|
14
|
+
let parent = element.parentElement;
|
|
15
|
+
if (parent) {
|
|
16
|
+
return parent;
|
|
17
|
+
}
|
|
18
|
+
const parentNode = element.parentNode;
|
|
19
|
+
if (parentNode && parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
20
|
+
// If the parent is a shadow root, return its host
|
|
21
|
+
return parentNode.host || null;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getFullAncestorChain(element) {
|
|
28
|
+
if (!element || !(element instanceof Element)) {
|
|
29
|
+
throw new Error('Invalid element provided');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ancestors = [];
|
|
33
|
+
let currentElement = element;
|
|
34
|
+
|
|
35
|
+
while (currentElement) {
|
|
36
|
+
ancestors.push(currentElement);
|
|
37
|
+
currentElement = this.getActualParent(currentElement);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return ancestors;
|
|
41
|
+
}
|
|
42
|
+
getFullAncestorChainToRoot(element, root) {
|
|
43
|
+
if (!element || !(element instanceof Element)) {
|
|
44
|
+
throw new Error('Invalid element provided');
|
|
45
|
+
}
|
|
46
|
+
if (!root || !(root instanceof Element)) {
|
|
47
|
+
throw new Error('Invalid root provided');
|
|
48
|
+
}
|
|
49
|
+
if (!this.containsElementCrossShadow(root, element)) {
|
|
50
|
+
throw new Error('Root does not contain the element');
|
|
51
|
+
}
|
|
52
|
+
const ancestors = [];
|
|
53
|
+
let currentElement = element;
|
|
54
|
+
while (currentElement && currentElement !== root && this.containsElementCrossShadow(root, currentElement)) {
|
|
55
|
+
ancestors.push(currentElement);
|
|
56
|
+
currentElement = this.getActualParent(currentElement);
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
if (currentElement === root) {
|
|
60
|
+
ancestors.push(currentElement);
|
|
61
|
+
}
|
|
62
|
+
return ancestors;
|
|
63
|
+
}
|
|
64
|
+
getClimbCountToParent(element, targetParent) {
|
|
65
|
+
if (!element || !(element instanceof Element)) {
|
|
66
|
+
throw new Error('Invalid element provided');
|
|
67
|
+
}
|
|
68
|
+
if (!targetParent || !(targetParent instanceof Element)) {
|
|
69
|
+
throw new Error('Invalid target parent provided');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let count = 0;
|
|
73
|
+
let currentElement = element;
|
|
74
|
+
|
|
75
|
+
while (currentElement && currentElement !== targetParent) {
|
|
76
|
+
currentElement = this.getActualParent(currentElement);
|
|
77
|
+
count++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return currentElement === targetParent ? count : -1; // Return -1 if target parent is not found
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
containsElementCrossShadow(element, target) {
|
|
84
|
+
if (!element || !(element instanceof Element)) {
|
|
85
|
+
throw new Error('Invalid element provided');
|
|
86
|
+
}
|
|
87
|
+
if (!target || !(target instanceof Element)) {
|
|
88
|
+
throw new Error('Invalid target provided');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if the element contains the target, accounting for shadow DOM
|
|
92
|
+
let currentElement = target;
|
|
93
|
+
|
|
94
|
+
while (currentElement) {
|
|
95
|
+
if (currentElement === element) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
currentElement = this.getActualParent(currentElement);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
findLowestCommonAncestor(elements) {
|
|
105
|
+
if (!Array.isArray(elements) || elements.length === 0) {
|
|
106
|
+
throw new Error('Invalid elements array provided');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Ensure all elements are valid
|
|
110
|
+
for (const el of elements) {
|
|
111
|
+
if (!(el instanceof Element)) {
|
|
112
|
+
throw new Error('All items in the array must be DOM Elements');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Start with the first element's ancestors
|
|
117
|
+
let commonAncestors = this.getFullAncestorChain(elements[0]);
|
|
118
|
+
|
|
119
|
+
commonAncestors.reverse(); // Reverse to start from the closest ancestor
|
|
120
|
+
|
|
121
|
+
// Iterate through the rest of the elements
|
|
122
|
+
for (let i = 1; i < elements.length; i++) {
|
|
123
|
+
const currentAncestors = this.getFullAncestorChain(elements[i]);
|
|
124
|
+
currentAncestors.reverse(); // Reverse to start from the closest ancestor
|
|
125
|
+
commonAncestors = commonAncestors.filter(ancestor =>
|
|
126
|
+
currentAncestors.includes(ancestor)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Return the lowest common ancestor, or null if none found
|
|
131
|
+
return commonAncestors.length > 0 ? commonAncestors[commonAncestors.length - 1] : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Finds the branching parent for a target element within an array of elements
|
|
136
|
+
* The branching parent is the direct child of the common ancestor that contains the target
|
|
137
|
+
* @param {Element[]} elements - Array of elements (target must be included)
|
|
138
|
+
* @param {Element} target - Target element to find branching parent for
|
|
139
|
+
|
|
140
|
+
* @returns {Element|null} - The branching parent element, or null if not found
|
|
141
|
+
*/
|
|
142
|
+
findBranchingParent(elements, target) {
|
|
143
|
+
if (!Array.isArray(elements) || elements.length === 0) {
|
|
144
|
+
throw new Error('Invalid elements array provided');
|
|
145
|
+
}
|
|
146
|
+
if (!(target instanceof Element)) {
|
|
147
|
+
throw new Error('Target must be a DOM Element');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Ensure all elements are valid
|
|
151
|
+
for (const el of elements) {
|
|
152
|
+
if (!(el instanceof Element)) {
|
|
153
|
+
throw new Error('All items in the array must be DOM Elements');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Check if the target is in the elements array
|
|
157
|
+
if (!elements.includes(target)) {
|
|
158
|
+
// throw new Error("Target element must be included in the elements array");
|
|
159
|
+
console.warn("Target element must be included in the elements array, returning null");
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const allTargetparents = this.getFullAncestorChain(target);
|
|
163
|
+
|
|
164
|
+
// reverse the order of parents to start from the closest parent
|
|
165
|
+
allTargetparents.reverse();
|
|
166
|
+
|
|
167
|
+
// Find the parent that only contains the target and no other elements
|
|
168
|
+
let branchingParent = null;
|
|
169
|
+
for (const parent of allTargetparents) {
|
|
170
|
+
/// find all elements that are inside this parent
|
|
171
|
+
const allElementsThatAreInsideParent = elements.filter(el => this.containsElementCrossShadow(parent, el));
|
|
172
|
+
// If the parent contains only the target element, it is the branching parent
|
|
173
|
+
if (allElementsThatAreInsideParent.length === 1 && allElementsThatAreInsideParent[0] === target) {
|
|
174
|
+
branchingParent = parent;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (branchingParent) {
|
|
179
|
+
return branchingParent;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null; // No branching parent found
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export default DOM_Parent;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { __PW } from "./pw";
|
|
2
|
+
|
|
3
|
+
const closestCrossShadow = __PW.domUtils.closestCrossShadow;
|
|
4
|
+
const isElementVisible = __PW.domUtils.isElementVisible;
|
|
5
|
+
|
|
6
|
+
export default class EventUtils {
|
|
7
|
+
deepEventTarget(event) {
|
|
8
|
+
return event.composedPath()[0];
|
|
9
|
+
}
|
|
10
|
+
deepActiveElement(document) {
|
|
11
|
+
let activeElement = document.activeElement;
|
|
12
|
+
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
|
|
13
|
+
activeElement = activeElement.shadowRoot.activeElement;
|
|
14
|
+
return activeElement;
|
|
15
|
+
}
|
|
16
|
+
modifiersForEvent(event) {
|
|
17
|
+
return (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
|
|
18
|
+
}
|
|
19
|
+
buttonForEvent(event) {
|
|
20
|
+
switch (event.which) {
|
|
21
|
+
case 1:
|
|
22
|
+
return "left";
|
|
23
|
+
case 2:
|
|
24
|
+
return "middle";
|
|
25
|
+
case 3:
|
|
26
|
+
return "right";
|
|
27
|
+
}
|
|
28
|
+
return "left";
|
|
29
|
+
}
|
|
30
|
+
positionForEvent(event) {
|
|
31
|
+
const targetElement = event.target;
|
|
32
|
+
if (targetElement.nodeName !== "CANVAS") return;
|
|
33
|
+
return {
|
|
34
|
+
x: event.offsetX,
|
|
35
|
+
y: event.offsetY,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
consumeEvent(e) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
e.stopPropagation();
|
|
41
|
+
e.stopImmediatePropagation();
|
|
42
|
+
}
|
|
43
|
+
asCheckbox(node) {
|
|
44
|
+
if (!node || node.nodeName !== "INPUT") return null;
|
|
45
|
+
const inputElement = node;
|
|
46
|
+
return ["checkbox", "radio"].includes(inputElement.type) ? inputElement : null;
|
|
47
|
+
}
|
|
48
|
+
isRangeInput(node) {
|
|
49
|
+
if (!node || node.nodeName !== "INPUT") return false;
|
|
50
|
+
const inputElement = node;
|
|
51
|
+
return inputElement.type.toLowerCase() === "range";
|
|
52
|
+
}
|
|
53
|
+
addEventListener(target, eventName, listener, useCapture) {
|
|
54
|
+
target.addEventListener(eventName, listener, useCapture);
|
|
55
|
+
const remove = () => {
|
|
56
|
+
target.removeEventListener(eventName, listener, useCapture);
|
|
57
|
+
};
|
|
58
|
+
return remove;
|
|
59
|
+
}
|
|
60
|
+
removeEventListeners(listeners) {
|
|
61
|
+
for (const listener of listeners) listener();
|
|
62
|
+
listeners.splice(0, listeners.length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getNearestInteractiveElement(targetElement, root = window.document) {
|
|
66
|
+
try {
|
|
67
|
+
if (!targetElement.matches("input,textarea,select") && !targetElement.isContentEditable) {
|
|
68
|
+
const interactiveParent = closestCrossShadow(
|
|
69
|
+
targetElement,
|
|
70
|
+
"button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]",
|
|
71
|
+
root
|
|
72
|
+
);
|
|
73
|
+
if (interactiveParent && isElementVisible(interactiveParent)) return interactiveParent;
|
|
74
|
+
}
|
|
75
|
+
return targetElement;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error(e);
|
|
78
|
+
// bvtRecorderBindings.log(`Error in getNearestInteractiveElement: ${e.message}`);
|
|
79
|
+
return targetElement;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
shouldGenerateKeyPressFor(event) {
|
|
83
|
+
// Enter aka. new line is handled in input event.
|
|
84
|
+
if (
|
|
85
|
+
event.key === "Enter" &&
|
|
86
|
+
(this.deepEventTarget(event).nodeName === "TEXTAREA" || this.deepEventTarget(event).isContentEditable)
|
|
87
|
+
)
|
|
88
|
+
return false;
|
|
89
|
+
// Backspace, Delete, AltGraph are changing input, will handle it there.
|
|
90
|
+
if (["Backspace", "Delete", "AltGraph"].includes(event.key)) return false;
|
|
91
|
+
// Ignore the QWERTZ shortcut for creating a at sign on MacOS
|
|
92
|
+
if (event.key === "@" && event.code === "KeyL") return false;
|
|
93
|
+
// Allow and ignore common used shortcut for pasting.
|
|
94
|
+
if (navigator.platform.includes("Mac")) {
|
|
95
|
+
if (event.key === "v" && event.metaKey) return false;
|
|
96
|
+
} else {
|
|
97
|
+
if (event.key === "v" && event.ctrlKey) return false;
|
|
98
|
+
if (event.key === "Insert" && event.shiftKey) return false;
|
|
99
|
+
}
|
|
100
|
+
if (["Shift", "Control", "Meta", "Alt", "Process"].includes(event.key)) return false;
|
|
101
|
+
const hasModifier = event.ctrlKey || event.altKey || event.metaKey;
|
|
102
|
+
if (event.key.length === 1 && !hasModifier) return !!this.asCheckbox(this.deepEventTarget(event));
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|