@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
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
import LocatorGenerator from "./unique_locators";
|
|
2
|
+
import SnapshotCapturer from "./snapshot_capturer";
|
|
3
|
+
import { initInjectedScript, __PW } from "./pw";
|
|
4
|
+
import AriaSnapshotUtils from "./aria_snapshot";
|
|
5
|
+
|
|
6
|
+
import Yaml from "./yaml";
|
|
7
|
+
import EventUtils from "./event_utils";
|
|
8
|
+
const RANDOM_ID = Math.random().toString(36).substring(7);
|
|
9
|
+
let inputId = 0;
|
|
10
|
+
const getNextInputId = () => {
|
|
11
|
+
inputId++;
|
|
12
|
+
return `bvt_input_${inputId}_${RANDOM_ID}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const elementHasText = (el) => {
|
|
16
|
+
if (el instanceof HTMLInputElement) {
|
|
17
|
+
if (el.type === "button" || el.type === "submit") {
|
|
18
|
+
return el.value?.length > 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const text = el.innerText;
|
|
22
|
+
|
|
23
|
+
if (text?.length > 0) {
|
|
24
|
+
// let hasChildTextNode = false;
|
|
25
|
+
if (el.childNodes.length === 0) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
29
|
+
const child = el.childNodes[i];
|
|
30
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
31
|
+
// hasChildTextNode = true;
|
|
32
|
+
if (child.nodeValue.trim().length > 0) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/*global __bvt_recordCommand, __bvt_setMode, __bvt_revertMode, __bvt_recordPageClose, __bvt_getMode, __bvt_log, __bvt_getObject */
|
|
41
|
+
const bvtRecorderBindings = {
|
|
42
|
+
recordCommand: (cmd) => __bvt_recordCommand(cmd),
|
|
43
|
+
setMode: (m) => __bvt_setMode(m),
|
|
44
|
+
revertMode: () => __bvt_revertMode(),
|
|
45
|
+
recordPageClose: () => __bvt_recordPageClose(),
|
|
46
|
+
getMode: () => __bvt_getMode(),
|
|
47
|
+
log: (msg) => __bvt_log(msg),
|
|
48
|
+
sendObject: (obj) => __bvt_getObject(obj),
|
|
49
|
+
// validateLocators: (obj) => __bvt_validateLocators(obj),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let lastInputId = null;
|
|
53
|
+
|
|
54
|
+
// define the jsdoc type for the action
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {Object} BVTRecorderAction
|
|
57
|
+
* @property {string} name
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// define the jsdoc type for the input
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} BVTRecorderStateInput
|
|
63
|
+
* @property {(event:Event) => BVTRecorderAction | undefined} getAction
|
|
64
|
+
* @property {(element:HTMLElement) => HTMLElement} getInterestedElement
|
|
65
|
+
* @property {string} hoverOutlineStyle
|
|
66
|
+
*/
|
|
67
|
+
class BVTRecorderState {
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} name
|
|
70
|
+
* @param {BVTRecorderStateInput} input
|
|
71
|
+
*/
|
|
72
|
+
constructor(name, input) {
|
|
73
|
+
this.name = name;
|
|
74
|
+
this.getAction = input.getAction;
|
|
75
|
+
this.getInterestedElement = input.getInterestedElement;
|
|
76
|
+
this.hoverOutlineStyle = input.hoverOutlineStyle;
|
|
77
|
+
this.start = input.start;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class BVTRecorder {
|
|
82
|
+
#activeTool = null;
|
|
83
|
+
#mode = "noop";
|
|
84
|
+
contextElement = null;
|
|
85
|
+
debugListener = false;
|
|
86
|
+
debugMouseOut = false;
|
|
87
|
+
debugMouseOver = false;
|
|
88
|
+
debugFocusOut = false;
|
|
89
|
+
debugFocusIn = false;
|
|
90
|
+
#outlineSymbol = Symbol("outline");
|
|
91
|
+
#isDraggingToolbar = false;
|
|
92
|
+
toolbar = null;
|
|
93
|
+
activeElement = null;
|
|
94
|
+
cleanup = null;
|
|
95
|
+
rect = null;
|
|
96
|
+
offsetX = null;
|
|
97
|
+
offsetY = null;
|
|
98
|
+
popupHandlers = [];
|
|
99
|
+
disableHighlight = false;
|
|
100
|
+
improviseLocators = true;
|
|
101
|
+
snapshotCapturer = new SnapshotCapturer({
|
|
102
|
+
excludeSelectors: ["script", "x-bvt-toolbar"],
|
|
103
|
+
});
|
|
104
|
+
injectedScript = initInjectedScript();
|
|
105
|
+
locatorGenerator = new LocatorGenerator(this.injectedScript);
|
|
106
|
+
PW = __PW;
|
|
107
|
+
snapshotUtils = new AriaSnapshotUtils();
|
|
108
|
+
eventUtils = new EventUtils();
|
|
109
|
+
|
|
110
|
+
constructor(config = {}) {
|
|
111
|
+
const { disableHighlight = false, improviseLocators = true, popupHandlers = [], testAttributes = [] } = config;
|
|
112
|
+
this.disableHighlight = disableHighlight;
|
|
113
|
+
this.improviseLocators = improviseLocators;
|
|
114
|
+
this.popupHandlers = popupHandlers;
|
|
115
|
+
this.locatorGenerator.options.customAttributes = testAttributes;
|
|
116
|
+
this.init();
|
|
117
|
+
}
|
|
118
|
+
init({ mode = "noop" } = {}) {
|
|
119
|
+
this.addRecorderStates();
|
|
120
|
+
this.mode = mode;
|
|
121
|
+
this.addOverrides();
|
|
122
|
+
this.addListeners();
|
|
123
|
+
this.addHighlightListeners(window);
|
|
124
|
+
window.addEventListener("DOMContentLoaded", async () => {
|
|
125
|
+
this.addToolbar();
|
|
126
|
+
this.mode = await bvtRecorderBindings.getMode();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
collectElementsFromAriaSnapshot(root, elementSet) {
|
|
130
|
+
if (root.element && root.element.tagName === "X-BVT-TOOL") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (root.element) {
|
|
134
|
+
elementSet.add({ element: root.element, snapshot: this.injectedScript.ariaSnapshot(root.element) });
|
|
135
|
+
}
|
|
136
|
+
for (const child of root.children) {
|
|
137
|
+
if (typeof child !== "string") {
|
|
138
|
+
this.collectElementsFromAriaSnapshot(child, elementSet);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
addRecorderStates() {
|
|
143
|
+
const noopTool = new BVTRecorderState("noop", {
|
|
144
|
+
getAction: (e) => {
|
|
145
|
+
this.eventUtils.consumeEvent(e);
|
|
146
|
+
},
|
|
147
|
+
getInterestedElement: () => { },
|
|
148
|
+
hoverOutlineStyle: "",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const idleTool = new BVTRecorderState("idle", {
|
|
152
|
+
start: () => {
|
|
153
|
+
// on start hide the toolbar
|
|
154
|
+
this.toolbar?.hide();
|
|
155
|
+
// on cleanup show the toolbar
|
|
156
|
+
|
|
157
|
+
return () => {
|
|
158
|
+
this.toolbar?.show();
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
getAction: () => null,
|
|
162
|
+
getInterestedElement: () => { },
|
|
163
|
+
hoverOutlineStyle: "",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const runningTool = new BVTRecorderState("running", {
|
|
167
|
+
start: () => {
|
|
168
|
+
// on start hide the toolbar
|
|
169
|
+
this.toolbar?.hide();
|
|
170
|
+
// on cleanup show the toolbar
|
|
171
|
+
|
|
172
|
+
return () => {
|
|
173
|
+
this.toolbar?.show();
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
getAction: () => null,
|
|
177
|
+
getInterestedElement: () => { },
|
|
178
|
+
hoverOutlineStyle: "",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const inspectingTool = new BVTRecorderState("inspecting", {
|
|
182
|
+
start: () => {
|
|
183
|
+
// on start hide the toolbar
|
|
184
|
+
this.toolbar?.hide();
|
|
185
|
+
// on cleanup show the toolbar
|
|
186
|
+
|
|
187
|
+
return () => {
|
|
188
|
+
this.toolbar?.show();
|
|
189
|
+
this.resetHighlight(this.activeElement);
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
getInterestedElement: (el) => el,
|
|
193
|
+
hoverOutlineStyle: "2px solid red",
|
|
194
|
+
getAction(e) {
|
|
195
|
+
bvtRecorderThis.eventUtils.consumeEvent(e);
|
|
196
|
+
if (e.type === "pointerdown") {
|
|
197
|
+
const el = bvtRecorderThis.eventUtils.deepEventTarget(e);
|
|
198
|
+
return {
|
|
199
|
+
details: {
|
|
200
|
+
name: "click",
|
|
201
|
+
},
|
|
202
|
+
element: el,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const multiInspectingTool = new BVTRecorderState("multiInspecting", {
|
|
209
|
+
start: () => {
|
|
210
|
+
// on start hide the toolbar
|
|
211
|
+
this.toolbar?.hide();
|
|
212
|
+
const el = document.getElementsByTagName("body")[0];
|
|
213
|
+
|
|
214
|
+
this.pageSnapshot = this.injectedScript.ariaSnapshot(el);
|
|
215
|
+
this.lastSnapshot = this.injectedScript._lastAriaSnapshot;
|
|
216
|
+
this.snapshotElements = new Set();
|
|
217
|
+
this.interestedElements = new Map();
|
|
218
|
+
this.collectElementsFromAriaSnapshot(this.lastSnapshot.root, this.snapshotElements);
|
|
219
|
+
this.elementSet = new Set();
|
|
220
|
+
for (const { element: el } of this.snapshotElements) {
|
|
221
|
+
this.elementSet.add(el);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return () => {
|
|
225
|
+
this.toolbar?.show();
|
|
226
|
+
for (const el of this.interestedElements.keys()) {
|
|
227
|
+
el.style.background = el.__originalBackground;
|
|
228
|
+
delete el.__originalBackground;
|
|
229
|
+
}
|
|
230
|
+
// reset the highlight for all active elements
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
getInterestedElement: (el) => {
|
|
234
|
+
return this.snapshotUtils.isSnapshotAvailable(el, this.elementSet);
|
|
235
|
+
},
|
|
236
|
+
hoverOutlineStyle: "2px solid red",
|
|
237
|
+
getAction: (e) => {
|
|
238
|
+
this.eventUtils.consumeEvent(e);
|
|
239
|
+
if (e.type === "pointerdown") {
|
|
240
|
+
const el = this.eventUtils.deepEventTarget(e);
|
|
241
|
+
let snapshot = this.injectedScript.ariaSnapshot(el);
|
|
242
|
+
if (this.snapshotUtils.isSnapshotAvailable(el, this.elementSet)) {
|
|
243
|
+
if (this.interestedElements.has(el)) {
|
|
244
|
+
this.interestedElements.delete(el);
|
|
245
|
+
el.style.background = el.__originalBackground;
|
|
246
|
+
delete el.__originalBackground;
|
|
247
|
+
} else {
|
|
248
|
+
el.__originalBackground = el.style.background;
|
|
249
|
+
el.style.background = "rgba(255, 204, 0, 0.3)";
|
|
250
|
+
this.interestedElements.set(el, snapshot);
|
|
251
|
+
}
|
|
252
|
+
const childrenArray = Array.from(this.interestedElements.keys());
|
|
253
|
+
if (childrenArray.length === 0) {
|
|
254
|
+
return {
|
|
255
|
+
details: {
|
|
256
|
+
name: "click",
|
|
257
|
+
value: "",
|
|
258
|
+
},
|
|
259
|
+
element: el,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const parsedParentSnapshot = this.injectedScript.utils.parseAriaSnapshot(Yaml, this.pageSnapshot).fragment;
|
|
263
|
+
const childrenSnapshots = Array.from(this.interestedElements.values());
|
|
264
|
+
const parsedChildrenSnapshots = childrenSnapshots.map((snapshot) => {
|
|
265
|
+
return this.injectedScript.utils.parseAriaSnapshot(Yaml, snapshot).fragment;
|
|
266
|
+
});
|
|
267
|
+
const filteredParent = this.snapshotUtils.filterParentToChildren(
|
|
268
|
+
parsedParentSnapshot,
|
|
269
|
+
parsedChildrenSnapshots
|
|
270
|
+
);
|
|
271
|
+
snapshot = this.snapshotUtils.serializeAriaSnapshot(filteredParent);
|
|
272
|
+
return {
|
|
273
|
+
details: {
|
|
274
|
+
name: "click",
|
|
275
|
+
value: snapshot,
|
|
276
|
+
},
|
|
277
|
+
element: el,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const bvtRecorderThis = this;
|
|
285
|
+
const recordingTextTool = new BVTRecorderState("recordingText", {
|
|
286
|
+
start: () => {
|
|
287
|
+
// on start hide the toolbar
|
|
288
|
+
this.toolbar?.hide();
|
|
289
|
+
// on cleanup show the toolbar
|
|
290
|
+
|
|
291
|
+
return () => {
|
|
292
|
+
this.toolbar?.show();
|
|
293
|
+
this.resetHighlight(this.activeElement);
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
getInterestedElement: (el) => {
|
|
298
|
+
const hasText = elementHasText(el);
|
|
299
|
+
if (hasText) {
|
|
300
|
+
return el;
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
},
|
|
304
|
+
hoverOutlineStyle: "2px solid red",
|
|
305
|
+
getAction(e) {
|
|
306
|
+
bvtRecorderThis.eventUtils.consumeEvent(e);
|
|
307
|
+
if (e.type === "pointerdown") {
|
|
308
|
+
const el = bvtRecorderThis.eventUtils.deepEventTarget(e);
|
|
309
|
+
const isInterested = this.getInterestedElement(el);
|
|
310
|
+
if (isInterested) {
|
|
311
|
+
return {
|
|
312
|
+
details: {
|
|
313
|
+
name: "click",
|
|
314
|
+
},
|
|
315
|
+
element: el,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const recordingHoverTool = new BVTRecorderState("recordingHover", {
|
|
323
|
+
start: () => {
|
|
324
|
+
// TODO: highlight the activeElement
|
|
325
|
+
// set hover tool selected
|
|
326
|
+
if (this.toolbar) {
|
|
327
|
+
this.toolbar.hoverTool.selected = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// on cleanup remove the highlight from activeElement
|
|
331
|
+
return () => {
|
|
332
|
+
this.resetHighlight(this.activeElement);
|
|
333
|
+
if (this.toolbar) {
|
|
334
|
+
this.toolbar.hoverTool.selected = false;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
getAction: (e) => {
|
|
339
|
+
this.eventUtils.consumeEvent(e);
|
|
340
|
+
if (e.type === "click") {
|
|
341
|
+
return {
|
|
342
|
+
details: {
|
|
343
|
+
name: "click",
|
|
344
|
+
},
|
|
345
|
+
element: this.eventUtils.deepEventTarget(e),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
getInterestedElement: (el) => el,
|
|
350
|
+
hoverOutlineStyle: "2px solid blue",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const recordingInputTool = new BVTRecorderState("recordingInput", {
|
|
354
|
+
start: () => {
|
|
355
|
+
// TODO: highlight the activeElement
|
|
356
|
+
|
|
357
|
+
// on cleanup remove the highlight from activeElement
|
|
358
|
+
return () => {
|
|
359
|
+
this.resetHighlight(this.activeElement);
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
getAction: (event) => {
|
|
363
|
+
switch (event.type) {
|
|
364
|
+
case "pointerdown": {
|
|
365
|
+
if (event.button === 2) return;
|
|
366
|
+
const el = this.eventUtils.getNearestInteractiveElement(this.eventUtils.deepEventTarget(event));
|
|
367
|
+
const checkbox = this.eventUtils.asCheckbox(el);
|
|
368
|
+
if (el.nodeName === "INPUT" && el.type.toLowerCase() === "file") {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (checkbox) {
|
|
372
|
+
return {
|
|
373
|
+
details: {
|
|
374
|
+
// since we are listening to mouseup, logic is inverted
|
|
375
|
+
name: !checkbox.checked ? "check" : "uncheck",
|
|
376
|
+
},
|
|
377
|
+
element: el,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
details: {
|
|
382
|
+
name: "click",
|
|
383
|
+
position: this.eventUtils.positionForEvent(event),
|
|
384
|
+
button: this.eventUtils.buttonForEvent(event),
|
|
385
|
+
modifiers: this.eventUtils.modifiersForEvent(event),
|
|
386
|
+
clickCount: event.detail,
|
|
387
|
+
},
|
|
388
|
+
element: el,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
case "input": {
|
|
392
|
+
const target = this.eventUtils.getNearestInteractiveElement(this.eventUtils.deepEventTarget(event));
|
|
393
|
+
if (target.nodeName === "INPUT" && target.type.toLowerCase() === "file") {
|
|
394
|
+
return {
|
|
395
|
+
details: {
|
|
396
|
+
name: "setInputFiles",
|
|
397
|
+
files: [...(target.files || [])].map((file) => file.name),
|
|
398
|
+
},
|
|
399
|
+
element: target,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (this.eventUtils.isRangeInput(target)) {
|
|
403
|
+
return {
|
|
404
|
+
details: {
|
|
405
|
+
name: "fill",
|
|
406
|
+
text: target.value, // should it be value or text?
|
|
407
|
+
},
|
|
408
|
+
element: target,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (["INPUT", "TEXTAREA"].includes(target.nodeName) || target.isContentEditable) {
|
|
412
|
+
if (target.nodeName === "INPUT" && ["checkbox", "radio"].includes(target.type.toLowerCase())) {
|
|
413
|
+
// Checkbox is handled in click, we can't let input trigger on checkbox - that would mean we dispatched click events while recording.
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
details: {
|
|
418
|
+
name: "fill",
|
|
419
|
+
text: target.isContentEditable ? target.innerText : target.value,
|
|
420
|
+
},
|
|
421
|
+
element: target,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (target.nodeName === "SELECT") {
|
|
425
|
+
const selectElement = target;
|
|
426
|
+
return {
|
|
427
|
+
details: {
|
|
428
|
+
name: "select",
|
|
429
|
+
options: [...selectElement.selectedOptions].map((option) => option.value),
|
|
430
|
+
},
|
|
431
|
+
element: target,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
case "keydown": {
|
|
437
|
+
if (!this.eventUtils.shouldGenerateKeyPressFor(event)) return;
|
|
438
|
+
// if (this._actionInProgress(event)) {
|
|
439
|
+
// this._expectProgrammaticKeyUp = true;
|
|
440
|
+
// return;
|
|
441
|
+
// }
|
|
442
|
+
// if (this._consumedDueWrongTarget(event)) return;
|
|
443
|
+
// Similarly to click, trigger checkbox on key event, not input.
|
|
444
|
+
if (event.key === " ") {
|
|
445
|
+
const checkbox = this.eventUtils.asCheckbox(this.eventUtils.deepEventTarget(event));
|
|
446
|
+
if (checkbox) {
|
|
447
|
+
return {
|
|
448
|
+
details: {
|
|
449
|
+
name: checkbox.checked ? "uncheck" : "check",
|
|
450
|
+
},
|
|
451
|
+
element: checkbox,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
details: {
|
|
457
|
+
name: "press",
|
|
458
|
+
key: event.key,
|
|
459
|
+
modifiers: this.eventUtils.modifiersForEvent(event),
|
|
460
|
+
},
|
|
461
|
+
element: this.eventUtils.deepEventTarget(event),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
getInterestedElement: (el) => {
|
|
467
|
+
const interactiveElement = this.eventUtils.getNearestInteractiveElement(el);
|
|
468
|
+
if (interactiveElement) {
|
|
469
|
+
return interactiveElement;
|
|
470
|
+
}
|
|
471
|
+
return el;
|
|
472
|
+
},
|
|
473
|
+
hoverOutlineStyle: "2px solid red",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const RecorderStateMap = {
|
|
477
|
+
noop: noopTool,
|
|
478
|
+
idle: idleTool,
|
|
479
|
+
running: runningTool,
|
|
480
|
+
recordingText: recordingTextTool,
|
|
481
|
+
recordingContext: recordingTextTool,
|
|
482
|
+
recordingInput: recordingInputTool,
|
|
483
|
+
recordingHover: recordingHoverTool,
|
|
484
|
+
inspecting: inspectingTool,
|
|
485
|
+
multiInspecting: multiInspectingTool,
|
|
486
|
+
};
|
|
487
|
+
this.RecorderStateMap = RecorderStateMap;
|
|
488
|
+
}
|
|
489
|
+
addOverrides() {
|
|
490
|
+
// override the window.close method
|
|
491
|
+
const windowClose = window.close.bind(window);
|
|
492
|
+
window.close = async function () {
|
|
493
|
+
bvtRecorderBindings.recordPageClose();
|
|
494
|
+
windowClose();
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const bvtRecorder = this;
|
|
498
|
+
|
|
499
|
+
// override the element.attachShadow method
|
|
500
|
+
const elementAttachShadow = Element.prototype.attachShadow;
|
|
501
|
+
Element.prototype.attachShadow = function (options) {
|
|
502
|
+
// bvtRecorderBindings.log(`Shadow DOM attached to ${this.tagName}`);
|
|
503
|
+
const shadowRoot = elementAttachShadow.call(this, options);
|
|
504
|
+
bvtRecorder.addHighlightListeners(shadowRoot);
|
|
505
|
+
return shadowRoot;
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
set mode(mode) {
|
|
509
|
+
const tool = this.RecorderStateMap[mode];
|
|
510
|
+
if (!tool) return;
|
|
511
|
+
if (this.cleanup) {
|
|
512
|
+
this.cleanup();
|
|
513
|
+
}
|
|
514
|
+
this.#activeTool = tool;
|
|
515
|
+
if (this.#activeTool.start) {
|
|
516
|
+
this.cleanup = this.#activeTool.start();
|
|
517
|
+
}
|
|
518
|
+
this.#mode = mode;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
get mode() {
|
|
522
|
+
return this.#mode;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
getFrameDetails() {
|
|
526
|
+
const details = {
|
|
527
|
+
url: window.location.href,
|
|
528
|
+
title: window.document.title,
|
|
529
|
+
isTop: window.top === window,
|
|
530
|
+
viewport: {
|
|
531
|
+
width: window.innerWidth,
|
|
532
|
+
height: window.innerHeight,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
return details;
|
|
536
|
+
}
|
|
537
|
+
setHighlight(element) {
|
|
538
|
+
if (element.closest("x-bvt-toolbar")) return;
|
|
539
|
+
const interestedElement = this.#activeTool.getInterestedElement(element);
|
|
540
|
+
if (!interestedElement) return;
|
|
541
|
+
this.activeElement = interestedElement;
|
|
542
|
+
if (interestedElement[this.#outlineSymbol] !== undefined) return;
|
|
543
|
+
interestedElement[this.#outlineSymbol] = interestedElement.style.outline;
|
|
544
|
+
interestedElement.style.outline = this.#activeTool.hoverOutlineStyle;
|
|
545
|
+
if (interestedElement.styleUpdateFn) {
|
|
546
|
+
interestedElement.styleUpdateFn(interestedElement);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
resetHighlight(element) {
|
|
550
|
+
if (!element) return;
|
|
551
|
+
const interestedElement = this.#activeTool.getInterestedElement(element);
|
|
552
|
+
if (!interestedElement) return;
|
|
553
|
+
if (interestedElement[this.#outlineSymbol] === undefined) return;
|
|
554
|
+
interestedElement.style.outline = interestedElement[this.#outlineSymbol];
|
|
555
|
+
interestedElement[this.#outlineSymbol] = undefined;
|
|
556
|
+
if (interestedElement.styleUpdateFn) {
|
|
557
|
+
interestedElement.styleUpdateFn(interestedElement);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
addHighlightListeners(root) {
|
|
561
|
+
root.addEventListener("mouseover", (event) => {
|
|
562
|
+
if (this.debugMouseOver) {
|
|
563
|
+
debugger;
|
|
564
|
+
}
|
|
565
|
+
const target = this.eventUtils.deepEventTarget(event);
|
|
566
|
+
this.setHighlight(target);
|
|
567
|
+
});
|
|
568
|
+
root.addEventListener("mouseout", (event) => {
|
|
569
|
+
if (this.debugMouseOut) {
|
|
570
|
+
debugger;
|
|
571
|
+
}
|
|
572
|
+
const target = this.eventUtils.deepEventTarget(event);
|
|
573
|
+
this.resetHighlight(target);
|
|
574
|
+
});
|
|
575
|
+
root.addEventListener("focusin", (event) => {
|
|
576
|
+
if (this.debugFocusIn) {
|
|
577
|
+
debugger;
|
|
578
|
+
}
|
|
579
|
+
const target = this.eventUtils.deepEventTarget(event);
|
|
580
|
+
this.setHighlight(target);
|
|
581
|
+
});
|
|
582
|
+
root.addEventListener("focusout", (event) => {
|
|
583
|
+
if (this.debugFocusOut) {
|
|
584
|
+
debugger;
|
|
585
|
+
}
|
|
586
|
+
const target = this.eventUtils.deepEventTarget(event);
|
|
587
|
+
this.resetHighlight(target);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
addToolbar() {
|
|
591
|
+
if (window.top !== window) return;
|
|
592
|
+
const toolbar = document.createElement("x-bvt-toolbar");
|
|
593
|
+
toolbar.id = "bvt-toolbar";
|
|
594
|
+
this.toolbar = toolbar;
|
|
595
|
+
document.body.appendChild(toolbar);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
handleStateTransition(element) {
|
|
599
|
+
if (this.#mode === "recordingContext") {
|
|
600
|
+
this.contextElement = element;
|
|
601
|
+
bvtRecorderBindings.revertMode();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (this.#mode === "recordingHover") {
|
|
605
|
+
bvtRecorderBindings.revertMode();
|
|
606
|
+
}
|
|
607
|
+
this.contextElement = null;
|
|
608
|
+
}
|
|
609
|
+
getElementProperties(element) {
|
|
610
|
+
if (!element || !(element instanceof HTMLElement || element instanceof SVGElement)) {
|
|
611
|
+
throw new Error("Please provide a valid HTML element");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const result = {
|
|
615
|
+
properties: {},
|
|
616
|
+
attributes: {},
|
|
617
|
+
dataset: {},
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const unsortedProperties = {};
|
|
621
|
+
const unsortedAttributes = {};
|
|
622
|
+
const unsortedDataset = {};
|
|
623
|
+
|
|
624
|
+
// Get enumerable properties
|
|
625
|
+
for (const prop in element) {
|
|
626
|
+
try {
|
|
627
|
+
const value = element[prop];
|
|
628
|
+
if (
|
|
629
|
+
typeof value !== "function" &&
|
|
630
|
+
typeof value !== "object" &&
|
|
631
|
+
value !== null &&
|
|
632
|
+
value !== undefined &&
|
|
633
|
+
!prop.includes("_")
|
|
634
|
+
) {
|
|
635
|
+
unsortedProperties[prop] = value;
|
|
636
|
+
}
|
|
637
|
+
} catch (error) {
|
|
638
|
+
unsortedProperties[prop] = "[Error accessing property]";
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Get all attributes
|
|
643
|
+
if (element.attributes) {
|
|
644
|
+
for (const attr of element.attributes) {
|
|
645
|
+
if (attr.name === "data-input-id") continue; // skip input id attribute
|
|
646
|
+
if (attr.name === "data-blinq-id") continue; // skip blinq id attribute{
|
|
647
|
+
unsortedAttributes[attr.name] = attr.value;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Get dataset properties (data-* attributes)
|
|
652
|
+
if (element.dataset) {
|
|
653
|
+
for (const [key, value] of Object.entries(element.dataset)) {
|
|
654
|
+
if (key === "inputId") continue; // skip input id dataset property
|
|
655
|
+
if (key === "blinqId") continue; // skip blinq id dataset property
|
|
656
|
+
unsortedDataset[key] = value;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Sort each object by key
|
|
661
|
+
const sortByKey = (obj) => Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
662
|
+
|
|
663
|
+
result.properties = sortByKey(unsortedProperties);
|
|
664
|
+
result.attributes = sortByKey(unsortedAttributes);
|
|
665
|
+
result.dataset = sortByKey(unsortedDataset);
|
|
666
|
+
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
getElementDetails(el, type) {
|
|
670
|
+
if (lastInputId !== el.dataset.inputId || type !== "input") {
|
|
671
|
+
el.dataset.inputId = getNextInputId();
|
|
672
|
+
// if (lastInputId !== el.dataset.inputId) {
|
|
673
|
+
|
|
674
|
+
// el[locatorsSymbol] = locators;
|
|
675
|
+
// }
|
|
676
|
+
|
|
677
|
+
lastInputId = el.dataset.inputId;
|
|
678
|
+
|
|
679
|
+
el.__locators = this.getLocatorsObject(el);
|
|
680
|
+
}
|
|
681
|
+
const role = this.PW.roleUtils.getAriaRole(el);
|
|
682
|
+
const label =
|
|
683
|
+
this.PW.roleUtils.getElementAccessibleName(el, false) ||
|
|
684
|
+
this.PW.roleUtils.getElementAccessibleName(el, true) ||
|
|
685
|
+
"";
|
|
686
|
+
const result = this.getElementProperties(el);
|
|
687
|
+
return {
|
|
688
|
+
role,
|
|
689
|
+
label,
|
|
690
|
+
inputID: el.dataset.inputId,
|
|
691
|
+
tagName: el.tagName,
|
|
692
|
+
type: el.type,
|
|
693
|
+
text: this.PW.selectorUtils.elementText(new Map(), el).full.trim(),
|
|
694
|
+
parent: `tagname: ${el.parentElement?.tagName}\ninnerText: ${el.parentElement?.innerText}`,
|
|
695
|
+
attrs: {
|
|
696
|
+
placeholder: el.getAttribute("placeholder"),
|
|
697
|
+
src: el.getAttribute("src"),
|
|
698
|
+
href: el.getAttribute("href"),
|
|
699
|
+
alt: el.getAttribute("alt"),
|
|
700
|
+
value: el.value,
|
|
701
|
+
tagName: el.tagName,
|
|
702
|
+
text: el.textContent,
|
|
703
|
+
type: el.type, // el.getAttribute("type"),
|
|
704
|
+
disabled: el.disabled,
|
|
705
|
+
readOnly: el.readOnly,
|
|
706
|
+
required: el.required,
|
|
707
|
+
checked: el.checked,
|
|
708
|
+
innerText: el.innerText,
|
|
709
|
+
},
|
|
710
|
+
attributes: result.attributes,
|
|
711
|
+
properties: result.properties,
|
|
712
|
+
dataset: result.dataset,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
handleEvent(e) {
|
|
716
|
+
if (!this.toolbar) return true;
|
|
717
|
+
const target = this.eventUtils.deepEventTarget(e);
|
|
718
|
+
switch (e.type) {
|
|
719
|
+
case "pointerdown": {
|
|
720
|
+
const tool = target.closest("x-bvt-tool");
|
|
721
|
+
if (tool?.name === "drag") {
|
|
722
|
+
this.rect = this.toolbar.getBoundingClientRect();
|
|
723
|
+
this.offsetX = e.clientX - this.rect.left;
|
|
724
|
+
this.offsetY = e.clientY - this.rect.top;
|
|
725
|
+
this.#isDraggingToolbar = true;
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
if (tool) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case "pointerup": {
|
|
734
|
+
if (this.#isDraggingToolbar) {
|
|
735
|
+
this.#isDraggingToolbar = false;
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
case "mousemove": {
|
|
741
|
+
if (this.#isDraggingToolbar) {
|
|
742
|
+
const left = e.clientX - this.offsetX;
|
|
743
|
+
const top = e.clientY - this.offsetY;
|
|
744
|
+
if (
|
|
745
|
+
left < 0 ||
|
|
746
|
+
top < 0 ||
|
|
747
|
+
left + this.rect.width > window.innerWidth ||
|
|
748
|
+
top + this.rect.height > window.innerHeight
|
|
749
|
+
) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
this.toolbar.style.left = `${left}px`;
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "click": {
|
|
758
|
+
const tool = target.closest("x-bvt-tool");
|
|
759
|
+
if (tool?.name === "hover") {
|
|
760
|
+
if (tool.selected) {
|
|
761
|
+
bvtRecorderBindings.revertMode();
|
|
762
|
+
} else {
|
|
763
|
+
bvtRecorderBindings.setMode("recordingHover");
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
isPopupCloseEvent(e) {
|
|
773
|
+
const type = e.type;
|
|
774
|
+
const isPointerDown = type === "pointerdown";
|
|
775
|
+
const enterKeyDown = type === "keydown" && e.key === "Enter";
|
|
776
|
+
if (!isPointerDown && !enterKeyDown) return false;
|
|
777
|
+
|
|
778
|
+
const el = this.eventUtils.deepEventTarget(e);
|
|
779
|
+
|
|
780
|
+
return this.popupHandlers.some((handler) => {
|
|
781
|
+
const popupTitleSelector = handler.locator.css;
|
|
782
|
+
const closeBtnSelector = handler.close_dialog_locator.css;
|
|
783
|
+
const cookieTitleEl = document.querySelector(popupTitleSelector);
|
|
784
|
+
if (!cookieTitleEl) return false;
|
|
785
|
+
const style = window.getComputedStyle(cookieTitleEl);
|
|
786
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
787
|
+
return el.matches(closeBtnSelector);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
getLocatorsObject(el) {
|
|
792
|
+
if (this.contextElement) {
|
|
793
|
+
const text = this.contextElement.innerText; // TODO: handle case where contextElement is not in dom/ children removed
|
|
794
|
+
const contextEl = this.contextElement;
|
|
795
|
+
// const { climb, commonParent } = window.getCommonParent(contextEl, el);
|
|
796
|
+
const commonParent = this.locatorGenerator.dom_Parent.findLowestCommonAncestor([contextEl, el]);
|
|
797
|
+
const climb = this.locatorGenerator.dom_Parent.getClimbCountToParent(contextEl, commonParent);
|
|
798
|
+
const result = this.locatorGenerator.getElementLocators(el, {
|
|
799
|
+
excludeText: true,
|
|
800
|
+
root: commonParent,
|
|
801
|
+
});
|
|
802
|
+
result.locators.forEach((locator) => {
|
|
803
|
+
locator.text = text;
|
|
804
|
+
locator.climb = climb;
|
|
805
|
+
});
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
const isRecordingText = this.#mode === "recordingText";
|
|
809
|
+
return this.locatorGenerator.getElementLocators(el, {
|
|
810
|
+
excludeText: isRecordingText,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
addListeners() {
|
|
814
|
+
const interestedEvents = ["input", "keydown", "click", "pointerdown", "pointerup", "mousemove"];
|
|
815
|
+
interestedEvents.forEach((eventName) => {
|
|
816
|
+
window.addEventListener(
|
|
817
|
+
eventName,
|
|
818
|
+
async (e) => {
|
|
819
|
+
if (this.debugListener) {
|
|
820
|
+
debugger;
|
|
821
|
+
}
|
|
822
|
+
performance.mark("command-received");
|
|
823
|
+
const shouldContinue = this.handleEvent(e);
|
|
824
|
+
if (!shouldContinue) return;
|
|
825
|
+
const action = this.#activeTool.getAction(e);
|
|
826
|
+
if (!action) return;
|
|
827
|
+
const actionElement = action.element;
|
|
828
|
+
// const element = this.getElementDetails(actionElement);
|
|
829
|
+
// const locators = this.getElementLocators(actionElement);
|
|
830
|
+
// const element = new BVTElement(actionElement, this.contextElement);
|
|
831
|
+
|
|
832
|
+
const prevActiveElement = document.querySelector(`[data-blinq-id]`);
|
|
833
|
+
if (prevActiveElement) {
|
|
834
|
+
prevActiveElement.removeAttribute("data-blinq-id");
|
|
835
|
+
}
|
|
836
|
+
actionElement.setAttribute("data-blinq-id", getNextInputId());
|
|
837
|
+
|
|
838
|
+
const prevContextElement = document.querySelector(`[data-blinq-context-id]`);
|
|
839
|
+
if (prevContextElement) {
|
|
840
|
+
prevContextElement.removeAttribute("data-blinq-context-id");
|
|
841
|
+
}
|
|
842
|
+
if (this.contextElement) {
|
|
843
|
+
this.contextElement.setAttribute("data-blinq-context-id", getNextInputId());
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
performance.mark("command-send");
|
|
847
|
+
const cmd = {
|
|
848
|
+
mode: this.#mode,
|
|
849
|
+
action: action.details,
|
|
850
|
+
element: this.getElementDetails(actionElement, eventName),
|
|
851
|
+
isPopupCloseClick: this.isPopupCloseEvent(e),
|
|
852
|
+
// ...this.getLocatorsObject(actionElement),
|
|
853
|
+
...(actionElement.__locators ?? this.getLocatorsObject(actionElement)),
|
|
854
|
+
frame: this.getFrameDetails(),
|
|
855
|
+
statistics: {
|
|
856
|
+
time: `${performance.measure("command-received", "command-send").duration.toFixed(2)} ms`,
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
const snapshotDetails = {
|
|
860
|
+
id: actionElement.getAttribute("data-blinq-id"),
|
|
861
|
+
contextId: this.contextElement?.getAttribute("data-blinq-context-id"),
|
|
862
|
+
doc: this.snapshotCapturer.createSnapshot({
|
|
863
|
+
excludeSelectors: ["x-bvt-toolbar", "script", "style", "link[rel=stylesheet]"],
|
|
864
|
+
}),
|
|
865
|
+
};
|
|
866
|
+
cmd.snapshotDetails = snapshotDetails;
|
|
867
|
+
// eventQueue.enqueue(async () => {
|
|
868
|
+
// await bvtRecorderBindings.validateLocators(snapshotDetails);
|
|
869
|
+
// });
|
|
870
|
+
// console.log(cmd);
|
|
871
|
+
await bvtRecorderBindings.recordCommand(cmd);
|
|
872
|
+
this.handleStateTransition(action.element);
|
|
873
|
+
},
|
|
874
|
+
{ capture: true }
|
|
875
|
+
);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// TODO: implement the corresponding logic for the below methods
|
|
880
|
+
setPopupHandlers(_popopHandlers) {
|
|
881
|
+
this.popupHandlers = _popopHandlers;
|
|
882
|
+
}
|
|
883
|
+
setDisableHighlight(disableHighlight) {
|
|
884
|
+
this.disableHighlight = disableHighlight;
|
|
885
|
+
}
|
|
886
|
+
setImproviseLocators(improviseLocators) {
|
|
887
|
+
this.improviseLocators = improviseLocators;
|
|
888
|
+
}
|
|
889
|
+
deselectAriaElements() {
|
|
890
|
+
for (const el of this.interestedElements.keys()) {
|
|
891
|
+
el.style.background = el.__originalBackground;
|
|
892
|
+
delete el.__originalBackground;
|
|
893
|
+
}
|
|
894
|
+
this.interestedElements.clear();
|
|
895
|
+
}
|
|
896
|
+
processAriaSnapshot(snapshot) {
|
|
897
|
+
const matchedElements = this.findMatchingElements(snapshot, this.snapshotElements);
|
|
898
|
+
for (const el of matchedElements.values()) {
|
|
899
|
+
const element = el;
|
|
900
|
+
if (element) {
|
|
901
|
+
element.__originalOutline = element.style.outline;
|
|
902
|
+
element.style.outline = "2px solid blue";
|
|
903
|
+
setTimeout(() => {
|
|
904
|
+
element.style.outline = element.__originalOutline;
|
|
905
|
+
delete element.__originalOutline;
|
|
906
|
+
}, 2000);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
setTestAttributes(attributes) {
|
|
911
|
+
this.locatorGenerator.options.customAttributes = attributes;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
window.__bvt_Recorder = new BVTRecorder(window.__bvt_Recorder_config);
|
|
916
|
+
|
|
917
|
+
class BVTTool extends HTMLElement {
|
|
918
|
+
#selected = false;
|
|
919
|
+
tooltipText = null;
|
|
920
|
+
#tooltip = null;
|
|
921
|
+
#isInitialized = false;
|
|
922
|
+
|
|
923
|
+
constructor(name, tooltipText) {
|
|
924
|
+
// Always call super first in constructor
|
|
925
|
+
super();
|
|
926
|
+
}
|
|
927
|
+
connectedCallback() {
|
|
928
|
+
if (this.#isInitialized) return;
|
|
929
|
+
this.init();
|
|
930
|
+
this.addEventListener("mouseenter", this.showTooltip);
|
|
931
|
+
this.addEventListener("mouseleave", this.hideTooltip);
|
|
932
|
+
this.#isInitialized = true;
|
|
933
|
+
}
|
|
934
|
+
init() {
|
|
935
|
+
this.setAttribute("role", "button");
|
|
936
|
+
this.style.cssText = `
|
|
937
|
+
display: inline-flex;
|
|
938
|
+
align-items: center;
|
|
939
|
+
justify-content: center;
|
|
940
|
+
overflow: hidden;
|
|
941
|
+
background: transparent;
|
|
942
|
+
border: none;
|
|
943
|
+
cursor: pointer;
|
|
944
|
+
color: rgba(8, 8, 8, 1);
|
|
945
|
+
outline: none;
|
|
946
|
+
width:32px;
|
|
947
|
+
border-radius:8px;
|
|
948
|
+
gap:8px;
|
|
949
|
+
padding:4px 0px 4px 0px;
|
|
950
|
+
transistion: background-color 0.47s ease, color 0.24s ease;
|
|
951
|
+
`;
|
|
952
|
+
}
|
|
953
|
+
get selected() {
|
|
954
|
+
return this.#selected;
|
|
955
|
+
}
|
|
956
|
+
set selected(value) {
|
|
957
|
+
this.#selected = value;
|
|
958
|
+
if (value) {
|
|
959
|
+
this.style.backgroundColor = "#093DB0"; // blue bg
|
|
960
|
+
this.style.color = "white";
|
|
961
|
+
} else {
|
|
962
|
+
this.style.backgroundColor = "#eee";
|
|
963
|
+
this.style.color = "rgba(8, 8, 8, 1)";
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
showTooltip(event) {
|
|
967
|
+
if (!this.#tooltip) {
|
|
968
|
+
this.#tooltip = document.createElement("div");
|
|
969
|
+
this.#tooltip.style.cssText = `
|
|
970
|
+
position: fixed;
|
|
971
|
+
pointer-events: none;
|
|
972
|
+
opacity: 0;
|
|
973
|
+
// transform: translate(-50%, -100%);
|
|
974
|
+
transition: opacity 0.2s ease;
|
|
975
|
+
z-index: 2147483648;
|
|
976
|
+
|
|
977
|
+
display: inline-flex;
|
|
978
|
+
padding: var(--border-radius-radius-md, 8px) var(--spacing-spacing-sm, 12px);
|
|
979
|
+
flex-direction: column;
|
|
980
|
+
align-items: flex-start;
|
|
981
|
+
gap: var(--border-radius-radius-lg, 16px);
|
|
982
|
+
|
|
983
|
+
border-radius: var(--border-radius-radius-pre-lg, 12px);
|
|
984
|
+
background: var(--surface-surface-dark, #080808);
|
|
985
|
+
|
|
986
|
+
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
|
987
|
+
color: var(--text-white, #FFF);
|
|
988
|
+
|
|
989
|
+
/* Text sm/Medium */
|
|
990
|
+
font-family: "Verdana";
|
|
991
|
+
font-size: 14px;
|
|
992
|
+
font-style: normal;
|
|
993
|
+
font-weight: 500;
|
|
994
|
+
line-height: 20px;
|
|
995
|
+
letter-spacing: -0.084px;
|
|
996
|
+
`;
|
|
997
|
+
this.#tooltip.textContent = this.tooltipText ?? "";
|
|
998
|
+
document.body.appendChild(this.#tooltip);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const rect = this.getBoundingClientRect();
|
|
1002
|
+
const tooltip = this.#tooltip;
|
|
1003
|
+
|
|
1004
|
+
tooltip.style.opacity = "0"; // hide before measuring
|
|
1005
|
+
tooltip.style.left = "0px"; // reset for accurate width calc
|
|
1006
|
+
tooltip.style.top = "0px";
|
|
1007
|
+
tooltip.style.transform = "none"; // remove old transform
|
|
1008
|
+
|
|
1009
|
+
// Allow DOM to render
|
|
1010
|
+
requestAnimationFrame(() => {
|
|
1011
|
+
const tooltipWidth = tooltip.offsetWidth;
|
|
1012
|
+
const margin = 8;
|
|
1013
|
+
let left = rect.left + rect.width / 2 - tooltipWidth / 2;
|
|
1014
|
+
|
|
1015
|
+
// Clamp to viewport
|
|
1016
|
+
if (left < margin) left = margin;
|
|
1017
|
+
if (left + tooltipWidth > window.innerWidth - margin) {
|
|
1018
|
+
left = window.innerWidth - margin - tooltipWidth;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
tooltip.style.left = `${left}px`;
|
|
1022
|
+
tooltip.style.top = `${rect.bottom + 20}px`;
|
|
1023
|
+
tooltip.style.opacity = "1";
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
hideTooltip() {
|
|
1027
|
+
if (this.#tooltip) {
|
|
1028
|
+
this.#tooltip.style.opacity = "0";
|
|
1029
|
+
setTimeout(() => {
|
|
1030
|
+
if (this.#tooltip) {
|
|
1031
|
+
document.body.removeChild(this.#tooltip);
|
|
1032
|
+
this.#tooltip = null;
|
|
1033
|
+
}
|
|
1034
|
+
}, 200);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Commented out to avoid tooltip position update on mouse move
|
|
1039
|
+
// updateTooltipPosition() {
|
|
1040
|
+
// if (this.#tooltip) {
|
|
1041
|
+
// const rect = this.getBoundingClientRect();
|
|
1042
|
+
// const tooltipRect = this.#tooltip.getBoundingClientRect();
|
|
1043
|
+
// this.#tooltip.style.left = `${rect.left + rect.width / 2}px`;
|
|
1044
|
+
// this.#tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`; // 8px gap above button
|
|
1045
|
+
// }
|
|
1046
|
+
// }
|
|
1047
|
+
|
|
1048
|
+
reset() {
|
|
1049
|
+
this.selected = false;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
class BVTToolBar extends HTMLElement {
|
|
1054
|
+
#isInitialized = false;
|
|
1055
|
+
constructor() {
|
|
1056
|
+
// Always call super first in constructor
|
|
1057
|
+
super();
|
|
1058
|
+
}
|
|
1059
|
+
connectedCallback() {
|
|
1060
|
+
if (this.#isInitialized) return;
|
|
1061
|
+
this.init();
|
|
1062
|
+
this.#isInitialized = true;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
show() {
|
|
1066
|
+
// this.style.display = "flex";
|
|
1067
|
+
this.style.transform = "translateY(0)";
|
|
1068
|
+
}
|
|
1069
|
+
hide() {
|
|
1070
|
+
// this.style.display = "none";
|
|
1071
|
+
this.style.transform = "translateY(-200%)";
|
|
1072
|
+
}
|
|
1073
|
+
getDragHandler() {
|
|
1074
|
+
// const dragHandler = new BVTTool("drag", "Hold and drag to reposition this toolbar");
|
|
1075
|
+
const dragHandler = document.createElement("x-bvt-tool");
|
|
1076
|
+
dragHandler.name = "drag";
|
|
1077
|
+
dragHandler.tooltipText = "Hold and drag to reposition this toolbar";
|
|
1078
|
+
|
|
1079
|
+
dragHandler.style.cursor = "move";
|
|
1080
|
+
dragHandler.innerHTML = `<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1081
|
+
<path d="M8.625 7.125a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM15.375 7.125a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM8.625 13.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM15.375 13.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM8.625 19.875a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM15.375 19.875a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" fill="#080808" style="fill: #080808;"/>
|
|
1082
|
+
</svg>`;
|
|
1083
|
+
|
|
1084
|
+
dragHandler.addEventListener("mousedown", (event) => {
|
|
1085
|
+
event.preventDefault(); // Prevent text selection
|
|
1086
|
+
|
|
1087
|
+
if (dragHandler.hideTooltip) {
|
|
1088
|
+
dragHandler.hideTooltip(); // Hide tooltip while dragging
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
document.body.style.userSelect = "none";
|
|
1092
|
+
// const toolbar = this;
|
|
1093
|
+
// const rect = toolbar.getBoundingClientRect();
|
|
1094
|
+
// const offsetX = event.clientX - rect.left;
|
|
1095
|
+
// const offsetY = event.clientY - rect.top;
|
|
1096
|
+
|
|
1097
|
+
// const moveHandler = (event) => {
|
|
1098
|
+
// const left = event.clientX - offsetX;
|
|
1099
|
+
// const top = event.clientY - offsetY;
|
|
1100
|
+
// if (left < 0 || top < 0 || left + rect.width > window.innerWidth || top + rect.height > window.innerHeight) {
|
|
1101
|
+
// return;
|
|
1102
|
+
// }
|
|
1103
|
+
// toolbar.style.left = `${left}px`;
|
|
1104
|
+
// toolbar.style.top = `${top}px`;
|
|
1105
|
+
// };
|
|
1106
|
+
|
|
1107
|
+
// const upHandler = () => {
|
|
1108
|
+
// document.removeEventListener("mousemove", moveHandler);
|
|
1109
|
+
// document.removeEventListener("mouseup", upHandler);
|
|
1110
|
+
// };
|
|
1111
|
+
|
|
1112
|
+
// document.addEventListener("mousemove", moveHandler);
|
|
1113
|
+
// document.addEventListener("mouseup", upHandler);
|
|
1114
|
+
});
|
|
1115
|
+
return dragHandler;
|
|
1116
|
+
}
|
|
1117
|
+
getHoverTool() {
|
|
1118
|
+
// const hoverTool = new BVTTool("hover", "Record hover over an element");
|
|
1119
|
+
const hoverTool = document.createElement("x-bvt-tool");
|
|
1120
|
+
hoverTool.name = "hover";
|
|
1121
|
+
hoverTool.tooltipText = "Record hover over an element";
|
|
1122
|
+
hoverTool.innerHTML = `<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="M13.999 17.175l-3.452.774a.391.391 0 00-.245.172l-1.664 2.6c-.809 1.265-2.743.924-3.072-.541L3.71 10.963C3.38 9.493 4.99 8.36 6.263 9.167l8.271 4.933c1.27.805.933 2.746-.535 3.075z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="stroke: currentColor; fill: none;" /><rect x="11" y="3" width="12" height="2" rx="1" fill="currentColor" style="fill: currentColor;"/><rect x="11" y="7" width="9" height="2" rx="1" fill="currentColor" style="fill: currentColor;"/><rect x="11" y="11" width="12" height="2" rx="1" fill="currentColor" style="fill: currentColor;"/><circle cx="9" cy="4" r="1" fill="currentColor" style="fill: currentColor;"/><circle cx="9" cy="8" r="1" fill="currentColor" style="fill: currentColor;"/></svg>`;
|
|
1123
|
+
// hoverTool.addEventListener("click", this.onHoverMode);
|
|
1124
|
+
return hoverTool;
|
|
1125
|
+
}
|
|
1126
|
+
init() {
|
|
1127
|
+
this.style.cssText = `
|
|
1128
|
+
position: fixed;
|
|
1129
|
+
width: fit-content;
|
|
1130
|
+
top: 8px;
|
|
1131
|
+
left: calc(50vw - 36px);
|
|
1132
|
+
z-index: 2147483647;
|
|
1133
|
+
display: flex;
|
|
1134
|
+
gap: 8px;
|
|
1135
|
+
background-color: rgba(241, 241, 241, 0.95);
|
|
1136
|
+
padding: 8px;
|
|
1137
|
+
border-radius: 12px;
|
|
1138
|
+
transistion: transform 0.47s ease;
|
|
1139
|
+
`;
|
|
1140
|
+
this.dragTool = this.getDragHandler();
|
|
1141
|
+
this.hoverTool = this.getHoverTool();
|
|
1142
|
+
this.append(this.dragTool, this.hoverTool);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
customElements.define("x-bvt-tool", BVTTool);
|
|
1147
|
+
customElements.define("x-bvt-toolbar", BVTToolBar);
|