automation_model 1.0.554-dev → 1.0.554-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.
@@ -10,18 +10,21 @@ import { getDateTimeValue } from "./date_time.js";
10
10
  import drawRectangle from "./drawRect.js";
11
11
  //import { closeUnexpectedPopups } from "./popups.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { maskValue, replaceWithLocalTestData } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
14
14
  import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
- import { getContext } from "./init_browser.js";
17
+ import { getContext, refreshBrowser } from "./init_browser.js";
18
18
  import { locate_element } from "./locate_element.js";
19
19
  import { randomUUID } from "crypto";
20
20
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
21
21
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
22
  import { LocatorLog } from "./locator_log.js";
23
+ import axios from "axios";
24
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
23
25
  export const Types = {
24
26
  CLICK: "click_element",
27
+ WAIT_ELEMENT: "wait_element",
25
28
  NAVIGATE: "navigate",
26
29
  FILL: "fill_element",
27
30
  EXECUTE: "execute_page_method",
@@ -31,6 +34,8 @@ export const Types = {
31
34
  GET_PAGE_STATUS: "get_page_status",
32
35
  CLICK_ROW_ACTION: "click_row_action",
33
36
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
37
+ VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
38
+ VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
34
39
  ANALYZE_TABLE: "analyze_table",
35
40
  SELECT: "select_combobox",
36
41
  VERIFY_PAGE_PATH: "verify_page_path",
@@ -41,6 +46,7 @@ export const Types = {
41
46
  UNCHECK: "uncheck_element",
42
47
  EXTRACT: "extract_attribute",
43
48
  CLOSE_PAGE: "close_page",
49
+ TABLE_OPERATION: "table_operation",
44
50
  SET_DATE_TIME: "set_date_time",
45
51
  SET_VIEWPORT: "set_viewport",
46
52
  VERIFY_VISUAL: "verify_visual",
@@ -48,8 +54,12 @@ export const Types = {
48
54
  SET_INPUT: "set_input",
49
55
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
50
56
  VERIFY_ATTRIBUTE: "verify_element_attribute",
57
+ VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
51
58
  };
52
59
  export const apps = {};
60
+ const formatElementName = (elementName) => {
61
+ return elementName ? JSON.stringify(elementName) : "element";
62
+ };
53
63
  class StableBrowser {
54
64
  browser;
55
65
  page;
@@ -63,6 +73,7 @@ class StableBrowser {
63
73
  appName = "main";
64
74
  tags = null;
65
75
  isRecording = false;
76
+ initSnapshotTaken = false;
66
77
  constructor(browser, page, logger = null, context = null, world = null) {
67
78
  this.browser = browser;
68
79
  this.page = page;
@@ -101,27 +112,9 @@ class StableBrowser {
101
112
  registerNetworkEvents(this.world, this, this.context, this.page);
102
113
  registerDownloadEvent(this.page, this.world, this.context);
103
114
  }
104
- async scrollPageToLoadLazyElements() {
105
- let lastHeight = await this.page.evaluate(() => document.body.scrollHeight);
106
- let retry = 0;
107
- while (true) {
108
- await this.page.evaluate(() => window.scrollBy(0, window.innerHeight));
109
- await new Promise((resolve) => setTimeout(resolve, 1000));
110
- let newHeight = await this.page.evaluate(() => document.body.scrollHeight);
111
- if (newHeight === lastHeight) {
112
- break;
113
- }
114
- lastHeight = newHeight;
115
- retry++;
116
- if (retry > 10) {
117
- break;
118
- }
119
- }
120
- await this.page.evaluate(() => window.scrollTo(0, 0));
121
- }
122
115
  registerEventListeners(context) {
123
116
  this.registerConsoleLogListener(this.page, context);
124
- this.registerRequestListener(this.page, context, this.webLogFile);
117
+ // this.registerRequestListener(this.page, context, this.webLogFile);
125
118
  if (!context.pageLoading) {
126
119
  context.pageLoading = { status: false };
127
120
  }
@@ -177,8 +170,8 @@ class StableBrowser {
177
170
  };
178
171
  }
179
172
  const tempContext = {};
180
- this._copyContext(this, tempContext);
181
- this._copyContext(apps[appName], this);
173
+ _copyContext(this, tempContext);
174
+ _copyContext(apps[appName], this);
182
175
  apps[this.appName] = tempContext;
183
176
  this.appName = appName;
184
177
  if (newContextCreated) {
@@ -187,22 +180,6 @@ class StableBrowser {
187
180
  await this.waitForPageLoad();
188
181
  }
189
182
  }
190
- _copyContext(from, to) {
191
- to.browser = from.browser;
192
- to.page = from.page;
193
- to.context = from.context;
194
- }
195
- getWebLogFile(logFolder) {
196
- if (!fs.existsSync(logFolder)) {
197
- fs.mkdirSync(logFolder, { recursive: true });
198
- }
199
- let nextIndex = 1;
200
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
201
- nextIndex++;
202
- }
203
- const fileName = nextIndex + ".json";
204
- return path.join(logFolder, fileName);
205
- }
206
183
  registerConsoleLogListener(page, context) {
207
184
  if (!this.context.webLogger) {
208
185
  this.context.webLogger = [];
@@ -266,55 +243,51 @@ class StableBrowser {
266
243
  // async closeUnexpectedPopups() {
267
244
  // await closeUnexpectedPopups(this.page);
268
245
  // }
269
- async goto(url) {
246
+ async goto(url, world = null) {
247
+ if (!url) {
248
+ throw new Error("url is null, verify that the environment file is correct");
249
+ }
270
250
  if (!url.startsWith("http")) {
271
251
  url = "https://" + url;
272
252
  }
273
- await this.page.goto(url, {
274
- timeout: 60000,
275
- });
276
- }
277
- _fixUsingParams(text, _params) {
278
- if (!_params || typeof text !== "string") {
279
- return text;
253
+ const state = {
254
+ value: url,
255
+ world: world,
256
+ type: Types.NAVIGATE,
257
+ text: `Navigate Page to: ${url}`,
258
+ operation: "goto",
259
+ log: "***** navigate page to " + url + " *****\n",
260
+ info: {},
261
+ locate: false,
262
+ scroll: false,
263
+ screenshot: false,
264
+ highlight: false,
265
+ };
266
+ try {
267
+ await _preCommand(state, this);
268
+ await this.page.goto(url, {
269
+ timeout: 60000,
270
+ });
271
+ await _screenshot(state, this);
280
272
  }
281
- for (let key in _params) {
282
- let regValue = key;
283
- if (key.startsWith("_")) {
284
- // remove the _ prefix
285
- regValue = key.substring(1);
286
- }
287
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
273
+ catch (error) {
274
+ console.error("Error on goto", error);
275
+ _commandError(state, error, this);
288
276
  }
289
- return text;
290
- }
291
- _fixLocatorUsingParams(locator, _params) {
292
- // check if not null
293
- if (!locator) {
294
- return locator;
277
+ finally {
278
+ _commandFinally(state, this);
295
279
  }
296
- // clone the locator
297
- locator = JSON.parse(JSON.stringify(locator));
298
- this.scanAndManipulate(locator, _params);
299
- return locator;
300
- }
301
- _isObject(value) {
302
- return value && typeof value === "object" && value.constructor === Object;
303
280
  }
304
- scanAndManipulate(currentObj, _params) {
305
- for (const key in currentObj) {
306
- if (typeof currentObj[key] === "string") {
307
- // Perform string manipulation
308
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
309
- }
310
- else if (this._isObject(currentObj[key])) {
311
- // Recursively scan nested objects
312
- this.scanAndManipulate(currentObj[key], _params);
281
+ async _getLocator(locator, scope, _params) {
282
+ locator = _fixLocatorUsingParams(locator, _params);
283
+ // locator = await this._replaceWithLocalData(locator);
284
+ for (let key in locator) {
285
+ if (typeof locator[key] !== "string")
286
+ continue;
287
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
288
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
313
289
  }
314
290
  }
315
- }
316
- _getLocator(locator, scope, _params) {
317
- locator = this._fixLocatorUsingParams(locator, _params);
318
291
  let locatorReturn;
319
292
  if (locator.role) {
320
293
  if (locator.role[1].nameReg) {
@@ -322,7 +295,7 @@ class StableBrowser {
322
295
  delete locator.role[1].nameReg;
323
296
  }
324
297
  // if (locator.role[1].name) {
325
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
298
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
326
299
  // }
327
300
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
328
301
  }
@@ -365,140 +338,60 @@ class StableBrowser {
365
338
  if (css && css.locator) {
366
339
  css = css.locator;
367
340
  }
368
- let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*:not(script, style, head)", false, false, _params);
341
+ let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
369
342
  if (result.elementCount === 0) {
370
343
  return;
371
344
  }
372
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
345
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
373
346
  // css climb to parent element
374
347
  const climbArray = [];
375
348
  for (let i = 0; i < climb; i++) {
376
349
  climbArray.push("..");
377
350
  }
378
351
  let climbXpath = "xpath=" + climbArray.join("/");
379
- return textElementCss + " >> " + climbXpath + " >> " + css;
380
- }
381
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
382
- //const stringifyText = JSON.stringify(text);
383
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
384
- function isParent(parent, child) {
385
- let currentNode = child.parentNode;
386
- while (currentNode !== null) {
387
- if (currentNode === parent) {
388
- return true;
389
- }
390
- currentNode = currentNode.parentNode;
391
- }
392
- return false;
393
- }
394
- document.isParent = isParent;
395
- function getRegex(str) {
396
- const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
397
- if (!match) {
398
- return null;
399
- }
400
- let [_, pattern, flags] = match;
401
- return new RegExp(pattern, flags);
402
- }
403
- document.getRegex = getRegex;
404
- function collectAllShadowDomElements(element, result = []) {
405
- // Check and add the element if it has a shadow root
406
- if (element.shadowRoot) {
407
- result.push(element);
408
- // Also search within the shadow root
409
- document.collectAllShadowDomElements(element.shadowRoot, result);
410
- }
411
- // Iterate over child nodes
412
- element.childNodes.forEach((child) => {
413
- // Recursively call the function for each child node
414
- document.collectAllShadowDomElements(child, result);
415
- });
416
- return result;
417
- }
418
- document.collectAllShadowDomElements = collectAllShadowDomElements;
419
- if (!tag) {
420
- tag = "*:not(script, style, head)";
421
- }
422
- let regexpSearch = document.getRegex(text);
423
- if (regexpSearch) {
424
- regex = true;
425
- }
426
- let elements = Array.from(document.querySelectorAll(tag));
427
- let shadowHosts = [];
428
- document.collectAllShadowDomElements(document, shadowHosts);
429
- for (let i = 0; i < shadowHosts.length; i++) {
430
- let shadowElement = shadowHosts[i].shadowRoot;
431
- if (!shadowElement) {
432
- console.log("shadowElement is null, for host " + shadowHosts[i]);
433
- continue;
434
- }
435
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
436
- elements = elements.concat(shadowElements);
437
- }
438
- let randomToken = null;
439
- const foundElements = [];
440
- if (regex) {
441
- if (!regexpSearch) {
442
- regexpSearch = new RegExp(text, "im");
443
- }
444
- for (let i = 0; i < elements.length; i++) {
445
- const element = elements[i];
446
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
447
- (element.value && regexpSearch.test(element.value))) {
448
- foundElements.push(element);
449
- }
450
- }
451
- }
452
- else {
453
- text = text.trim();
454
- for (let i = 0; i < elements.length; i++) {
455
- const element = elements[i];
456
- if (partial) {
457
- if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
458
- (element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
459
- foundElements.push(element);
460
- }
461
- }
462
- else {
463
- if ((element.innerText && element.innerText.trim() === text) ||
464
- (element.value && element.value === text)) {
465
- foundElements.push(element);
466
- }
467
- }
468
- }
469
- }
470
- let noChildElements = [];
471
- for (let i = 0; i < foundElements.length; i++) {
472
- let element = foundElements[i];
473
- let hasChild = false;
474
- for (let j = 0; j < foundElements.length; j++) {
475
- if (i === j) {
476
- continue;
477
- }
478
- if (isParent(element, foundElements[j])) {
479
- hasChild = true;
480
- break;
481
- }
482
- }
483
- if (!hasChild) {
484
- noChildElements.push(element);
485
- }
486
- }
487
- let elementCount = 0;
488
- if (noChildElements.length > 0) {
489
- for (let i = 0; i < noChildElements.length; i++) {
490
- if (randomToken === null) {
491
- randomToken = Math.random().toString(36).substring(7);
492
- }
493
- let element = noChildElements[i];
494
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
495
- elementCount++;
496
- }
352
+ let resultCss = textElementCss + " >> " + climbXpath;
353
+ if (css) {
354
+ resultCss = resultCss + " >> " + css;
355
+ }
356
+ return resultCss;
357
+ }
358
+ async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
359
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
360
+ const locator = scope.locator(query);
361
+ const count = await locator.count();
362
+ if (!tag1) {
363
+ tag1 = "*";
364
+ }
365
+ const randomToken = Math.random().toString(36).substring(7);
366
+ let tagCount = 0;
367
+ for (let i = 0; i < count; i++) {
368
+ const element = locator.nth(i);
369
+ // check if the tag matches
370
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
371
+ if (!tag.startsWith("*")) {
372
+ if (el.tagName.toLowerCase() !== tag) {
373
+ return false;
374
+ }
375
+ }
376
+ if (!el.setAttribute) {
377
+ el = el.parentElement;
378
+ }
379
+ // remove any attributes start with data-blinq-id
380
+ // for (let i = 0; i < el.attributes.length; i++) {
381
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
382
+ // el.removeAttribute(el.attributes[i].name);
383
+ // }
384
+ // }
385
+ el.setAttribute("data-blinq-id-" + randomToken, "");
386
+ return true;
387
+ }, [tag1, randomToken]))) {
388
+ continue;
497
389
  }
498
- return { elementCount: elementCount, randomToken: randomToken };
499
- }, [text1, tag1, regex1, partial1]);
390
+ tagCount++;
391
+ }
392
+ return { elementCount: tagCount, randomToken };
500
393
  }
501
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
394
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
502
395
  if (!info) {
503
396
  info = {};
504
397
  }
@@ -512,7 +405,7 @@ class StableBrowser {
512
405
  let locatorSearch = selectorHierarchy[index];
513
406
  let originalLocatorSearch = "";
514
407
  try {
515
- originalLocatorSearch = this._fixUsingParams(JSON.stringify(locatorSearch), _params);
408
+ originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
516
409
  locatorSearch = JSON.parse(originalLocatorSearch);
517
410
  }
518
411
  catch (e) {
@@ -521,30 +414,31 @@ class StableBrowser {
521
414
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
522
415
  let locator = null;
523
416
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
524
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
417
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
418
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
525
419
  if (!locatorString) {
526
420
  info.failCause.textNotFound = true;
527
- info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
421
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
528
422
  return;
529
423
  }
530
- locator = this._getLocator({ css: locatorString }, scope, _params);
424
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
531
425
  }
532
426
  else if (locatorSearch.text) {
533
- let text = this._fixUsingParams(locatorSearch.text, _params);
534
- let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, _params);
427
+ let text = _fixUsingParams(locatorSearch.text, _params);
428
+ let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
535
429
  if (result.elementCount === 0) {
536
430
  info.failCause.textNotFound = true;
537
- info.failCause.lastError = "failed to locate element by text: " + text;
431
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
538
432
  return;
539
433
  }
540
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
434
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
541
435
  if (locatorSearch.childCss) {
542
436
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
543
437
  }
544
- locator = this._getLocator(locatorSearch, scope, _params);
438
+ locator = await this._getLocator(locatorSearch, scope, _params);
545
439
  }
546
440
  else {
547
- locator = this._getLocator(locatorSearch, scope, _params);
441
+ locator = await this._getLocator(locatorSearch, scope, _params);
548
442
  }
549
443
  // let cssHref = false;
550
444
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
@@ -573,7 +467,7 @@ class StableBrowser {
573
467
  if (!visibleOnly) {
574
468
  visible = true;
575
469
  }
576
- if (visible && enabled) {
470
+ if (visible && (allowDisabled || enabled)) {
577
471
  foundLocators.push(locator.nth(j));
578
472
  if (info.locatorLog) {
579
473
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
@@ -586,9 +480,11 @@ class StableBrowser {
586
480
  info.printMessages = {};
587
481
  }
588
482
  if (info.locatorLog && !visible) {
483
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
589
484
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
590
485
  }
591
486
  if (info.locatorLog && !enabled) {
487
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
592
488
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
593
489
  }
594
490
  if (!info.printMessages[j.toString()]) {
@@ -656,7 +552,7 @@ class StableBrowser {
656
552
  }
657
553
  return { rerun: false };
658
554
  }
659
- async _locate(selectors, info, _params, timeout) {
555
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
660
556
  if (!timeout) {
661
557
  timeout = 30000;
662
558
  }
@@ -666,9 +562,18 @@ class StableBrowser {
666
562
  let selector = selectors.locators[j];
667
563
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
668
564
  }
669
- let element = await this._locate_internal(selectors, info, _params, timeout);
565
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
670
566
  if (!element.rerun) {
671
- return element;
567
+ const randomToken = Math.random().toString(36).substring(7);
568
+ element.evaluate((el, randomToken) => {
569
+ el.setAttribute("data-blinq-id-" + randomToken, "");
570
+ }, randomToken);
571
+ if (element._frame) {
572
+ return element;
573
+ }
574
+ const scope = element.page();
575
+ const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
576
+ return newSelector;
672
577
  }
673
578
  }
674
579
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -736,19 +641,19 @@ class StableBrowser {
736
641
  }
737
642
  if (!scope) {
738
643
  if (info && info.locatorLog) {
739
- info.locatorLog.setFrameSearchStatus("frame-" + fLocator, "NOT_FOUND");
644
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
740
645
  }
741
646
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
742
647
  if (Date.now() - startTime > timeout) {
743
648
  info.failCause.iframeNotFound = true;
744
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
649
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
745
650
  throw new Error("unable to locate iframe " + selectors.iframe_src);
746
651
  }
747
652
  await new Promise((resolve) => setTimeout(resolve, 1000));
748
653
  }
749
654
  else {
750
655
  if (info && info.locatorLog) {
751
- info.locatorLog.setFrameSearchStatus("frame-" + fLocator, "FOUND");
656
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
752
657
  }
753
658
  break;
754
659
  }
@@ -766,7 +671,7 @@ class StableBrowser {
766
671
  return bodyContent;
767
672
  });
768
673
  }
769
- async _locate_internal(selectors, info, _params, timeout = 30000) {
674
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
770
675
  if (!info) {
771
676
  info = {};
772
677
  info.failCause = {};
@@ -815,17 +720,17 @@ class StableBrowser {
815
720
  }
816
721
  // info.log += "scanning locators in priority 1" + "\n";
817
722
  let onlyPriority3 = selectorsLocators[0].priority === 3;
818
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
723
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
819
724
  if (result.foundElements.length === 0) {
820
725
  // info.log += "scanning locators in priority 2" + "\n";
821
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
726
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
822
727
  }
823
728
  if (result.foundElements.length === 0 && onlyPriority3) {
824
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
729
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
825
730
  }
826
731
  else {
827
732
  if (result.foundElements.length === 0 && !highPriorityOnly) {
828
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
733
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
829
734
  }
830
735
  }
831
736
  let foundElements = result.foundElements;
@@ -870,15 +775,15 @@ class StableBrowser {
870
775
  break;
871
776
  }
872
777
  if (Date.now() - startTime > highPriorityTimeout) {
873
- info.log += "high priority timeout, will try all elements" + "\n";
778
+ //info.log += "high priority timeout, will try all elements" + "\n";
874
779
  highPriorityOnly = false;
875
780
  if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
876
781
  lazy_scroll = true;
877
- await this.scrollPageToLoadLazyElements();
782
+ await scrollPageToLoadLazyElements(this.page);
878
783
  }
879
784
  }
880
785
  if (Date.now() - startTime > visibleOnlyTimeout) {
881
- info.log += "visible only timeout, will try all elements" + "\n";
786
+ //info.log += "visible only timeout, will try all elements" + "\n";
882
787
  visibleOnly = false;
883
788
  }
884
789
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -892,10 +797,12 @@ class StableBrowser {
892
797
  // }
893
798
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
894
799
  info.failCause.locatorNotFound = true;
895
- info.failCause.lastError = "failed to locate unique element";
800
+ if (!info?.failCause?.lastError) {
801
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
802
+ }
896
803
  throw new Error("failed to locate first element no elements found, " + info.log);
897
804
  }
898
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
805
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
899
806
  let foundElements = [];
900
807
  const result = {
901
808
  foundElements: foundElements,
@@ -903,14 +810,15 @@ class StableBrowser {
903
810
  for (let i = 0; i < locatorsGroup.length; i++) {
904
811
  let foundLocators = [];
905
812
  try {
906
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
813
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
907
814
  }
908
815
  catch (e) {
909
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
910
- this.logger.debug(e);
816
+ // this call can fail it the browser is navigating
817
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
818
+ // this.logger.debug(e);
911
819
  foundLocators = [];
912
820
  try {
913
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
821
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
914
822
  }
915
823
  catch (e) {
916
824
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -925,9 +833,40 @@ class StableBrowser {
925
833
  result.locatorIndex = i;
926
834
  }
927
835
  if (foundLocators.length > 1) {
928
- info.failCause.foundMultiple = true;
929
- if (info.locatorLog) {
930
- info.locatorLog.setLocatorSearchStatus(locatorsGroup[i], "FOUND_NOT_UNIQUE");
836
+ // remove elements that consume the same space with 10 pixels tolerance
837
+ const boxes = [];
838
+ for (let j = 0; j < foundLocators.length; j++) {
839
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
840
+ }
841
+ for (let j = 0; j < boxes.length; j++) {
842
+ for (let k = 0; k < boxes.length; k++) {
843
+ if (j === k) {
844
+ continue;
845
+ }
846
+ // check if x, y, width, height are the same with 10 pixels tolerance
847
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
848
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
849
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
850
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
851
+ // as the element is not unique, will remove it
852
+ boxes.splice(k, 1);
853
+ k--;
854
+ }
855
+ }
856
+ }
857
+ if (boxes.length === 1) {
858
+ result.foundElements.push({
859
+ locator: boxes[0].locator.first(),
860
+ box: boxes[0].box,
861
+ unique: true,
862
+ });
863
+ result.locatorIndex = i;
864
+ }
865
+ else {
866
+ info.failCause.foundMultiple = true;
867
+ if (info.locatorLog) {
868
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
869
+ }
931
870
  }
932
871
  }
933
872
  }
@@ -1038,25 +977,14 @@ class StableBrowser {
1038
977
  options,
1039
978
  world,
1040
979
  text: "Click element",
980
+ _text: "Click on " + selectors.element_name,
1041
981
  type: Types.CLICK,
1042
982
  operation: "click",
1043
983
  log: "***** click on " + selectors.element_name + " *****\n",
1044
984
  };
1045
985
  try {
1046
986
  await _preCommand(state, this);
1047
- if (state.options && state.options.context) {
1048
- state.selectors.locators[0].text = state.options.context;
1049
- }
1050
- try {
1051
- await state.element.click();
1052
- // await new Promise((resolve) => setTimeout(resolve, 1000));
1053
- }
1054
- catch (e) {
1055
- // await this.closeUnexpectedPopups();
1056
- state.element = await this._locate(selectors, state.info, _params);
1057
- await state.element.dispatchEvent("click");
1058
- // await new Promise((resolve) => setTimeout(resolve, 1000));
1059
- }
987
+ await performAction("click", state.element, options, this, state, _params);
1060
988
  await this.waitForPageLoad();
1061
989
  return state.info;
1062
990
  }
@@ -1067,6 +995,38 @@ class StableBrowser {
1067
995
  _commandFinally(state, this);
1068
996
  }
1069
997
  }
998
+ async waitForElement(selectors, _params, options = {}, world = null) {
999
+ const timeout = this._getFindElementTimeout(options);
1000
+ const state = {
1001
+ selectors,
1002
+ _params,
1003
+ options,
1004
+ world,
1005
+ text: "Wait for element",
1006
+ _text: "Wait for " + selectors.element_name,
1007
+ type: Types.WAIT_ELEMENT,
1008
+ operation: "waitForElement",
1009
+ log: "***** wait for " + selectors.element_name + " *****\n",
1010
+ };
1011
+ let found = false;
1012
+ try {
1013
+ await _preCommand(state, this);
1014
+ // if (state.options && state.options.context) {
1015
+ // state.selectors.locators[0].text = state.options.context;
1016
+ // }
1017
+ await state.element.waitFor({ timeout: timeout });
1018
+ found = true;
1019
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1020
+ }
1021
+ catch (e) {
1022
+ console.error("Error on waitForElement", e);
1023
+ // await _commandError(state, e, this);
1024
+ }
1025
+ finally {
1026
+ _commandFinally(state, this);
1027
+ }
1028
+ return found;
1029
+ }
1070
1030
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
1071
1031
  const state = {
1072
1032
  selectors,
@@ -1075,6 +1035,7 @@ class StableBrowser {
1075
1035
  world,
1076
1036
  type: checked ? Types.CHECK : Types.UNCHECK,
1077
1037
  text: checked ? `Check element` : `Uncheck element`,
1038
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
1078
1039
  operation: "setCheck",
1079
1040
  log: "***** check " + selectors.element_name + " *****\n",
1080
1041
  };
@@ -1084,9 +1045,15 @@ class StableBrowser {
1084
1045
  // let element = await this._locate(selectors, info, _params);
1085
1046
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1086
1047
  try {
1087
- // await this._highlightElements(element);
1048
+ // if (world && world.screenshot && !world.screenshotPath) {
1049
+ // console.log(`Highlighting while running from recorder`);
1050
+ await this._highlightElements(element);
1088
1051
  await state.element.setChecked(checked);
1089
1052
  await new Promise((resolve) => setTimeout(resolve, 1000));
1053
+ // await this._unHighlightElements(element);
1054
+ // }
1055
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1056
+ // await this._unHighlightElements(element);
1090
1057
  }
1091
1058
  catch (e) {
1092
1059
  if (e.message && e.message.includes("did not change its state")) {
@@ -1118,22 +1085,13 @@ class StableBrowser {
1118
1085
  world,
1119
1086
  type: Types.HOVER,
1120
1087
  text: `Hover element`,
1088
+ _text: `Hover on ${selectors.element_name}`,
1121
1089
  operation: "hover",
1122
1090
  log: "***** hover " + selectors.element_name + " *****\n",
1123
1091
  };
1124
1092
  try {
1125
1093
  await _preCommand(state, this);
1126
- try {
1127
- await state.element.hover();
1128
- await new Promise((resolve) => setTimeout(resolve, 1000));
1129
- }
1130
- catch (e) {
1131
- //await this.closeUnexpectedPopups();
1132
- state.info.log += "hover failed, will try again" + "\n";
1133
- state.element = await this._locate(selectors, state.info, _params);
1134
- await state.element.hover({ timeout: 10000 });
1135
- await new Promise((resolve) => setTimeout(resolve, 1000));
1136
- }
1094
+ await performAction("hover", state.element, options, this, state, _params);
1137
1095
  await _screenshot(state, this);
1138
1096
  await this.waitForPageLoad();
1139
1097
  return state.info;
@@ -1157,6 +1115,7 @@ class StableBrowser {
1157
1115
  value: values.toString(),
1158
1116
  type: Types.SELECT,
1159
1117
  text: `Select option: ${values}`,
1118
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1160
1119
  operation: "selectOption",
1161
1120
  log: "***** select option " + selectors.element_name + " *****\n",
1162
1121
  };
@@ -1191,6 +1150,7 @@ class StableBrowser {
1191
1150
  highlight: false,
1192
1151
  type: Types.TYPE_PRESS,
1193
1152
  text: `Type value: ${_value}`,
1153
+ _text: `Type value: ${_value}`,
1194
1154
  operation: "type",
1195
1155
  log: "",
1196
1156
  };
@@ -1270,6 +1230,7 @@ class StableBrowser {
1270
1230
  world,
1271
1231
  type: Types.SET_DATE_TIME,
1272
1232
  text: `Set date time value: ${value}`,
1233
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1273
1234
  operation: "setDateTime",
1274
1235
  log: "***** set date time value " + selectors.element_name + " *****\n",
1275
1236
  throwError: false,
@@ -1277,7 +1238,7 @@ class StableBrowser {
1277
1238
  try {
1278
1239
  await _preCommand(state, this);
1279
1240
  try {
1280
- await state.element.click();
1241
+ await performAction("click", state.element, options, this, state, _params);
1281
1242
  await new Promise((resolve) => setTimeout(resolve, 500));
1282
1243
  if (format) {
1283
1244
  state.value = dayjs(state.value).format(format);
@@ -1341,6 +1302,7 @@ class StableBrowser {
1341
1302
  world,
1342
1303
  type: Types.FILL,
1343
1304
  text: `Click type input with value: ${_value}`,
1305
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1344
1306
  operation: "clickType",
1345
1307
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1346
1308
  };
@@ -1363,12 +1325,11 @@ class StableBrowser {
1363
1325
  }
1364
1326
  }
1365
1327
  if (options === null || options === undefined || options.press) {
1366
- try {
1367
- await state.element.click({ timeout: 5000 });
1368
- }
1369
- catch (e) {
1370
- await state.element.dispatchEvent("click");
1328
+ if (!options) {
1329
+ options = {};
1371
1330
  }
1331
+ options.timeout = 5000;
1332
+ await performAction("click", state.element, options, this, state, _params);
1372
1333
  }
1373
1334
  else {
1374
1335
  try {
@@ -1406,7 +1367,12 @@ class StableBrowser {
1406
1367
  await this.waitForPageLoad();
1407
1368
  }
1408
1369
  else if (enter === false) {
1409
- await state.element.dispatchEvent("change");
1370
+ try {
1371
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1372
+ }
1373
+ catch (e) {
1374
+ // ignore
1375
+ }
1410
1376
  //await this.page.keyboard.press("Tab");
1411
1377
  }
1412
1378
  else {
@@ -1458,6 +1424,7 @@ class StableBrowser {
1458
1424
  return await this._getText(selectors, 0, _params, options, info, world);
1459
1425
  }
1460
1426
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1427
+ const timeout = this._getFindElementTimeout(options);
1461
1428
  _validateSelectors(selectors);
1462
1429
  let screenshotId = null;
1463
1430
  let screenshotPath = null;
@@ -1467,7 +1434,7 @@ class StableBrowser {
1467
1434
  }
1468
1435
  info.operation = "getText";
1469
1436
  info.selectors = selectors;
1470
- let element = await this._locate(selectors, info, _params);
1437
+ let element = await this._locate(selectors, info, _params, timeout);
1471
1438
  if (climb > 0) {
1472
1439
  const climbArray = [];
1473
1440
  for (let i = 0; i < climb; i++) {
@@ -1486,6 +1453,18 @@ class StableBrowser {
1486
1453
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1487
1454
  try {
1488
1455
  await this._highlightElements(element);
1456
+ // if (world && world.screenshot && !world.screenshotPath) {
1457
+ // // console.log(`Highlighting for get text while running from recorder`);
1458
+ // this._highlightElements(element)
1459
+ // .then(async () => {
1460
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1461
+ // this._unhighlightElements(element).then(
1462
+ // () => {}
1463
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1464
+ // );
1465
+ // })
1466
+ // .catch(e);
1467
+ // }
1489
1468
  const elementText = await element.innerText();
1490
1469
  return {
1491
1470
  text: elementText,
@@ -1497,7 +1476,7 @@ class StableBrowser {
1497
1476
  }
1498
1477
  catch (e) {
1499
1478
  //await this.closeUnexpectedPopups();
1500
- this.logger.info("no innerText will use textContent");
1479
+ this.logger.info("no innerText, will use textContent");
1501
1480
  const elementText = await element.textContent();
1502
1481
  return { text: elementText, screenshotId, screenshotPath, value: value };
1503
1482
  }
@@ -1522,6 +1501,7 @@ class StableBrowser {
1522
1501
  highlight: false,
1523
1502
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1524
1503
  text: `Verify element contains pattern: ${pattern}`,
1504
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1525
1505
  operation: "containsPattern",
1526
1506
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1527
1507
  };
@@ -1557,6 +1537,8 @@ class StableBrowser {
1557
1537
  }
1558
1538
  }
1559
1539
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1540
+ const timeout = this._getFindElementTimeout(options);
1541
+ const startTime = Date.now();
1560
1542
  const state = {
1561
1543
  selectors,
1562
1544
  _params,
@@ -1583,62 +1565,54 @@ class StableBrowser {
1583
1565
  }
1584
1566
  let foundObj = null;
1585
1567
  try {
1586
- await _preCommand(state, this);
1587
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1588
- if (foundObj && foundObj.element) {
1589
- await this.scrollIfNeeded(foundObj.element, state.info);
1590
- }
1591
- await _screenshot(state, this);
1592
- const dateAlternatives = findDateAlternatives(text);
1593
- const numberAlternatives = findNumberAlternatives(text);
1594
- if (dateAlternatives.date) {
1595
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1596
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1597
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1598
- return state.info;
1568
+ while (Date.now() - startTime < timeout) {
1569
+ try {
1570
+ await _preCommand(state, this);
1571
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1572
+ if (foundObj && foundObj.element) {
1573
+ await this.scrollIfNeeded(foundObj.element, state.info);
1599
1574
  }
1600
- }
1601
- throw new Error("element doesn't contain text " + text);
1602
- }
1603
- else if (numberAlternatives.number) {
1604
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1605
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1606
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1575
+ await _screenshot(state, this);
1576
+ const dateAlternatives = findDateAlternatives(text);
1577
+ const numberAlternatives = findNumberAlternatives(text);
1578
+ if (dateAlternatives.date) {
1579
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1580
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1581
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1582
+ return state.info;
1583
+ }
1584
+ }
1585
+ }
1586
+ else if (numberAlternatives.number) {
1587
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1588
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1589
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1590
+ return state.info;
1591
+ }
1592
+ }
1593
+ }
1594
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1607
1595
  return state.info;
1608
1596
  }
1609
1597
  }
1610
- throw new Error("element doesn't contain text " + text);
1611
- }
1612
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1613
- state.info.foundText = foundObj?.text;
1614
- state.info.value = foundObj?.value;
1615
- throw new Error("element doesn't contain text " + text);
1598
+ catch (e) {
1599
+ // Log error but continue retrying until timeout is reached
1600
+ this.logger.warn("Retrying containsText due to: " + e.message);
1601
+ }
1602
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1616
1603
  }
1617
- return state.info;
1604
+ state.info.foundText = foundObj?.text;
1605
+ state.info.value = foundObj?.value;
1606
+ throw new Error("element doesn't contain text " + text);
1618
1607
  }
1619
1608
  catch (e) {
1620
1609
  await _commandError(state, e, this);
1610
+ throw e;
1621
1611
  }
1622
1612
  finally {
1623
1613
  _commandFinally(state, this);
1624
1614
  }
1625
1615
  }
1626
- _getDataFile(world = null) {
1627
- let dataFile = null;
1628
- if (world && world.reportFolder) {
1629
- dataFile = path.join(world.reportFolder, "data.json");
1630
- }
1631
- else if (this.reportFolder) {
1632
- dataFile = path.join(this.reportFolder, "data.json");
1633
- }
1634
- else if (this.context && this.context.reportFolder) {
1635
- dataFile = path.join(this.context.reportFolder, "data.json");
1636
- }
1637
- else {
1638
- dataFile = "data.json";
1639
- }
1640
- return dataFile;
1641
- }
1642
1616
  async waitForUserInput(message, world = null) {
1643
1617
  if (!message) {
1644
1618
  message = "# Wait for user input. Press any key to continue";
@@ -1667,7 +1641,7 @@ class StableBrowser {
1667
1641
  return;
1668
1642
  }
1669
1643
  // if data file exists, load it
1670
- const dataFile = this._getDataFile(world);
1644
+ const dataFile = _getDataFile(world, this.context, this);
1671
1645
  let data = this.getTestData(world);
1672
1646
  // merge the testData with the existing data
1673
1647
  Object.assign(data, testData);
@@ -1770,7 +1744,7 @@ class StableBrowser {
1770
1744
  }
1771
1745
  }
1772
1746
  getTestData(world = null) {
1773
- const dataFile = this._getDataFile(world);
1747
+ const dataFile = _getDataFile(world, this.context, this);
1774
1748
  let data = {};
1775
1749
  if (fs.existsSync(dataFile)) {
1776
1750
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1857,6 +1831,15 @@ class StableBrowser {
1857
1831
  document.documentElement.clientWidth,
1858
1832
  ])));
1859
1833
  let screenshotBuffer = null;
1834
+ // if (focusedElement) {
1835
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1836
+ // await this._unhighlightElements(focusedElement);
1837
+ // await new Promise((resolve) => setTimeout(resolve, 100));
1838
+ // console.log(`Unhighlighted previous element`);
1839
+ // }
1840
+ // if (focusedElement) {
1841
+ // await this._highlightElements(focusedElement);
1842
+ // }
1860
1843
  if (this.context.browserName === "chromium") {
1861
1844
  const client = await playContext.newCDPSession(this.page);
1862
1845
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1878,6 +1861,10 @@ class StableBrowser {
1878
1861
  else {
1879
1862
  screenshotBuffer = await this.page.screenshot();
1880
1863
  }
1864
+ // if (focusedElement) {
1865
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1866
+ // await this._unhighlightElements(focusedElement);
1867
+ // }
1881
1868
  let image = await Jimp.read(screenshotBuffer);
1882
1869
  // Get the image dimensions
1883
1870
  const { width, height } = image.bitmap;
@@ -1890,6 +1877,7 @@ class StableBrowser {
1890
1877
  else {
1891
1878
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1892
1879
  }
1880
+ return screenshotBuffer;
1893
1881
  }
1894
1882
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1895
1883
  const state = {
@@ -1925,8 +1913,10 @@ class StableBrowser {
1925
1913
  world,
1926
1914
  type: Types.EXTRACT,
1927
1915
  text: `Extract attribute from element`,
1916
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1928
1917
  operation: "extractAttribute",
1929
1918
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1919
+ allowDisabled: true,
1930
1920
  };
1931
1921
  await new Promise((resolve) => setTimeout(resolve, 2000));
1932
1922
  try {
@@ -1948,6 +1938,7 @@ class StableBrowser {
1948
1938
  state.info.value = state.value;
1949
1939
  this.setTestData({ [variable]: state.value }, world);
1950
1940
  this.logger.info("set test data: " + variable + "=" + state.value);
1941
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1951
1942
  return state.info;
1952
1943
  }
1953
1944
  catch (e) {
@@ -1966,14 +1957,21 @@ class StableBrowser {
1966
1957
  options,
1967
1958
  world,
1968
1959
  type: Types.VERIFY_ATTRIBUTE,
1960
+ highlight: true,
1961
+ screenshot: true,
1969
1962
  text: `Verify element attribute`,
1963
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1970
1964
  operation: "verifyAttribute",
1971
1965
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1966
+ allowDisabled: true,
1972
1967
  };
1973
1968
  await new Promise((resolve) => setTimeout(resolve, 2000));
1974
1969
  let val;
1970
+ let expectedValue;
1975
1971
  try {
1976
1972
  await _preCommand(state, this);
1973
+ expectedValue = state.value;
1974
+ state.info.expectedValue = expectedValue;
1977
1975
  switch (attribute) {
1978
1976
  case "innerText":
1979
1977
  val = String(await state.element.innerText());
@@ -1987,23 +1985,30 @@ class StableBrowser {
1987
1985
  case "disabled":
1988
1986
  val = String(await state.element.isDisabled());
1989
1987
  break;
1988
+ case "readOnly":
1989
+ const isEditable = await state.element.isEditable();
1990
+ val = String(!isEditable);
1991
+ break;
1990
1992
  default:
1991
1993
  val = String(await state.element.getAttribute(attribute));
1992
1994
  break;
1993
1995
  }
1996
+ state.info.value = val;
1994
1997
  let regex;
1995
- if (value.startsWith("/") && value.endsWith("/")) {
1996
- const patternBody = value.slice(1, -1);
1998
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1999
+ const patternBody = expectedValue.slice(1, -1);
1997
2000
  regex = new RegExp(patternBody, "g");
1998
2001
  }
1999
2002
  else {
2000
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2003
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2001
2004
  regex = new RegExp(escapedPattern, "g");
2002
2005
  }
2003
2006
  if (!val.match(regex)) {
2004
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
2007
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2008
+ state.info.failCause.assertionFailed = true;
2009
+ state.info.failCause.lastError = errorMessage;
2010
+ throw new Error(errorMessage);
2005
2011
  }
2006
- state.info.value = val;
2007
2012
  return state.info;
2008
2013
  }
2009
2014
  catch (e) {
@@ -2031,7 +2036,7 @@ class StableBrowser {
2031
2036
  if (options && options.timeout) {
2032
2037
  timeout = options.timeout;
2033
2038
  }
2034
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2039
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2035
2040
  const request = {
2036
2041
  method: "POST",
2037
2042
  url: serviceUrl,
@@ -2102,27 +2107,32 @@ class StableBrowser {
2102
2107
  async _highlightElements(scope, css) {
2103
2108
  try {
2104
2109
  if (!scope) {
2110
+ // console.log(`Scope is not defined`);
2105
2111
  return;
2106
2112
  }
2107
2113
  if (!css) {
2108
2114
  scope
2109
2115
  .evaluate((node) => {
2110
2116
  if (node && node.style) {
2111
- let originalBorder = node.style.border;
2112
- node.style.border = "2px solid red";
2117
+ let originalOutline = node.style.outline;
2118
+ // console.log(`Original outline was: ${originalOutline}`);
2119
+ // node.__previousOutline = originalOutline;
2120
+ node.style.outline = "2px solid red";
2121
+ // console.log(`New outline is: ${node.style.outline}`);
2113
2122
  if (window) {
2114
2123
  window.addEventListener("beforeunload", function (e) {
2115
- node.style.border = originalBorder;
2124
+ node.style.outline = originalOutline;
2116
2125
  });
2117
2126
  }
2118
2127
  setTimeout(function () {
2119
- node.style.border = originalBorder;
2128
+ node.style.outline = originalOutline;
2120
2129
  }, 2000);
2121
2130
  }
2122
2131
  })
2123
2132
  .then(() => { })
2124
2133
  .catch((e) => {
2125
2134
  // ignore
2135
+ // console.error(`Could not highlight node : ${e}`);
2126
2136
  });
2127
2137
  }
2128
2138
  else {
@@ -2138,17 +2148,18 @@ class StableBrowser {
2138
2148
  if (!element.style) {
2139
2149
  return;
2140
2150
  }
2141
- var originalBorder = element.style.border;
2151
+ let originalOutline = element.style.outline;
2152
+ element.__previousOutline = originalOutline;
2142
2153
  // Set the new border to be red and 2px solid
2143
- element.style.border = "2px solid red";
2154
+ element.style.outline = "2px solid red";
2144
2155
  if (window) {
2145
2156
  window.addEventListener("beforeunload", function (e) {
2146
- element.style.border = originalBorder;
2157
+ element.style.outline = originalOutline;
2147
2158
  });
2148
2159
  }
2149
2160
  // Set a timeout to revert to the original border after 2 seconds
2150
2161
  setTimeout(function () {
2151
- element.style.border = originalBorder;
2162
+ element.style.outline = originalOutline;
2152
2163
  }, 2000);
2153
2164
  }
2154
2165
  return;
@@ -2156,6 +2167,7 @@ class StableBrowser {
2156
2167
  .then(() => { })
2157
2168
  .catch((e) => {
2158
2169
  // ignore
2170
+ // console.error(`Could not highlight css: ${e}`);
2159
2171
  });
2160
2172
  }
2161
2173
  }
@@ -2163,6 +2175,54 @@ class StableBrowser {
2163
2175
  console.debug(error);
2164
2176
  }
2165
2177
  }
2178
+ // async _unhighlightElements(scope, css) {
2179
+ // try {
2180
+ // if (!scope) {
2181
+ // return;
2182
+ // }
2183
+ // if (!css) {
2184
+ // scope
2185
+ // .evaluate((node) => {
2186
+ // if (node && node.style) {
2187
+ // if (!node.__previousOutline) {
2188
+ // node.style.outline = "";
2189
+ // } else {
2190
+ // node.style.outline = node.__previousOutline;
2191
+ // }
2192
+ // }
2193
+ // })
2194
+ // .then(() => {})
2195
+ // .catch((e) => {
2196
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2197
+ // });
2198
+ // } else {
2199
+ // scope
2200
+ // .evaluate(([css]) => {
2201
+ // if (!css) {
2202
+ // return;
2203
+ // }
2204
+ // let elements = Array.from(document.querySelectorAll(css));
2205
+ // for (i = 0; i < elements.length; i++) {
2206
+ // let element = elements[i];
2207
+ // if (!element.style) {
2208
+ // return;
2209
+ // }
2210
+ // if (!element.__previousOutline) {
2211
+ // element.style.outline = "";
2212
+ // } else {
2213
+ // element.style.outline = element.__previousOutline;
2214
+ // }
2215
+ // }
2216
+ // })
2217
+ // .then(() => {})
2218
+ // .catch((e) => {
2219
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2220
+ // });
2221
+ // }
2222
+ // } catch (error) {
2223
+ // // console.debug(error);
2224
+ // }
2225
+ // }
2166
2226
  async verifyPagePath(pathPart, options = {}, world = null) {
2167
2227
  const startTime = Date.now();
2168
2228
  let error = null;
@@ -2207,6 +2267,7 @@ class StableBrowser {
2207
2267
  _reportToWorld(world, {
2208
2268
  type: Types.VERIFY_PAGE_PATH,
2209
2269
  text: "Verify page path",
2270
+ _text: "Verify the page path contains " + pathPart,
2210
2271
  screenshotId,
2211
2272
  result: error
2212
2273
  ? {
@@ -2224,6 +2285,35 @@ class StableBrowser {
2224
2285
  });
2225
2286
  }
2226
2287
  }
2288
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2289
+ const frames = this.page.frames();
2290
+ let results = [];
2291
+ // let ignoreCase = false;
2292
+ for (let i = 0; i < frames.length; i++) {
2293
+ if (dateAlternatives.date) {
2294
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
2295
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2296
+ result.frame = frames[i];
2297
+ results.push(result);
2298
+ }
2299
+ }
2300
+ else if (numberAlternatives.number) {
2301
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2302
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2303
+ result.frame = frames[i];
2304
+ results.push(result);
2305
+ }
2306
+ }
2307
+ else {
2308
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2309
+ result.frame = frames[i];
2310
+ results.push(result);
2311
+ }
2312
+ }
2313
+ state.info.results = results;
2314
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2315
+ return resultWithElementsFound;
2316
+ }
2227
2317
  async verifyTextExistInPage(text, options = {}, world = null) {
2228
2318
  text = unEscapeString(text);
2229
2319
  const state = {
@@ -2233,12 +2323,16 @@ class StableBrowser {
2233
2323
  locate: false,
2234
2324
  scroll: false,
2235
2325
  highlight: false,
2236
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2326
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2237
2327
  text: `Verify text exists in page`,
2328
+ _text: `Verify the text '${text}' exists in page`,
2238
2329
  operation: "verifyTextExistInPage",
2239
2330
  log: "***** verify text " + text + " exists in page *****\n",
2240
2331
  };
2241
- const timeout = this._getLoadTimeout(options);
2332
+ if (testForRegex(text)) {
2333
+ text = text.replace(/\\"/g, '"');
2334
+ }
2335
+ const timeout = this._getFindElementTimeout(options);
2242
2336
  await new Promise((resolve) => setTimeout(resolve, 2000));
2243
2337
  const newValue = await this._replaceWithLocalData(text, world);
2244
2338
  if (newValue !== text) {
@@ -2251,31 +2345,15 @@ class StableBrowser {
2251
2345
  await _preCommand(state, this);
2252
2346
  state.info.text = text;
2253
2347
  while (true) {
2254
- const frames = this.page.frames();
2255
- let results = [];
2256
- for (let i = 0; i < frames.length; i++) {
2257
- if (dateAlternatives.date) {
2258
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2259
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
2260
- result.frame = frames[i];
2261
- results.push(result);
2262
- }
2263
- }
2264
- else if (numberAlternatives.number) {
2265
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2266
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
2267
- result.frame = frames[i];
2268
- results.push(result);
2269
- }
2270
- }
2271
- else {
2272
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
2273
- result.frame = frames[i];
2274
- results.push(result);
2275
- }
2348
+ let resultWithElementsFound = {
2349
+ length: 0,
2350
+ };
2351
+ try {
2352
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2353
+ }
2354
+ catch (error) {
2355
+ // ignore
2276
2356
  }
2277
- state.info.results = results;
2278
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2279
2357
  if (resultWithElementsFound.length === 0) {
2280
2358
  if (Date.now() - state.startTime > timeout) {
2281
2359
  throw new Error(`Text ${text} not found in page`);
@@ -2283,18 +2361,40 @@ class StableBrowser {
2283
2361
  await new Promise((resolve) => setTimeout(resolve, 1000));
2284
2362
  continue;
2285
2363
  }
2286
- if (resultWithElementsFound[0].randomToken) {
2287
- const frame = resultWithElementsFound[0].frame;
2288
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2289
- await this._highlightElements(frame, dataAttribute);
2290
- const element = await frame.$(dataAttribute);
2291
- if (element) {
2292
- await this.scrollIfNeeded(element, state.info);
2293
- await element.dispatchEvent("bvt_verify_page_contains_text");
2364
+ try {
2365
+ if (resultWithElementsFound[0].randomToken) {
2366
+ const frame = resultWithElementsFound[0].frame;
2367
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2368
+ await this._highlightElements(frame, dataAttribute);
2369
+ // if (world && world.screenshot && !world.screenshotPath) {
2370
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2371
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2372
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2373
+ // this._unhighlightElements(frame, dataAttribute)
2374
+ // .then(async () => {
2375
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2376
+ // })
2377
+ // .catch(
2378
+ // (e) => {}
2379
+ // console.error(e)
2380
+ // );
2381
+ // });
2382
+ // }
2383
+ const element = await frame.locator(dataAttribute).first();
2384
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2385
+ // await this._unhighlightElements(frame, dataAttribute);
2386
+ if (element) {
2387
+ await this.scrollIfNeeded(element, state.info);
2388
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2389
+ // await _screenshot(state, this, element);
2390
+ }
2294
2391
  }
2392
+ await _screenshot(state, this);
2393
+ return state.info;
2394
+ }
2395
+ catch (error) {
2396
+ console.error(error);
2295
2397
  }
2296
- await _screenshot(state, this);
2297
- return state.info;
2298
2398
  }
2299
2399
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2300
2400
  }
@@ -2316,10 +2416,14 @@ class StableBrowser {
2316
2416
  highlight: false,
2317
2417
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2318
2418
  text: `Verify text does not exist in page`,
2419
+ _text: `Verify the text '${text}' does not exist in page`,
2319
2420
  operation: "verifyTextNotExistInPage",
2320
2421
  log: "***** verify text " + text + " does not exist in page *****\n",
2321
2422
  };
2322
- const timeout = this._getLoadTimeout(options);
2423
+ if (testForRegex(text)) {
2424
+ text = text.replace(/\\"/g, '"');
2425
+ }
2426
+ const timeout = this._getFindElementTimeout(options);
2323
2427
  await new Promise((resolve) => setTimeout(resolve, 2000));
2324
2428
  const newValue = await this._replaceWithLocalData(text, world);
2325
2429
  if (newValue !== text) {
@@ -2331,32 +2435,16 @@ class StableBrowser {
2331
2435
  try {
2332
2436
  await _preCommand(state, this);
2333
2437
  state.info.text = text;
2438
+ let resultWithElementsFound = {
2439
+ length: null, // initial cannot be 0
2440
+ };
2334
2441
  while (true) {
2335
- const frames = this.page.frames();
2336
- let results = [];
2337
- for (let i = 0; i < frames.length; i++) {
2338
- if (dateAlternatives.date) {
2339
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2340
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
2341
- result.frame = frames[i];
2342
- results.push(result);
2343
- }
2344
- }
2345
- else if (numberAlternatives.number) {
2346
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2347
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
2348
- result.frame = frames[i];
2349
- results.push(result);
2350
- }
2351
- }
2352
- else {
2353
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
2354
- result.frame = frames[i];
2355
- results.push(result);
2356
- }
2442
+ try {
2443
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2444
+ }
2445
+ catch (error) {
2446
+ // ignore
2357
2447
  }
2358
- state.info.results = results;
2359
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2360
2448
  if (resultWithElementsFound.length === 0) {
2361
2449
  await _screenshot(state, this);
2362
2450
  return state.info;
@@ -2374,15 +2462,140 @@ class StableBrowser {
2374
2462
  _commandFinally(state, this);
2375
2463
  }
2376
2464
  }
2377
- _getServerUrl() {
2378
- let serviceUrl = "https://api.blinq.io";
2379
- if (process.env.NODE_ENV_BLINQ === "dev") {
2380
- serviceUrl = "https://dev.api.blinq.io";
2465
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
2466
+ textAnchor = unEscapeString(textAnchor);
2467
+ textToVerify = unEscapeString(textToVerify);
2468
+ const state = {
2469
+ text_search: textToVerify,
2470
+ options,
2471
+ world,
2472
+ locate: false,
2473
+ scroll: false,
2474
+ highlight: false,
2475
+ type: Types.VERIFY_TEXT_WITH_RELATION,
2476
+ text: `Verify text with relation to another text`,
2477
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2478
+ operation: "verify_text_with_relation",
2479
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2480
+ };
2481
+ const timeout = this._getFindElementTimeout(options);
2482
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2483
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
2484
+ if (newValue !== textAnchor) {
2485
+ this.logger.info(textAnchor + "=" + newValue);
2486
+ textAnchor = newValue;
2487
+ }
2488
+ newValue = await this._replaceWithLocalData(textToVerify, world);
2489
+ if (newValue !== textToVerify) {
2490
+ this.logger.info(textToVerify + "=" + newValue);
2491
+ textToVerify = newValue;
2492
+ }
2493
+ let dateAlternatives = findDateAlternatives(textToVerify);
2494
+ let numberAlternatives = findNumberAlternatives(textToVerify);
2495
+ let foundAncore = false;
2496
+ try {
2497
+ await _preCommand(state, this);
2498
+ state.info.text = textToVerify;
2499
+ let resultWithElementsFound = {
2500
+ length: 0,
2501
+ };
2502
+ while (true) {
2503
+ try {
2504
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2505
+ }
2506
+ catch (error) {
2507
+ // ignore
2508
+ }
2509
+ if (resultWithElementsFound.length === 0) {
2510
+ if (Date.now() - state.startTime > timeout) {
2511
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2512
+ }
2513
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2514
+ continue;
2515
+ }
2516
+ try {
2517
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2518
+ foundAncore = true;
2519
+ const result = resultWithElementsFound[i];
2520
+ const token = result.randomToken;
2521
+ const frame = result.frame;
2522
+ let css = `[data-blinq-id-${token}]`;
2523
+ const climbArray1 = [];
2524
+ for (let i = 0; i < climb; i++) {
2525
+ climbArray1.push("..");
2526
+ }
2527
+ let climbXpath = "xpath=" + climbArray1.join("/");
2528
+ css = css + " >> " + climbXpath;
2529
+ const count = await frame.locator(css).count();
2530
+ for (let j = 0; j < count; j++) {
2531
+ const continer = await frame.locator(css).nth(j);
2532
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
2533
+ if (result.elementCount > 0) {
2534
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2535
+ await this._highlightElements(frame, dataAttribute);
2536
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2537
+ // if (world && world.screenshot && !world.screenshotPath) {
2538
+ // console.log(`Highlighting for vtrt while running from recorder`);
2539
+ // this._highlightElements(frame, dataAttribute)
2540
+ // .then(async () => {
2541
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2542
+ // this._unhighlightElements(frame, dataAttribute).then(
2543
+ // () => {}
2544
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2545
+ // );
2546
+ // })
2547
+ // .catch(e);
2548
+ // }
2549
+ //await this._highlightElements(frame, cssAnchor);
2550
+ const element = await frame.locator(dataAttribute).first();
2551
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2552
+ // await this._unhighlightElements(frame, dataAttribute);
2553
+ if (element) {
2554
+ await this.scrollIfNeeded(element, state.info);
2555
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2556
+ }
2557
+ await _screenshot(state, this);
2558
+ return state.info;
2559
+ }
2560
+ }
2561
+ }
2562
+ }
2563
+ catch (error) {
2564
+ console.error(error);
2565
+ }
2566
+ }
2567
+ // await expect(element).toHaveCount(1, { timeout: 10000 });
2381
2568
  }
2382
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2383
- serviceUrl = "https://stage.api.blinq.io";
2569
+ catch (e) {
2570
+ await _commandError(state, e, this);
2571
+ }
2572
+ finally {
2573
+ _commandFinally(state, this);
2574
+ }
2575
+ }
2576
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2577
+ const frames = this.page.frames();
2578
+ let results = [];
2579
+ let ignoreCase = false;
2580
+ for (let i = 0; i < frames.length; i++) {
2581
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2582
+ result.frame = frames[i];
2583
+ const climbArray = [];
2584
+ for (let i = 0; i < climb; i++) {
2585
+ climbArray.push("..");
2586
+ }
2587
+ let climbXpath = "xpath=" + climbArray.join("/");
2588
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2589
+ const count = await frames[i].locator(newLocator).count();
2590
+ if (count > 0) {
2591
+ result.elementCount = count;
2592
+ result.locator = newLocator;
2593
+ results.push(result);
2594
+ }
2384
2595
  }
2385
- return serviceUrl;
2596
+ // state.info.results = results;
2597
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2598
+ return resultWithElementsFound;
2386
2599
  }
2387
2600
  async visualVerification(text, options = {}, world = null) {
2388
2601
  const startTime = Date.now();
@@ -2398,14 +2611,17 @@ class StableBrowser {
2398
2611
  throw new Error("TOKEN is not set");
2399
2612
  }
2400
2613
  try {
2401
- let serviceUrl = this._getServerUrl();
2614
+ let serviceUrl = _getServerUrl();
2402
2615
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2403
2616
  info.screenshotPath = screenshotPath;
2404
2617
  const screenshot = await this.takeScreenshot();
2405
- const request = {
2406
- method: "POST",
2618
+ let request = {
2619
+ method: "post",
2620
+ maxBodyLength: Infinity,
2407
2621
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2408
2622
  headers: {
2623
+ "x-bvt-project-id": path.basename(this.project_path),
2624
+ "x-source": "aaa",
2409
2625
  "Content-Type": "application/json",
2410
2626
  Authorization: `Bearer ${process.env.TOKEN}`,
2411
2627
  },
@@ -2414,7 +2630,7 @@ class StableBrowser {
2414
2630
  screenshot: screenshot,
2415
2631
  }),
2416
2632
  };
2417
- let result = await this.context.api.request(request);
2633
+ const result = await axios.request(request);
2418
2634
  if (result.data.status !== true) {
2419
2635
  throw new Error("Visual validation failed");
2420
2636
  }
@@ -2442,6 +2658,7 @@ class StableBrowser {
2442
2658
  _reportToWorld(world, {
2443
2659
  type: Types.VERIFY_VISUAL,
2444
2660
  text: "Visual verification",
2661
+ _text: "Visual verification of " + text,
2445
2662
  screenshotId,
2446
2663
  result: error
2447
2664
  ? {
@@ -2563,7 +2780,7 @@ class StableBrowser {
2563
2780
  info.operation = "analyzeTable";
2564
2781
  info.selectors = selectors;
2565
2782
  info.query = query;
2566
- query = this._fixUsingParams(query, _params);
2783
+ query = _fixUsingParams(query, _params);
2567
2784
  info.query_fixed = query;
2568
2785
  info.operator = operator;
2569
2786
  info.value = value;
@@ -2708,6 +2925,32 @@ class StableBrowser {
2708
2925
  }
2709
2926
  return timeout;
2710
2927
  }
2928
+ _getFindElementTimeout(options) {
2929
+ if (options && options.timeout) {
2930
+ return options.timeout;
2931
+ }
2932
+ if (this.configuration.find_element_timeout) {
2933
+ return this.configuration.find_element_timeout;
2934
+ }
2935
+ return 30000;
2936
+ }
2937
+ async saveStoreState(path = null, world = null) {
2938
+ const storageState = await this.page.context().storageState();
2939
+ //const testDataFile = _getDataFile(world, this.context, this);
2940
+ if (path) {
2941
+ // save { storageState: storageState } into the path
2942
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
2943
+ }
2944
+ else {
2945
+ await this.setTestData({ storageState: storageState }, world);
2946
+ }
2947
+ }
2948
+ async restoreSaveState(path = null, world = null) {
2949
+ await refreshBrowser(this, path, world);
2950
+ this.registerEventListeners(this.context);
2951
+ registerNetworkEvents(this.world, this, this.context, this.page);
2952
+ registerDownloadEvent(this.page, this.world, this.context);
2953
+ }
2711
2954
  async waitForPageLoad(options = {}, world = null) {
2712
2955
  let timeout = this._getLoadTimeout(options);
2713
2956
  const promiseArray = [];
@@ -2775,6 +3018,7 @@ class StableBrowser {
2775
3018
  highlight: false,
2776
3019
  type: Types.CLOSE_PAGE,
2777
3020
  text: `Close page`,
3021
+ _text: `Close the page`,
2778
3022
  operation: "closePage",
2779
3023
  log: "***** close page *****\n",
2780
3024
  throwError: false,
@@ -2791,8 +3035,95 @@ class StableBrowser {
2791
3035
  _commandFinally(state, this);
2792
3036
  }
2793
3037
  }
3038
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3039
+ let operation = null;
3040
+ if (!options || !options.operation) {
3041
+ throw new Error("operation is not defined");
3042
+ }
3043
+ operation = options.operation;
3044
+ // validate operation is one of the supported operations
3045
+ if (operation != "click" && operation != "hover+click") {
3046
+ throw new Error("operation is not supported");
3047
+ }
3048
+ const state = {
3049
+ options,
3050
+ world,
3051
+ locate: false,
3052
+ scroll: false,
3053
+ highlight: false,
3054
+ type: Types.TABLE_OPERATION,
3055
+ text: `Table operation`,
3056
+ _text: `Table ${operation} operation`,
3057
+ operation: operation,
3058
+ log: "***** Table operation *****\n",
3059
+ };
3060
+ const timeout = this._getFindElementTimeout(options);
3061
+ try {
3062
+ await _preCommand(state, this);
3063
+ const start = Date.now();
3064
+ let cellArea = null;
3065
+ while (true) {
3066
+ try {
3067
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3068
+ if (cellArea) {
3069
+ break;
3070
+ }
3071
+ }
3072
+ catch (e) {
3073
+ // ignore
3074
+ }
3075
+ if (Date.now() - start > timeout) {
3076
+ throw new Error(`Cell not found in table`);
3077
+ }
3078
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3079
+ }
3080
+ switch (operation) {
3081
+ case "click":
3082
+ if (!options.css) {
3083
+ // will click in the center of the cell
3084
+ let xOffset = 0;
3085
+ let yOffset = 0;
3086
+ if (options.xOffset) {
3087
+ xOffset = options.xOffset;
3088
+ }
3089
+ if (options.yOffset) {
3090
+ yOffset = options.yOffset;
3091
+ }
3092
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3093
+ }
3094
+ else {
3095
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3096
+ if (results.length === 0) {
3097
+ throw new Error(`Element not found in cell area`);
3098
+ }
3099
+ state.element = results[0];
3100
+ await performAction("click", state.element, options, this, state, _params);
3101
+ }
3102
+ break;
3103
+ case "hover+click":
3104
+ if (!options.css) {
3105
+ throw new Error("css is not defined");
3106
+ }
3107
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3108
+ if (results.length === 0) {
3109
+ throw new Error(`Element not found in cell area`);
3110
+ }
3111
+ state.element = results[0];
3112
+ await performAction("hover+click", state.element, options, this, state, _params);
3113
+ break;
3114
+ default:
3115
+ throw new Error("operation is not supported");
3116
+ }
3117
+ }
3118
+ catch (e) {
3119
+ await _commandError(state, e, this);
3120
+ }
3121
+ finally {
3122
+ _commandFinally(state, this);
3123
+ }
3124
+ }
2794
3125
  saveTestDataAsGlobal(options, world) {
2795
- const dataFile = this._getDataFile(world);
3126
+ const dataFile = _getDataFile(world, this.context, this);
2796
3127
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2797
3128
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2798
3129
  }
@@ -2822,6 +3153,7 @@ class StableBrowser {
2822
3153
  _reportToWorld(world, {
2823
3154
  type: Types.SET_VIEWPORT,
2824
3155
  text: "set viewport size to " + width + "x" + hight,
3156
+ _text: "Set the viewport size to " + width + "x" + hight,
2825
3157
  screenshotId,
2826
3158
  result: error
2827
3159
  ? {
@@ -2893,14 +3225,25 @@ class StableBrowser {
2893
3225
  }
2894
3226
  }
2895
3227
  async beforeStep(world, step) {
2896
- this.stepName = step.pickleStep.text;
2897
- this.logger.info("step: " + this.stepName);
2898
3228
  if (this.stepIndex === undefined) {
2899
3229
  this.stepIndex = 0;
2900
3230
  }
2901
3231
  else {
2902
3232
  this.stepIndex++;
2903
3233
  }
3234
+ if (step && step.pickleStep && step.pickleStep.text) {
3235
+ this.stepName = step.pickleStep.text;
3236
+ this.logger.info("step: " + this.stepName);
3237
+ }
3238
+ else if (step && step.text) {
3239
+ this.stepName = step.text;
3240
+ }
3241
+ else {
3242
+ this.stepName = "step " + this.stepIndex;
3243
+ }
3244
+ if (this.context) {
3245
+ this.context.examplesRow = extractStepExampleParameters(step);
3246
+ }
2904
3247
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2905
3248
  if (this.context.browserObject.context) {
2906
3249
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2913,6 +3256,41 @@ class StableBrowser {
2913
3256
  this.saveTestDataAsGlobal({}, world);
2914
3257
  }
2915
3258
  }
3259
+ if (this.initSnapshotTaken === false) {
3260
+ this.initSnapshotTaken = true;
3261
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3262
+ const snapshot = await this.getAriaSnapshot();
3263
+ if (snapshot) {
3264
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3265
+ }
3266
+ }
3267
+ }
3268
+ }
3269
+ async getAriaSnapshot() {
3270
+ try {
3271
+ // find the page url
3272
+ const url = await this.page.url();
3273
+ // extract the path from the url
3274
+ const path = new URL(url).pathname;
3275
+ // get the page title
3276
+ const title = await this.page.title();
3277
+ // go over other frams
3278
+ const frames = this.page.frames();
3279
+ const snapshots = [];
3280
+ const content = [`- path: ${path}`, `- title: ${title}`];
3281
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3282
+ for (let i = 0; i < frames.length; i++) {
3283
+ content.push(`- frame: ${i}`);
3284
+ const frame = frames[i];
3285
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3286
+ content.push(snapshot);
3287
+ }
3288
+ return content.join("\n");
3289
+ }
3290
+ catch (e) {
3291
+ console.error(e);
3292
+ }
3293
+ return null;
2916
3294
  }
2917
3295
  async afterStep(world, step) {
2918
3296
  this.stepName = null;
@@ -2923,6 +3301,16 @@ class StableBrowser {
2923
3301
  });
2924
3302
  }
2925
3303
  }
3304
+ if (this.context) {
3305
+ this.context.examplesRow = null;
3306
+ }
3307
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3308
+ const snapshot = await this.getAriaSnapshot();
3309
+ if (snapshot) {
3310
+ const obj = {};
3311
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3312
+ }
3313
+ }
2926
3314
  }
2927
3315
  }
2928
3316
  function createTimedPromise(promise, label) {
@@ -2930,156 +3318,5 @@ function createTimedPromise(promise, label) {
2930
3318
  .then((result) => ({ status: "fulfilled", label, result }))
2931
3319
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2932
3320
  }
2933
- const KEYBOARD_EVENTS = [
2934
- "ALT",
2935
- "AltGraph",
2936
- "CapsLock",
2937
- "Control",
2938
- "Fn",
2939
- "FnLock",
2940
- "Hyper",
2941
- "Meta",
2942
- "NumLock",
2943
- "ScrollLock",
2944
- "Shift",
2945
- "Super",
2946
- "Symbol",
2947
- "SymbolLock",
2948
- "Enter",
2949
- "Tab",
2950
- "ArrowDown",
2951
- "ArrowLeft",
2952
- "ArrowRight",
2953
- "ArrowUp",
2954
- "End",
2955
- "Home",
2956
- "PageDown",
2957
- "PageUp",
2958
- "Backspace",
2959
- "Clear",
2960
- "Copy",
2961
- "CrSel",
2962
- "Cut",
2963
- "Delete",
2964
- "EraseEof",
2965
- "ExSel",
2966
- "Insert",
2967
- "Paste",
2968
- "Redo",
2969
- "Undo",
2970
- "Accept",
2971
- "Again",
2972
- "Attn",
2973
- "Cancel",
2974
- "ContextMenu",
2975
- "Escape",
2976
- "Execute",
2977
- "Find",
2978
- "Finish",
2979
- "Help",
2980
- "Pause",
2981
- "Play",
2982
- "Props",
2983
- "Select",
2984
- "ZoomIn",
2985
- "ZoomOut",
2986
- "BrightnessDown",
2987
- "BrightnessUp",
2988
- "Eject",
2989
- "LogOff",
2990
- "Power",
2991
- "PowerOff",
2992
- "PrintScreen",
2993
- "Hibernate",
2994
- "Standby",
2995
- "WakeUp",
2996
- "AllCandidates",
2997
- "Alphanumeric",
2998
- "CodeInput",
2999
- "Compose",
3000
- "Convert",
3001
- "Dead",
3002
- "FinalMode",
3003
- "GroupFirst",
3004
- "GroupLast",
3005
- "GroupNext",
3006
- "GroupPrevious",
3007
- "ModeChange",
3008
- "NextCandidate",
3009
- "NonConvert",
3010
- "PreviousCandidate",
3011
- "Process",
3012
- "SingleCandidate",
3013
- "HangulMode",
3014
- "HanjaMode",
3015
- "JunjaMode",
3016
- "Eisu",
3017
- "Hankaku",
3018
- "Hiragana",
3019
- "HiraganaKatakana",
3020
- "KanaMode",
3021
- "KanjiMode",
3022
- "Katakana",
3023
- "Romaji",
3024
- "Zenkaku",
3025
- "ZenkakuHanaku",
3026
- "F1",
3027
- "F2",
3028
- "F3",
3029
- "F4",
3030
- "F5",
3031
- "F6",
3032
- "F7",
3033
- "F8",
3034
- "F9",
3035
- "F10",
3036
- "F11",
3037
- "F12",
3038
- "Soft1",
3039
- "Soft2",
3040
- "Soft3",
3041
- "Soft4",
3042
- "ChannelDown",
3043
- "ChannelUp",
3044
- "Close",
3045
- "MailForward",
3046
- "MailReply",
3047
- "MailSend",
3048
- "MediaFastForward",
3049
- "MediaPause",
3050
- "MediaPlay",
3051
- "MediaPlayPause",
3052
- "MediaRecord",
3053
- "MediaRewind",
3054
- "MediaStop",
3055
- "MediaTrackNext",
3056
- "MediaTrackPrevious",
3057
- "AudioBalanceLeft",
3058
- "AudioBalanceRight",
3059
- "AudioBassBoostDown",
3060
- "AudioBassBoostToggle",
3061
- "AudioBassBoostUp",
3062
- "AudioFaderFront",
3063
- "AudioFaderRear",
3064
- "AudioSurroundModeNext",
3065
- "AudioTrebleDown",
3066
- "AudioTrebleUp",
3067
- "AudioVolumeDown",
3068
- "AudioVolumeMute",
3069
- "AudioVolumeUp",
3070
- "MicrophoneToggle",
3071
- "MicrophoneVolumeDown",
3072
- "MicrophoneVolumeMute",
3073
- "MicrophoneVolumeUp",
3074
- "TV",
3075
- "TV3DMode",
3076
- "TVAntennaCable",
3077
- "TVAudioDescription",
3078
- ];
3079
- function unEscapeString(str) {
3080
- const placeholder = "__NEWLINE__";
3081
- str = str.replace(new RegExp(placeholder, "g"), "\n");
3082
- return str;
3083
- }
3084
3321
  export { StableBrowser };
3085
3322
  //# sourceMappingURL=stable_browser.js.map