@dev-blinq/cucumber_client 1.0.1198-dev → 1.0.1198-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 (39) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +220 -0
  2. package/bin/assets/preload/find_context.js +1 -1
  3. package/bin/assets/preload/recorderv3.js +75 -5
  4. package/bin/assets/preload/unique_locators.js +24 -3
  5. package/bin/assets/scripts/aria_snapshot.js +235 -0
  6. package/bin/assets/scripts/dom_attr.js +372 -0
  7. package/bin/assets/scripts/dom_element.js +0 -0
  8. package/bin/assets/scripts/dom_parent.js +185 -0
  9. package/bin/assets/scripts/event_utils.js +105 -0
  10. package/bin/assets/scripts/pw.js +7886 -0
  11. package/bin/assets/scripts/recorder.js +1147 -0
  12. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  13. package/bin/assets/scripts/unique_locators.js +841 -0
  14. package/bin/assets/scripts/yaml.js +4770 -0
  15. package/bin/assets/templates/page_template.txt +2 -16
  16. package/bin/assets/templates/utils_template.txt +59 -7
  17. package/bin/client/cli_helpers.js +11 -13
  18. package/bin/client/code_cleanup/utils.js +42 -14
  19. package/bin/client/code_gen/code_inversion.js +48 -11
  20. package/bin/client/code_gen/index.js +3 -0
  21. package/bin/client/code_gen/page_reflection.js +37 -20
  22. package/bin/client/code_gen/playwright_codeget.js +153 -25
  23. package/bin/client/cucumber/feature.js +92 -34
  24. package/bin/client/cucumber/steps_definitions.js +109 -83
  25. package/bin/client/local_agent.js +6 -2
  26. package/bin/client/project.js +6 -2
  27. package/bin/client/recorderv3/bvt_recorder.js +272 -76
  28. package/bin/client/recorderv3/implemented_steps.js +69 -14
  29. package/bin/client/recorderv3/index.js +49 -7
  30. package/bin/client/recorderv3/network.js +299 -0
  31. package/bin/client/recorderv3/step_runner.js +183 -13
  32. package/bin/client/recorderv3/step_utils.js +155 -8
  33. package/bin/client/recorderv3/update_feature.js +58 -30
  34. package/bin/client/recording.js +7 -0
  35. package/bin/client/run_cucumber.js +16 -2
  36. package/bin/client/scenario_report.js +18 -6
  37. package/bin/client/test_scenario.js +0 -1
  38. package/bin/index.js +1 -0
  39. package/package.json +15 -8
@@ -511,7 +511,7 @@ function findContext(element) {
511
511
  const textsInnerText = [];
512
512
  for (let i = 0; i < texts.length; i++) {
513
513
  const text = texts[i];
514
- textsInnerText.push(text.textContent);
514
+ textsInnerText.push(text.parentElement.textContent.trim());
515
515
  // set attribute to the text: blinq-text-${key}-${i}
516
516
  text.parentElement.setAttribute(`blinq-text-${key}-${i}`, "");
517
517
  }
@@ -919,6 +919,65 @@ class BVTRecorder {
919
919
  }
920
920
  this.contextElement = null;
921
921
  }
922
+ getElementProperties(element) {
923
+ if (!element || !(element instanceof HTMLElement || element instanceof SVGElement)) {
924
+ throw new Error("Please provide a valid HTML element");
925
+ }
926
+
927
+ const result = {
928
+ properties: {},
929
+ attributes: {},
930
+ dataset: {},
931
+ };
932
+
933
+ const unsortedProperties = {};
934
+ const unsortedAttributes = {};
935
+ const unsortedDataset = {};
936
+
937
+ // Get enumerable properties
938
+ for (const prop in element) {
939
+ try {
940
+ const value = element[prop];
941
+ if (
942
+ typeof value !== "function" &&
943
+ typeof value !== "object" &&
944
+ value !== null &&
945
+ value !== undefined &&
946
+ !prop.includes("_")
947
+ ) {
948
+ unsortedProperties[prop] = value;
949
+ }
950
+ } catch (error) {
951
+ unsortedProperties[prop] = "[Error accessing property]";
952
+ }
953
+ }
954
+
955
+ // Get all attributes
956
+ if (element.attributes) {
957
+ for (const attr of element.attributes) {
958
+ if (attr.name === "data-blinq-id" || attr.name === "data-input-id") continue;
959
+ unsortedAttributes[attr.name] = attr.value;
960
+ }
961
+ }
962
+
963
+ // Get dataset properties (data-* attributes)
964
+ if (element.dataset) {
965
+ for (const [key, value] of Object.entries(element.dataset)) {
966
+ if (key === "blinqId" || key === "inputId") continue;
967
+ unsortedDataset[key] = value;
968
+ }
969
+ }
970
+
971
+ // Sort each object by key
972
+ const sortByKey = (obj) => Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
973
+
974
+ result.properties = sortByKey(unsortedProperties);
975
+ result.attributes = sortByKey(unsortedAttributes);
976
+ result.dataset = sortByKey(unsortedDataset);
977
+
978
+ return result;
979
+ }
980
+
922
981
  getElementDetails(el, type) {
923
982
  if (lastInputId !== el.dataset.inputId || type !== "input") {
924
983
  el.dataset.inputId = getNextInputId();
@@ -928,9 +987,12 @@ class BVTRecorder {
928
987
  // }
929
988
 
930
989
  lastInputId = el.dataset.inputId;
990
+
991
+ el.__locators = this.getLocatorsObject(el);
931
992
  }
932
993
  const role = window.getAriaRole(el);
933
- const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) || role || "";
994
+ const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) || "";
995
+ const result = this.getElementProperties(el);
934
996
  return {
935
997
  role,
936
998
  label,
@@ -947,13 +1009,16 @@ class BVTRecorder {
947
1009
  value: el.value,
948
1010
  tagName: el.tagName,
949
1011
  text: el.textContent,
950
- type: el.getAttribute("type"),
1012
+ type: el.type, // el.getAttribute("type"),
951
1013
  disabled: el.disabled,
952
1014
  readOnly: el.readOnly,
953
1015
  required: el.required,
954
1016
  checked: el.checked,
955
1017
  innerText: el.innerText,
956
1018
  },
1019
+ attributes: result.attributes,
1020
+ properties: result.properties,
1021
+ dataset: result.dataset,
957
1022
  };
