@dev-blinq/cucumber_client 1.0.1234-dev → 1.0.1234-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 +48 -63
- package/bin/client/apiTest/apiTest.js +6 -0
- package/bin/client/cli_helpers.js +11 -13
- package/bin/client/code_cleanup/utils.js +37 -14
- 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
|
@@ -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();
|
|
@@ -932,12 +991,8 @@ class BVTRecorder {
|
|
|
932
991
|
el.__locators = this.getLocatorsObject(el);
|
|
933
992
|
}
|
|
934
993
|
const role = window.getAriaRole(el);
|
|
935
|
-
const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) ||
|
|
936
|
-
const
|
|
937
|
-
for (var att, i = 0, atts = el.attributes, n = atts.length; i < n; i++) {
|
|
938
|
-
const att = atts[i];
|
|
939
|
-
attrs2[att.nodeName] = att.nodeValue;
|
|
940
|
-
}
|
|
994
|
+
const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) || "";
|
|
995
|
+
const result = this.getElementProperties(el);
|
|
941
996
|
return {
|
|
942
997
|
role,
|
|
943
998
|
label,
|
|
@@ -961,7 +1016,9 @@ class BVTRecorder {
|
|
|
961
1016
|
checked: el.checked,
|
|
962
1017
|
innerText: el.innerText,
|
|
963
1018
|
},
|
|
964
|
-
|
|
1019
|
+
attributes: result.attributes,
|
|
1020
|
+
properties: result.properties,
|
|
1021
|
+
dataset: result.dataset,
|
|
965
1022
|
};
|
|
966
1023
|
}
|
|
967
1024
|
handleEvent(e) {
|
|
@@ -1133,7 +1190,7 @@ class BVTRecorder {
|
|
|
1133
1190
|
cssLocators.push(origenCss);
|
|
1134
1191
|
}
|
|
1135
1192
|
const noClasses = CssSelectorGenerator.getCssSelector(el, {
|
|
1136
|
-
blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id
|
|
1193
|
+
blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id/],
|
|
1137
1194
|
combineWithinSelector: true,
|
|
1138
1195
|
combineBetweenSelectors: true,
|
|
1139
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
|