@dev-blinq/cucumber_client 1.0.1237-dev → 1.0.1237-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 +5 -3
- 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 +127 -34
- 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 +5 -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
|
@@ -920,7 +920,7 @@ class BVTRecorder {
|
|
|
920
920
|
this.contextElement = null;
|
|
921
921
|
}
|
|
922
922
|
getElementProperties(element) {
|
|
923
|
-
if (!element || !(element instanceof HTMLElement)) {
|
|
923
|
+
if (!element || !(element instanceof HTMLElement || element instanceof SVGElement)) {
|
|
924
924
|
throw new Error("Please provide a valid HTML element");
|
|
925
925
|
}
|
|
926
926
|
|
|
@@ -955,6 +955,7 @@ class BVTRecorder {
|
|
|
955
955
|
// Get all attributes
|
|
956
956
|
if (element.attributes) {
|
|
957
957
|
for (const attr of element.attributes) {
|
|
958
|
+
if (attr.name === "data-blinq-id" || attr.name === "data-input-id") continue;
|
|
958
959
|
unsortedAttributes[attr.name] = attr.value;
|
|
959
960
|
}
|
|
960
961
|
}
|
|
@@ -962,6 +963,7 @@ class BVTRecorder {
|
|
|
962
963
|
// Get dataset properties (data-* attributes)
|
|
963
964
|
if (element.dataset) {
|
|
964
965
|
for (const [key, value] of Object.entries(element.dataset)) {
|
|
966
|
+
if (key === "blinqId" || key === "inputId") continue;
|
|
965
967
|
unsortedDataset[key] = value;
|
|
966
968
|
}
|
|
967
969
|
}
|
|
@@ -989,7 +991,7 @@ class BVTRecorder {
|
|
|
989
991
|
el.__locators = this.getLocatorsObject(el);
|
|
990
992
|
}
|
|
991
993
|
const role = window.getAriaRole(el);
|
|
992
|
-
const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) ||
|
|
994
|
+
const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) || "";
|
|
993
995
|
const result = this.getElementProperties(el);
|
|
994
996
|
return {
|
|
995
997
|
role,
|
|
@@ -1188,7 +1190,7 @@ class BVTRecorder {
|
|
|
1188
1190
|
cssLocators.push(origenCss);
|
|
1189
1191
|
}
|
|
1190
1192
|
const noClasses = CssSelectorGenerator.getCssSelector(el, {
|
|
1191
|
-
blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id
|
|
1193
|
+
blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id/],
|
|
1192
1194
|
combineWithinSelector: true,
|
|
1193
1195
|
combineBetweenSelectors: true,
|
|
1194
1196
|
selectors: ["id", "attribute", "tag", "nthchild", "nthoftype"],
|
|
@@ -24,7 +24,7 @@ const getMatchingElements = (selector, options = {}) => {
|
|
|
24
24
|
const root = options?.root || window.document;
|
|
25
25
|
const prefix = options?.prefix;
|
|
26
26
|
if (prefix) {
|
|
27
|
-
selector = `${prefix} >> ${selector}`;
|
|
27
|
+
selector = `${prefix} >> ${selector} >> visible=true`;
|
|
28
28
|
}
|
|
29
29
|
return window.__injectedScript.querySelectorAll(window.__injectedScript.parseSelector(selector), root);
|
|
30
30
|
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
class AriaSnapshotUtils {
|
|
2
|
+
isLeafNode(node) {
|
|
3
|
+
if (node.kind === "text") {
|
|
4
|
+
return true;
|
|
5
|
+
} else {
|
|
6
|
+
return !node.children || node.children.length === 0;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
deepClone(node) {
|
|
11
|
+
if (node.kind === "text") {
|
|
12
|
+
return node.text;
|
|
13
|
+
} else {
|
|
14
|
+
const result = {
|
|
15
|
+
kind: "role",
|
|
16
|
+
role: node.role,
|
|
17
|
+
};
|
|
18
|
+
if ("checked" in node) result.checked = node.checked;
|
|
19
|
+
if ("disabled" in node) result.disabled = node.disabled;
|
|
20
|
+
if ("expanded" in node) result.expanded = node.expanded;
|
|
21
|
+
if ("level" in node) result.level = node.level;
|
|
22
|
+
if ("pressed" in node) result.pressed = node.pressed;
|
|
23
|
+
if ("selected" in node) result.selected = node.selected;
|
|
24
|
+
if (node.name !== undefined) {
|
|
25
|
+
result.name = node.name;
|
|
26
|
+
}
|
|
27
|
+
if (node.props) {
|
|
28
|
+
result.props = Object.assign({}, node.props);
|
|
29
|
+
}
|
|
30
|
+
if (node.containerMode) {
|
|
31
|
+
result.containerMode = node.containerMode;
|
|
32
|
+
}
|
|
33
|
+
if (node.children) {
|
|
34
|
+
result.children = node.children.map(child => this.deepClone(child));
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
yamlEscapeKeyIfNeeded(str) {
|
|
41
|
+
if (!this.yamlStringNeedsQuotes(str)) return str;
|
|
42
|
+
return `'` + str.replace(/'/g, `''`) + `'`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
yamlEscapeValueIfNeeded(str) {
|
|
46
|
+
if (!this.yamlStringNeedsQuotes(str)) return str;
|
|
47
|
+
return (
|
|
48
|
+
'"' +
|
|
49
|
+
str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, (c) => {
|
|
50
|
+
switch (c) {
|
|
51
|
+
case "\\":
|
|
52
|
+
return "\\\\";
|
|
53
|
+
case '"':
|
|
54
|
+
return '\\"';
|
|
55
|
+
case "\b":
|
|
56
|
+
return "\\b";
|
|
57
|
+
case "\f":
|
|
58
|
+
return "\\f";
|
|
59
|
+
case "\n":
|
|
60
|
+
return "\\n";
|
|
61
|
+
case "\r":
|
|
62
|
+
return "\\r";
|
|
63
|
+
case "\t":
|
|
64
|
+
return "\\t";
|
|
65
|
+
default:
|
|
66
|
+
const code = c.charCodeAt(0);
|
|
67
|
+
return "\\x" + code.toString(16).padStart(2, "0");
|
|
68
|
+
}
|
|
69
|
+
}) +
|
|
70
|
+
'"'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
yamlStringNeedsQuotes(str) {
|
|
75
|
+
if (!str) return false;
|
|
76
|
+
if (str.length === 0) return true;
|
|
77
|
+
if (/^\s|\s$/.test(str)) return true;
|
|
78
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(str)) return true;
|
|
79
|
+
if (/^-/.test(str)) return true;
|
|
80
|
+
if (/[\n:](\s|$)/.test(str)) return true;
|
|
81
|
+
if (/\s#/.test(str)) return true;
|
|
82
|
+
if (/[\n\r]/.test(str)) return true;
|
|
83
|
+
if (/^[&*\],?!>|@"'#%]/.test(str)) return true;
|
|
84
|
+
if (/[{}`]/.test(str)) return true;
|
|
85
|
+
if (/^\[/.test(str)) return true;
|
|
86
|
+
if (!isNaN(Number(str)) || ["y", "n", "yes", "no", "true", "false", "on", "off", "null"].includes(str.toLowerCase()))
|
|
87
|
+
return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
filterParentToChildren(parent, targetChildren) {
|
|
92
|
+
const isDirectMatch = targetChildren.some((child) => JSON.stringify(child) === JSON.stringify(parent));
|
|
93
|
+
if (isDirectMatch || this.isLeafNode(parent)) {
|
|
94
|
+
return isDirectMatch ? this.deepClone(parent) : null;
|
|
95
|
+
}
|
|
96
|
+
if (parent.kind === "role" && parent.children && parent.children.length > 0) {
|
|
97
|
+
const filteredChildren = parent.children
|
|
98
|
+
.map((child) => this.filterParentToChildren(child, targetChildren))
|
|
99
|
+
.filter((child) => child !== null);
|
|
100
|
+
if (filteredChildren.length > 0) {
|
|
101
|
+
const result = this.deepClone(parent);
|
|
102
|
+
result.children = filteredChildren;
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
serializeAriaSnapshot(ariaSnapshot, indent = 0) {
|
|
110
|
+
const lines = [];
|
|
111
|
+
const visit = (node, parentNode, indent) => {
|
|
112
|
+
if (typeof node === "string") {
|
|
113
|
+
const text = this.yamlEscapeValueIfNeeded(node);
|
|
114
|
+
if (text) lines.push(indent + "- text: " + text);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let key = node.role;
|
|
118
|
+
|
|
119
|
+
if (node.name && node.name.length <= 900) {
|
|
120
|
+
const name = node.name;
|
|
121
|
+
if (name) {
|
|
122
|
+
const stringifiedName = name.startsWith("/") && name.endsWith("/") ? name : JSON.stringify(name);
|
|
123
|
+
key += " " + stringifiedName;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (node.checked === "mixed") key += ` [checked=mixed]`;
|
|
127
|
+
if (node.checked === true) key += ` [checked]`;
|
|
128
|
+
if (node.disabled) key += ` [disabled]`;
|
|
129
|
+
if (node.expanded) key += ` [expanded]`;
|
|
130
|
+
if (node.level) key += ` [level=${node.level}]`;
|
|
131
|
+
if (node.pressed === "mixed") key += ` [pressed=mixed]`;
|
|
132
|
+
if (node.pressed === true) key += ` [pressed]`;
|
|
133
|
+
if (node.selected === true) key += ` [selected]`;
|
|
134
|
+
|
|
135
|
+
const escapedKey = indent + "- " + this.yamlEscapeKeyIfNeeded(key);
|
|
136
|
+
if (node.props === undefined) node.props = {};
|
|
137
|
+
if (node.children === undefined) node.children = [];
|
|
138
|
+
const hasProps = !!Object.keys(node.props).length;
|
|
139
|
+
if (!node.children.length && !hasProps) {
|
|
140
|
+
lines.push(escapedKey);
|
|
141
|
+
} else if (node.children.length === 1 && typeof node.children[0] === "string" && !hasProps) {
|
|
142
|
+
const text = node.children[0];
|
|
143
|
+
if (text) lines.push(escapedKey + ": " + this.yamlEscapeValueIfNeeded(text));
|
|
144
|
+
else lines.push(escapedKey);
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(escapedKey + ":");
|
|
147
|
+
for (const [name, value] of Object.entries(node.props) || []) {
|
|
148
|
+
lines.push(indent + " - /" + name + ": " + this.yamlEscapeValueIfNeeded(value));
|
|
149
|
+
}
|
|
150
|
+
for (const child of node.children || []) {
|
|
151
|
+
visit(child, node, indent + " ");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const node = ariaSnapshot;
|
|
157
|
+
if (node.role === "fragment") {
|
|
158
|
+
for (const child of node.children || []) visit(child, node, "");
|
|
159
|
+
} else {
|
|
160
|
+
visit(node, null, "");
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
isSnapshotAvailable(element, elementSet) {
|
|
166
|
+
if (elementSet.has(element)) {
|
|
167
|
+
return element;
|
|
168
|
+
} else {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
findMatchingElements(inputSnapshot, objectSet) {
|
|
174
|
+
const matches = new Map();
|
|
175
|
+
const lines = inputSnapshot.trim().split("\n");
|
|
176
|
+
const subtrees = this.extractSubtrees(lines);
|
|
177
|
+
|
|
178
|
+
subtrees.forEach((subtree) => {
|
|
179
|
+
const subtreeText = subtree.map((t) => t.trim()).join("\n");
|
|
180
|
+
for (const obj of objectSet) {
|
|
181
|
+
const normalizedSubtree = subtreeText.trim();
|
|
182
|
+
const normalizedSnapshot = obj.snapshot
|
|
183
|
+
.split("\n")
|
|
184
|
+
.map((s) => s.trim())
|
|
185
|
+
.join("\n");
|
|
186
|
+
if (normalizedSnapshot === normalizedSubtree) {
|
|
187
|
+
matches.set(subtreeText, obj.element);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return matches;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
extractSubtrees(lines) {
|
|
197
|
+
const subtrees = [];
|
|
198
|
+
const indentSize = this.getIndentSize(lines);
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < lines.length; i++) {
|
|
201
|
+
const currentLine = lines[i];
|
|
202
|
+
const currentIndent = this.getIndentLevel(currentLine);
|
|
203
|
+
const subtree = [currentLine];
|
|
204
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
205
|
+
const nextLine = lines[j];
|
|
206
|
+
const nextIndent = this.getIndentLevel(nextLine);
|
|
207
|
+
if (nextIndent > currentIndent) {
|
|
208
|
+
subtree.push(nextLine);
|
|
209
|
+
} else {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
subtrees.push(subtree);
|
|
214
|
+
}
|
|
215
|
+
subtrees.sort((a, b) => a.length - b.length);
|
|
216
|
+
return subtrees;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getIndentLevel(line) {
|
|
220
|
+
const match = line.match(/^(\s*)/);
|
|
221
|
+
return match ? match[1].length : 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getIndentSize(lines) {
|
|
225
|
+
for (let i = 1; i < lines.length; i++) {
|
|
226
|
+
const indentLevel = this.getIndentLevel(lines[i]);
|
|
227
|
+
if (indentLevel > 0) {
|
|
228
|
+
return indentLevel - this.getIndentLevel(lines[i - 1]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return 2;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default AriaSnapshotUtils
|
|
@@ -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
|