@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
@@ -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);