@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.
Files changed (40) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +220 -0
  2. package/bin/assets/preload/recorderv3.js +5 -3
  3. package/bin/assets/preload/unique_locators.js +1 -1
  4. package/bin/assets/scripts/aria_snapshot.js +235 -0
  5. package/bin/assets/scripts/dom_attr.js +372 -0
  6. package/bin/assets/scripts/dom_element.js +0 -0
  7. package/bin/assets/scripts/dom_parent.js +185 -0
  8. package/bin/assets/scripts/event_utils.js +105 -0
  9. package/bin/assets/scripts/pw.js +7886 -0
  10. package/bin/assets/scripts/recorder.js +1147 -0
  11. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  12. package/bin/assets/scripts/unique_locators.js +852 -0
  13. package/bin/assets/scripts/yaml.js +4770 -0
  14. package/bin/assets/templates/_hooks_template.txt +37 -0
  15. package/bin/assets/templates/page_template.txt +2 -16
  16. package/bin/assets/templates/utils_template.txt +44 -71
  17. package/bin/client/apiTest/apiTest.js +6 -0
  18. package/bin/client/cli_helpers.js +11 -13
  19. package/bin/client/code_cleanup/utils.js +36 -13
  20. package/bin/client/code_gen/code_inversion.js +68 -10
  21. package/bin/client/code_gen/page_reflection.js +12 -15
  22. package/bin/client/code_gen/playwright_codeget.js +127 -34
  23. package/bin/client/cucumber/feature.js +85 -27
  24. package/bin/client/cucumber/steps_definitions.js +84 -76
  25. package/bin/client/cucumber_selector.js +13 -1
  26. package/bin/client/local_agent.js +3 -3
  27. package/bin/client/project.js +7 -1
  28. package/bin/client/recorderv3/bvt_recorder.js +267 -87
  29. package/bin/client/recorderv3/implemented_steps.js +74 -12
  30. package/bin/client/recorderv3/index.js +58 -8
  31. package/bin/client/recorderv3/network.js +299 -0
  32. package/bin/client/recorderv3/step_runner.js +319 -67
  33. package/bin/client/recorderv3/step_utils.js +152 -5
  34. package/bin/client/recorderv3/update_feature.js +58 -30
  35. package/bin/client/recording.js +5 -0
  36. package/bin/client/run_cucumber.js +5 -1
  37. package/bin/client/scenario_report.js +0 -5
  38. package/bin/client/test_scenario.js +0 -1
  39. package/bin/index.js +1 -0
  40. 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) || role || "";
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/, /\[blinq-container/],
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