958
1023
  }
959
1024
  handleEvent(e) {
@@ -1061,7 +1126,10 @@ class BVTRecorder {
1061
1126
  mode: "IGNORE_DIGIT",
1062
1127
  });
1063
1128
  allStrategyLocators["ignore_digit"].push(locator);
1064
- } else if (locator.css.includes("text=") || locator.css.includes("[name=") || locator.css.includes("label=")) {
1129
+ } else if (
1130
+ locator.css &&
1131
+ (locator.css.includes("text=") || locator.css.includes("[name=") || locator.css.includes("label="))
1132
+ ) {
1065
1133
  locs.push(locator);
1066
1134
  allStrategyLocators["basic"].push(locator);
1067
1135
  } else {
@@ -1095,6 +1163,7 @@ class BVTRecorder {
1095
1163
  improviseLocators: false,
1096
1164
  mustIncludeCSSChain: true,
1097
1165
  root: commonParent,
1166
+ noCSSId: true,
1098
1167
  });
1099
1168
  locators.forEach((locator) => {
1100
1169
  locator.text = text;
@@ -1121,7 +1190,7 @@ class BVTRecorder {
1121
1190
  cssLocators.push(origenCss);
1122
1191
  }
1123
1192
  const noClasses = CssSelectorGenerator.getCssSelector(el, {
1124
- blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id/, /\[blinq-container/],
1193
+ blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id/],
1125
1194
  combineWithinSelector: true,
1126
1195
  combineBetweenSelectors: true,
1127
1196
  selectors: ["id", "attribute", "tag", "nthchild", "nthoftype"],
@@ -1215,7 +1284,8 @@ class BVTRecorder {
1215
1284
  action: action.details,
1216
1285
  element: this.getElementDetails(actionElement, eventName),
1217
1286
  isPopupCloseClick: this.isPopupCloseEvent(e),
1218
- ...this.getLocatorsObject(actionElement),
1287
+ // ...this.getLocatorsObject(actionElement),
1288
+ ...(actionElement.__locators ?? this.getLocatorsObject(actionElement)),
1219
1289
  frame: this.getFrameDetails(),
1220
1290
  statistics: {
1221
1291
  time: `${performance.measure("command-received", "command-send").duration.toFixed(2)} ms`,
@@ -24,11 +24,13 @@ 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
  };
31
31
 
32
+ window.getMatchingElements = getMatchingElements;
33
+
32
34
  const getPWSelectors = (element, options = {}) => {
33
35
  const selectors = [];
34
36
  const exludeText = options?.excludeText || false;
@@ -147,7 +149,25 @@ const getElementLocators = (element, options) => {
147
149
  .map((item) => {
148
150
  return item;
149
151
  });
150
-
152
+ if (result.nonUnique.length === 0) {
153
+ // find slector with min score
154
+ let minScore = Infinity;
155
+ let minSelector = null;
156
+ for (const locator of pw_locators) {
157
+ if (locator.score < minScore) {
158
+ minScore = locator.score;
159
+ minSelector = locator.selector;
160
+ }
161
+ }
162
+ if (minSelector) {
163
+ result.nonUnique.push({
164
+ css: minSelector,
165
+ priority: 2,
166
+ elements: getMatchingElements(minSelector, options),
167
+ score: minScore,
168
+ });
169
+ }
170
+ }
151
171
  return result;
152
172
  };
153
173
 
@@ -337,6 +357,7 @@ function generateUniqueCSSSelector(element, options) {
337
357
  const root = options?.root || window.document;
338
358
  const separator = options?.separator || " > ";
339
359
  const isUnique = options?.isunique || ((selector) => getMatchingElements(selector, options).length === 1);
360
+ const noCSSId = options?.noCSSId || false;
340
361
 
341
362
  if (!(element instanceof Element)) return null;
342
363
 
@@ -344,7 +365,7 @@ function generateUniqueCSSSelector(element, options) {
344
365
 
345
366
  let selector = "";
346
367
  const id = element.getAttribute("id");
347
- if (id && !/\d/.test(id)) {
368
+ if (id && !/\d/.test(id) && (!noCSSId)) {
348
369
  selector = "#" + cssEscape(id);
349
370
  if (isUnique(selector)) return selector;
350
371
  }
@@ -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