automation_model 1.0.551-dev → 1.0.551-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
280
  }
301
- _isObject(value) {
302
- return value && typeof value === "object" && value.constructor === Object;
303
- }
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,29 +775,34 @@ 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));
885
790
  }
886
791
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
887
- if (info.locatorLog) {
888
- this.logger.debug(info.locatorLog.toString());
889
- }
792
+ // if (info.locatorLog) {
793
+ // const lines = info.locatorLog.toString().split("\n");
794
+ // for (let line of lines) {
795
+ // this.logger.debug(line);
796
+ // }
797
+ // }
890
798
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
891
799
  info.failCause.locatorNotFound = true;
892
- 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
+ }
893
803
  throw new Error("failed to locate first element no elements found, " + info.log);
894
804
  }
895
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
805
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
896
806
  let foundElements = [];
897
807
  const result = {
898
808
  foundElements: foundElements,
@@ -900,14 +810,15 @@ class StableBrowser {
900
810
  for (let i = 0; i < locatorsGroup.length; i++) {
901
811
  let foundLocators = [];
902
812
  try {
903
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
813
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
904
814
  }
905
815
  catch (e) {
906
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
907
- 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);
908
819
  foundLocators = [];
909
820
  try {
910
- 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);
911
822
  }
912
823
  catch (e) {
913
824
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -924,7 +835,7 @@ class StableBrowser {
924
835
  if (foundLocators.length > 1) {
925
836
  info.failCause.foundMultiple = true;
926
837
  if (info.locatorLog) {
927
- info.locatorLog.setLocatorSearchStatus(locatorsGroup[i], "FOUND_NOT_UNIQUE");
838
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
928
839
  }
929
840
  }
930
841
  }
@@ -1035,25 +946,14 @@ class StableBrowser {
1035
946
  options,
1036
947
  world,
1037
948
  text: "Click element",
949
+ _text: "Click on " + selectors.element_name,
1038
950
  type: Types.CLICK,
1039
951
  operation: "click",
1040
952
  log: "***** click on " + selectors.element_name + " *****\n",
1041
953
  };
1042
954
  try {
1043
955
  await _preCommand(state, this);
1044
- if (state.options && state.options.context) {
1045
- state.selectors.locators[0].text = state.options.context;
1046
- }
1047
- try {
1048
- await state.element.click();
1049
- // await new Promise((resolve) => setTimeout(resolve, 1000));
1050
- }
1051
- catch (e) {
1052
- // await this.closeUnexpectedPopups();
1053
- state.element = await this._locate(selectors, state.info, _params);
1054
- await state.element.dispatchEvent("click");
1055
- // await new Promise((resolve) => setTimeout(resolve, 1000));
1056
- }
956
+ await performAction("click", state.element, options, this, state, _params);
1057
957
  await this.waitForPageLoad();
1058
958
  return state.info;
1059
959
  }
@@ -1064,6 +964,38 @@ class StableBrowser {
1064
964
  _commandFinally(state, this);
1065
965
  }
1066
966
  }
967
+ async waitForElement(selectors, _params, options = {}, world = null) {
968
+ const timeout = this._getFindElementTimeout(options);
969
+ const state = {
970
+ selectors,
971
+ _params,
972
+ options,
973
+ world,
974
+ text: "Wait for element",
975
+ _text: "Wait for " + selectors.element_name,
976
+ type: Types.WAIT_ELEMENT,
977
+ operation: "waitForElement",
978
+ log: "***** wait for " + selectors.element_name + " *****\n",
979
+ };
980
+ let found = false;
981
+ try {
982
+ await _preCommand(state, this);
983
+ // if (state.options && state.options.context) {
984
+ // state.selectors.locators[0].text = state.options.context;
985
+ // }
986
+ await state.element.waitFor({ timeout: timeout });
987
+ found = true;
988
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
989
+ }
990
+ catch (e) {
991
+ console.error("Error on waitForElement", e);
992
+ // await _commandError(state, e, this);
993
+ }
994
+ finally {
995
+ _commandFinally(state, this);
996
+ }
997
+ return found;
998
+ }
1067
999
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
1068
1000
  const state = {
1069
1001
  selectors,
@@ -1072,6 +1004,7 @@ class StableBrowser {
1072
1004
  world,
1073
1005
  type: checked ? Types.CHECK : Types.UNCHECK,
1074
1006
  text: checked ? `Check element` : `Uncheck element`,
1007
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
1075
1008
  operation: "setCheck",
1076
1009
  log: "***** check " + selectors.element_name + " *****\n",
1077
1010
  };
@@ -1081,9 +1014,15 @@ class StableBrowser {
1081
1014
  // let element = await this._locate(selectors, info, _params);
1082
1015
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1083
1016
  try {
1084
- // await this._highlightElements(element);
1017
+ // if (world && world.screenshot && !world.screenshotPath) {
1018
+ // console.log(`Highlighting while running from recorder`);
1019
+ await this._highlightElements(element);
1085
1020
  await state.element.setChecked(checked);
1086
1021
  await new Promise((resolve) => setTimeout(resolve, 1000));
1022
+ // await this._unHighlightElements(element);
1023
+ // }
1024
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1025
+ // await this._unHighlightElements(element);
1087
1026
  }
1088
1027
  catch (e) {
1089
1028
  if (e.message && e.message.includes("did not change its state")) {
@@ -1115,22 +1054,13 @@ class StableBrowser {
1115
1054
  world,
1116
1055
  type: Types.HOVER,
1117
1056
  text: `Hover element`,
1057
+ _text: `Hover on ${selectors.element_name}`,
1118
1058
  operation: "hover",
1119
1059
  log: "***** hover " + selectors.element_name + " *****\n",
1120
1060
  };
1121
1061
  try {
1122
1062
  await _preCommand(state, this);
1123
- try {
1124
- await state.element.hover();
1125
- await new Promise((resolve) => setTimeout(resolve, 1000));
1126
- }
1127
- catch (e) {
1128
- //await this.closeUnexpectedPopups();
1129
- state.info.log += "hover failed, will try again" + "\n";
1130
- state.element = await this._locate(selectors, state.info, _params);
1131
- await state.element.hover({ timeout: 10000 });
1132
- await new Promise((resolve) => setTimeout(resolve, 1000));
1133
- }
1063
+ await performAction("hover", state.element, options, this, state, _params);
1134
1064
  await _screenshot(state, this);
1135
1065
  await this.waitForPageLoad();
1136
1066
  return state.info;
@@ -1154,6 +1084,7 @@ class StableBrowser {
1154
1084
  value: values.toString(),
1155
1085
  type: Types.SELECT,
1156
1086
  text: `Select option: ${values}`,
1087
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1157
1088
  operation: "selectOption",
1158
1089
  log: "***** select option " + selectors.element_name + " *****\n",
1159
1090
  };
@@ -1188,6 +1119,7 @@ class StableBrowser {
1188
1119
  highlight: false,
1189
1120
  type: Types.TYPE_PRESS,
1190
1121
  text: `Type value: ${_value}`,
1122
+ _text: `Type value: ${_value}`,
1191
1123
  operation: "type",
1192
1124
  log: "",
1193
1125
  };
@@ -1267,6 +1199,7 @@ class StableBrowser {
1267
1199
  world,
1268
1200
  type: Types.SET_DATE_TIME,
1269
1201
  text: `Set date time value: ${value}`,
1202
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1270
1203
  operation: "setDateTime",
1271
1204
  log: "***** set date time value " + selectors.element_name + " *****\n",
1272
1205
  throwError: false,
@@ -1274,7 +1207,7 @@ class StableBrowser {
1274
1207
  try {
1275
1208
  await _preCommand(state, this);
1276
1209
  try {
1277
- await state.element.click();
1210
+ await performAction("click", state.element, options, this, state, _params);
1278
1211
  await new Promise((resolve) => setTimeout(resolve, 500));
1279
1212
  if (format) {
1280
1213
  state.value = dayjs(state.value).format(format);
@@ -1338,6 +1271,7 @@ class StableBrowser {
1338
1271
  world,
1339
1272
  type: Types.FILL,
1340
1273
  text: `Click type input with value: ${_value}`,
1274
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1341
1275
  operation: "clickType",
1342
1276
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1343
1277
  };
@@ -1360,12 +1294,11 @@ class StableBrowser {
1360
1294
  }
1361
1295
  }
1362
1296
  if (options === null || options === undefined || options.press) {
1363
- try {
1364
- await state.element.click({ timeout: 5000 });
1365
- }
1366
- catch (e) {
1367
- await state.element.dispatchEvent("click");
1297
+ if (!options) {
1298
+ options = {};
1368
1299
  }
1300
+ options.timeout = 5000;
1301
+ await performAction("click", state.element, options, this, state, _params);
1369
1302
  }
1370
1303
  else {
1371
1304
  try {
@@ -1403,7 +1336,12 @@ class StableBrowser {
1403
1336
  await this.waitForPageLoad();
1404
1337
  }
1405
1338
  else if (enter === false) {
1406
- await state.element.dispatchEvent("change");
1339
+ try {
1340
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1341
+ }
1342
+ catch (e) {
1343
+ // ignore
1344
+ }
1407
1345
  //await this.page.keyboard.press("Tab");
1408
1346
  }
1409
1347
  else {
@@ -1455,6 +1393,7 @@ class StableBrowser {
1455
1393
  return await this._getText(selectors, 0, _params, options, info, world);
1456
1394
  }
1457
1395
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1396
+ const timeout = this._getFindElementTimeout(options);
1458
1397
  _validateSelectors(selectors);
1459
1398
  let screenshotId = null;
1460
1399
  let screenshotPath = null;
@@ -1464,7 +1403,7 @@ class StableBrowser {
1464
1403
  }
1465
1404
  info.operation = "getText";
1466
1405
  info.selectors = selectors;
1467
- let element = await this._locate(selectors, info, _params);
1406
+ let element = await this._locate(selectors, info, _params, timeout);
1468
1407
  if (climb > 0) {
1469
1408
  const climbArray = [];
1470
1409
  for (let i = 0; i < climb; i++) {
@@ -1483,6 +1422,18 @@ class StableBrowser {
1483
1422
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1484
1423
  try {
1485
1424
  await this._highlightElements(element);
1425
+ // if (world && world.screenshot && !world.screenshotPath) {
1426
+ // // console.log(`Highlighting for get text while running from recorder`);
1427
+ // this._highlightElements(element)
1428
+ // .then(async () => {
1429
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1430
+ // this._unhighlightElements(element).then(
1431
+ // () => {}
1432
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1433
+ // );
1434
+ // })
1435
+ // .catch(e);
1436
+ // }
1486
1437
  const elementText = await element.innerText();
1487
1438
  return {
1488
1439
  text: elementText,
@@ -1494,7 +1445,7 @@ class StableBrowser {
1494
1445
  }
1495
1446
  catch (e) {
1496
1447
  //await this.closeUnexpectedPopups();
1497
- this.logger.info("no innerText will use textContent");
1448
+ this.logger.info("no innerText, will use textContent");
1498
1449
  const elementText = await element.textContent();
1499
1450
  return { text: elementText, screenshotId, screenshotPath, value: value };
1500
1451
  }
@@ -1519,6 +1470,7 @@ class StableBrowser {
1519
1470
  highlight: false,
1520
1471
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1521
1472
  text: `Verify element contains pattern: ${pattern}`,
1473
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1522
1474
  operation: "containsPattern",
1523
1475
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1524
1476
  };
@@ -1554,6 +1506,8 @@ class StableBrowser {
1554
1506
  }
1555
1507
  }
1556
1508
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1509
+ const timeout = this._getFindElementTimeout(options);
1510
+ const startTime = Date.now();
1557
1511
  const state = {
1558
1512
  selectors,
1559
1513
  _params,
@@ -1580,62 +1534,54 @@ class StableBrowser {
1580
1534
  }
1581
1535
  let foundObj = null;
1582
1536
  try {
1583
- await _preCommand(state, this);
1584
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1585
- if (foundObj && foundObj.element) {
1586
- await this.scrollIfNeeded(foundObj.element, state.info);
1587
- }
1588
- await _screenshot(state, this);
1589
- const dateAlternatives = findDateAlternatives(text);
1590
- const numberAlternatives = findNumberAlternatives(text);
1591
- if (dateAlternatives.date) {
1592
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1593
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1594
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1595
- return state.info;
1537
+ while (Date.now() - startTime < timeout) {
1538
+ try {
1539
+ await _preCommand(state, this);
1540
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1541
+ if (foundObj && foundObj.element) {
1542
+ await this.scrollIfNeeded(foundObj.element, state.info);
1596
1543
  }
1597
- }
1598
- throw new Error("element doesn't contain text " + text);
1599
- }
1600
- else if (numberAlternatives.number) {
1601
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1602
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1603
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1544
+ await _screenshot(state, this);
1545
+ const dateAlternatives = findDateAlternatives(text);
1546
+ const numberAlternatives = findNumberAlternatives(text);
1547
+ if (dateAlternatives.date) {
1548
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1549
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1550
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1551
+ return state.info;
1552
+ }
1553
+ }
1554
+ }
1555
+ else if (numberAlternatives.number) {
1556
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1557
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1558
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1559
+ return state.info;
1560
+ }
1561
+ }
1562
+ }
1563
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1604
1564
  return state.info;
1605
1565
  }
1606
1566
  }
1607
- throw new Error("element doesn't contain text " + text);
1608
- }
1609
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1610
- state.info.foundText = foundObj?.text;
1611
- state.info.value = foundObj?.value;
1612
- throw new Error("element doesn't contain text " + text);
1567
+ catch (e) {
1568
+ // Log error but continue retrying until timeout is reached
1569
+ this.logger.warn("Retrying containsText due to: " + e.message);
1570
+ }
1571
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1613
1572
  }
1614
- return state.info;
1573
+ state.info.foundText = foundObj?.text;
1574
+ state.info.value = foundObj?.value;
1575
+ throw new Error("element doesn't contain text " + text);
1615
1576
  }
1616
1577
  catch (e) {
1617
1578
  await _commandError(state, e, this);
1579
+ throw e;
1618
1580
  }
1619
1581
  finally {
1620
1582
  _commandFinally(state, this);
1621
1583
  }
1622
1584
  }
1623
- _getDataFile(world = null) {
1624
- let dataFile = null;
1625
- if (world && world.reportFolder) {
1626
- dataFile = path.join(world.reportFolder, "data.json");
1627
- }
1628
- else if (this.reportFolder) {
1629
- dataFile = path.join(this.reportFolder, "data.json");
1630
- }
1631
- else if (this.context && this.context.reportFolder) {
1632
- dataFile = path.join(this.context.reportFolder, "data.json");
1633
- }
1634
- else {
1635
- dataFile = "data.json";
1636
- }
1637
- return dataFile;
1638
- }
1639
1585
  async waitForUserInput(message, world = null) {
1640
1586
  if (!message) {
1641
1587
  message = "# Wait for user input. Press any key to continue";
@@ -1664,7 +1610,7 @@ class StableBrowser {
1664
1610
  return;
1665
1611
  }
1666
1612
  // if data file exists, load it
1667
- const dataFile = this._getDataFile(world);
1613
+ const dataFile = _getDataFile(world, this.context, this);
1668
1614
  let data = this.getTestData(world);
1669
1615
  // merge the testData with the existing data
1670
1616
  Object.assign(data, testData);
@@ -1767,7 +1713,7 @@ class StableBrowser {
1767
1713
  }
1768
1714
  }
1769
1715
  getTestData(world = null) {
1770
- const dataFile = this._getDataFile(world);
1716
+ const dataFile = _getDataFile(world, this.context, this);
1771
1717
  let data = {};
1772
1718
  if (fs.existsSync(dataFile)) {
1773
1719
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1854,6 +1800,15 @@ class StableBrowser {
1854
1800
  document.documentElement.clientWidth,
1855
1801
  ])));
1856
1802
  let screenshotBuffer = null;
1803
+ // if (focusedElement) {
1804
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1805
+ // await this._unhighlightElements(focusedElement);
1806
+ // await new Promise((resolve) => setTimeout(resolve, 100));
1807
+ // console.log(`Unhighlighted previous element`);
1808
+ // }
1809
+ // if (focusedElement) {
1810
+ // await this._highlightElements(focusedElement);
1811
+ // }
1857
1812
  if (this.context.browserName === "chromium") {
1858
1813
  const client = await playContext.newCDPSession(this.page);
1859
1814
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1875,6 +1830,10 @@ class StableBrowser {
1875
1830
  else {
1876
1831
  screenshotBuffer = await this.page.screenshot();
1877
1832
  }
1833
+ // if (focusedElement) {
1834
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1835
+ // await this._unhighlightElements(focusedElement);
1836
+ // }
1878
1837
  let image = await Jimp.read(screenshotBuffer);
1879
1838
  // Get the image dimensions
1880
1839
  const { width, height } = image.bitmap;
@@ -1887,6 +1846,7 @@ class StableBrowser {
1887
1846
  else {
1888
1847
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1889
1848
  }
1849
+ return screenshotBuffer;
1890
1850
  }
1891
1851
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1892
1852
  const state = {
@@ -1922,8 +1882,10 @@ class StableBrowser {
1922
1882
  world,
1923
1883
  type: Types.EXTRACT,
1924
1884
  text: `Extract attribute from element`,
1885
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1925
1886
  operation: "extractAttribute",
1926
1887
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1888
+ allowDisabled: true,
1927
1889
  };
1928
1890
  await new Promise((resolve) => setTimeout(resolve, 2000));
1929
1891
  try {
@@ -1945,6 +1907,7 @@ class StableBrowser {
1945
1907
  state.info.value = state.value;
1946
1908
  this.setTestData({ [variable]: state.value }, world);
1947
1909
  this.logger.info("set test data: " + variable + "=" + state.value);
1910
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1948
1911
  return state.info;
1949
1912
  }
1950
1913
  catch (e) {
@@ -1963,14 +1926,21 @@ class StableBrowser {
1963
1926
  options,
1964
1927
  world,
1965
1928
  type: Types.VERIFY_ATTRIBUTE,
1929
+ highlight: true,
1930
+ screenshot: true,
1966
1931
  text: `Verify element attribute`,
1932
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1967
1933
  operation: "verifyAttribute",
1968
1934
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1935
+ allowDisabled: true,
1969
1936
  };
1970
1937
  await new Promise((resolve) => setTimeout(resolve, 2000));
1971
1938
  let val;
1939
+ let expectedValue;
1972
1940
  try {
1973
1941
  await _preCommand(state, this);
1942
+ expectedValue = state.value;
1943
+ state.info.expectedValue = expectedValue;
1974
1944
  switch (attribute) {
1975
1945
  case "innerText":
1976
1946
  val = String(await state.element.innerText());
@@ -1984,23 +1954,30 @@ class StableBrowser {
1984
1954
  case "disabled":
1985
1955
  val = String(await state.element.isDisabled());
1986
1956
  break;
1957
+ case "readOnly":
1958
+ const isEditable = await state.element.isEditable();
1959
+ val = String(!isEditable);
1960
+ break;
1987
1961
  default:
1988
1962
  val = String(await state.element.getAttribute(attribute));
1989
1963
  break;
1990
1964
  }
1965
+ state.info.value = val;
1991
1966
  let regex;
1992
- if (value.startsWith("/") && value.endsWith("/")) {
1993
- const patternBody = value.slice(1, -1);
1967
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1968
+ const patternBody = expectedValue.slice(1, -1);
1994
1969
  regex = new RegExp(patternBody, "g");
1995
1970
  }
1996
1971
  else {
1997
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1972
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1998
1973
  regex = new RegExp(escapedPattern, "g");
1999
1974
  }
2000
1975
  if (!val.match(regex)) {
2001
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
1976
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
1977
+ state.info.failCause.assertionFailed = true;
1978
+ state.info.failCause.lastError = errorMessage;
1979
+ throw new Error(errorMessage);
2002
1980
  }
2003
- state.info.value = val;
2004
1981
  return state.info;
2005
1982
  }
2006
1983
  catch (e) {
@@ -2028,7 +2005,7 @@ class StableBrowser {
2028
2005
  if (options && options.timeout) {
2029
2006
  timeout = options.timeout;
2030
2007
  }
2031
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2008
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2032
2009
  const request = {
2033
2010
  method: "POST",
2034
2011
  url: serviceUrl,
@@ -2099,27 +2076,32 @@ class StableBrowser {
2099
2076
  async _highlightElements(scope, css) {
2100
2077
  try {
2101
2078
  if (!scope) {
2079
+ // console.log(`Scope is not defined`);
2102
2080
  return;
2103
2081
  }
2104
2082
  if (!css) {
2105
2083
  scope
2106
2084
  .evaluate((node) => {
2107
2085
  if (node && node.style) {
2108
- let originalBorder = node.style.border;
2109
- node.style.border = "2px solid red";
2086
+ let originalOutline = node.style.outline;
2087
+ // console.log(`Original outline was: ${originalOutline}`);
2088
+ // node.__previousOutline = originalOutline;
2089
+ node.style.outline = "2px solid red";
2090
+ // console.log(`New outline is: ${node.style.outline}`);
2110
2091
  if (window) {
2111
2092
  window.addEventListener("beforeunload", function (e) {
2112
- node.style.border = originalBorder;
2093
+ node.style.outline = originalOutline;
2113
2094
  });
2114
2095
  }
2115
2096
  setTimeout(function () {
2116
- node.style.border = originalBorder;
2097
+ node.style.outline = originalOutline;
2117
2098
  }, 2000);
2118
2099
  }
2119
2100
  })
2120
2101
  .then(() => { })
2121
2102
  .catch((e) => {
2122
2103
  // ignore
2104
+ // console.error(`Could not highlight node : ${e}`);
2123
2105
  });
2124
2106
  }
2125
2107
  else {
@@ -2135,17 +2117,18 @@ class StableBrowser {
2135
2117
  if (!element.style) {
2136
2118
  return;
2137
2119
  }
2138
- var originalBorder = element.style.border;
2120
+ let originalOutline = element.style.outline;
2121
+ element.__previousOutline = originalOutline;
2139
2122
  // Set the new border to be red and 2px solid
2140
- element.style.border = "2px solid red";
2123
+ element.style.outline = "2px solid red";
2141
2124
  if (window) {
2142
2125
  window.addEventListener("beforeunload", function (e) {
2143
- element.style.border = originalBorder;
2126
+ element.style.outline = originalOutline;
2144
2127
  });
2145
2128
  }
2146
2129
  // Set a timeout to revert to the original border after 2 seconds
2147
2130
  setTimeout(function () {
2148
- element.style.border = originalBorder;
2131
+ element.style.outline = originalOutline;
2149
2132
  }, 2000);
2150
2133
  }
2151
2134
  return;
@@ -2153,6 +2136,7 @@ class StableBrowser {
2153
2136
  .then(() => { })
2154
2137
  .catch((e) => {
2155
2138
  // ignore
2139
+ // console.error(`Could not highlight css: ${e}`);
2156
2140
  });
2157
2141
  }
2158
2142
  }
@@ -2160,6 +2144,54 @@ class StableBrowser {
2160
2144
  console.debug(error);
2161
2145
  }
2162
2146
  }
2147
+ // async _unhighlightElements(scope, css) {
2148
+ // try {
2149
+ // if (!scope) {
2150
+ // return;
2151
+ // }
2152
+ // if (!css) {
2153
+ // scope
2154
+ // .evaluate((node) => {
2155
+ // if (node && node.style) {
2156
+ // if (!node.__previousOutline) {
2157
+ // node.style.outline = "";
2158
+ // } else {
2159
+ // node.style.outline = node.__previousOutline;
2160
+ // }
2161
+ // }
2162
+ // })
2163
+ // .then(() => {})
2164
+ // .catch((e) => {
2165
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2166
+ // });
2167
+ // } else {
2168
+ // scope
2169
+ // .evaluate(([css]) => {
2170
+ // if (!css) {
2171
+ // return;
2172
+ // }
2173
+ // let elements = Array.from(document.querySelectorAll(css));
2174
+ // for (i = 0; i < elements.length; i++) {
2175
+ // let element = elements[i];
2176
+ // if (!element.style) {
2177
+ // return;
2178
+ // }
2179
+ // if (!element.__previousOutline) {
2180
+ // element.style.outline = "";
2181
+ // } else {
2182
+ // element.style.outline = element.__previousOutline;
2183
+ // }
2184
+ // }
2185
+ // })
2186
+ // .then(() => {})
2187
+ // .catch((e) => {
2188
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2189
+ // });
2190
+ // }
2191
+ // } catch (error) {
2192
+ // // console.debug(error);
2193
+ // }
2194
+ // }
2163
2195
  async verifyPagePath(pathPart, options = {}, world = null) {
2164
2196
  const startTime = Date.now();
2165
2197
  let error = null;
@@ -2204,6 +2236,7 @@ class StableBrowser {
2204
2236
  _reportToWorld(world, {
2205
2237
  type: Types.VERIFY_PAGE_PATH,
2206
2238
  text: "Verify page path",
2239
+ _text: "Verify the page path contains " + pathPart,
2207
2240
  screenshotId,
2208
2241
  result: error
2209
2242
  ? {
@@ -2221,6 +2254,35 @@ class StableBrowser {
2221
2254
  });
2222
2255
  }
2223
2256
  }
2257
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2258
+ const frames = this.page.frames();
2259
+ let results = [];
2260
+ let ignoreCase = false;
2261
+ for (let i = 0; i < frames.length; i++) {
2262
+ if (dateAlternatives.date) {
2263
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
2264
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2265
+ result.frame = frames[i];
2266
+ results.push(result);
2267
+ }
2268
+ }
2269
+ else if (numberAlternatives.number) {
2270
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2271
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2272
+ result.frame = frames[i];
2273
+ results.push(result);
2274
+ }
2275
+ }
2276
+ else {
2277
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2278
+ result.frame = frames[i];
2279
+ results.push(result);
2280
+ }
2281
+ }
2282
+ state.info.results = results;
2283
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2284
+ return resultWithElementsFound;
2285
+ }
2224
2286
  async verifyTextExistInPage(text, options = {}, world = null) {
2225
2287
  text = unEscapeString(text);
2226
2288
  const state = {
@@ -2230,12 +2292,16 @@ class StableBrowser {
2230
2292
  locate: false,
2231
2293
  scroll: false,
2232
2294
  highlight: false,
2233
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2295
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2234
2296
  text: `Verify text exists in page`,
2297
+ _text: `Verify the text '${text}' exists in page`,
2235
2298
  operation: "verifyTextExistInPage",
2236
2299
  log: "***** verify text " + text + " exists in page *****\n",
2237
2300
  };
2238
- const timeout = this._getLoadTimeout(options);
2301
+ if (testForRegex(text)) {
2302
+ text = text.replace(/\\"/g, '"');
2303
+ }
2304
+ const timeout = this._getFindElementTimeout(options);
2239
2305
  await new Promise((resolve) => setTimeout(resolve, 2000));
2240
2306
  const newValue = await this._replaceWithLocalData(text, world);
2241
2307
  if (newValue !== text) {
@@ -2248,31 +2314,15 @@ class StableBrowser {
2248
2314
  await _preCommand(state, this);
2249
2315
  state.info.text = text;
2250
2316
  while (true) {
2251
- const frames = this.page.frames();
2252
- let results = [];
2253
- for (let i = 0; i < frames.length; i++) {
2254
- if (dateAlternatives.date) {
2255
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2256
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
2257
- result.frame = frames[i];
2258
- results.push(result);
2259
- }
2260
- }
2261
- else if (numberAlternatives.number) {
2262
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2263
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
2264
- result.frame = frames[i];
2265
- results.push(result);
2266
- }
2267
- }
2268
- else {
2269
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
2270
- result.frame = frames[i];
2271
- results.push(result);
2272
- }
2317
+ let resultWithElementsFound = {
2318
+ length: 0,
2319
+ };
2320
+ try {
2321
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2322
+ }
2323
+ catch (error) {
2324
+ // ignore
2273
2325
  }
2274
- state.info.results = results;
2275
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2276
2326
  if (resultWithElementsFound.length === 0) {
2277
2327
  if (Date.now() - state.startTime > timeout) {
2278
2328
  throw new Error(`Text ${text} not found in page`);
@@ -2280,18 +2330,40 @@ class StableBrowser {
2280
2330
  await new Promise((resolve) => setTimeout(resolve, 1000));
2281
2331
  continue;
2282
2332
  }
2283
- if (resultWithElementsFound[0].randomToken) {
2284
- const frame = resultWithElementsFound[0].frame;
2285
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2286
- await this._highlightElements(frame, dataAttribute);
2287
- const element = await frame.$(dataAttribute);
2288
- if (element) {
2289
- await this.scrollIfNeeded(element, state.info);
2290
- await element.dispatchEvent("bvt_verify_page_contains_text");
2333
+ try {
2334
+ if (resultWithElementsFound[0].randomToken) {
2335
+ const frame = resultWithElementsFound[0].frame;
2336
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2337
+ await this._highlightElements(frame, dataAttribute);
2338
+ // if (world && world.screenshot && !world.screenshotPath) {
2339
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2340
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2341
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2342
+ // this._unhighlightElements(frame, dataAttribute)
2343
+ // .then(async () => {
2344
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2345
+ // })
2346
+ // .catch(
2347
+ // (e) => {}
2348
+ // console.error(e)
2349
+ // );
2350
+ // });
2351
+ // }
2352
+ const element = await frame.locator(dataAttribute).first();
2353
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2354
+ // await this._unhighlightElements(frame, dataAttribute);
2355
+ if (element) {
2356
+ await this.scrollIfNeeded(element, state.info);
2357
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2358
+ // await _screenshot(state, this, element);
2359
+ }
2291
2360
  }
2361
+ await _screenshot(state, this);
2362
+ return state.info;
2363
+ }
2364
+ catch (error) {
2365
+ console.error(error);
2292
2366
  }
2293
- await _screenshot(state, this);
2294
- return state.info;
2295
2367
  }
2296
2368
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2297
2369
  }
@@ -2313,10 +2385,14 @@ class StableBrowser {
2313
2385
  highlight: false,
2314
2386
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2315
2387
  text: `Verify text does not exist in page`,
2388
+ _text: `Verify the text '${text}' does not exist in page`,
2316
2389
  operation: "verifyTextNotExistInPage",
2317
2390
  log: "***** verify text " + text + " does not exist in page *****\n",
2318
2391
  };
2319
- const timeout = this._getLoadTimeout(options);
2392
+ if (testForRegex(text)) {
2393
+ text = text.replace(/\\"/g, '"');
2394
+ }
2395
+ const timeout = this._getFindElementTimeout(options);
2320
2396
  await new Promise((resolve) => setTimeout(resolve, 2000));
2321
2397
  const newValue = await this._replaceWithLocalData(text, world);
2322
2398
  if (newValue !== text) {
@@ -2328,32 +2404,16 @@ class StableBrowser {
2328
2404
  try {
2329
2405
  await _preCommand(state, this);
2330
2406
  state.info.text = text;
2407
+ let resultWithElementsFound = {
2408
+ length: null, // initial cannot be 0
2409
+ };
2331
2410
  while (true) {
2332
- const frames = this.page.frames();
2333
- let results = [];
2334
- for (let i = 0; i < frames.length; i++) {
2335
- if (dateAlternatives.date) {
2336
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2337
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
2338
- result.frame = frames[i];
2339
- results.push(result);
2340
- }
2341
- }
2342
- else if (numberAlternatives.number) {
2343
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2344
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
2345
- result.frame = frames[i];
2346
- results.push(result);
2347
- }
2348
- }
2349
- else {
2350
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
2351
- result.frame = frames[i];
2352
- results.push(result);
2353
- }
2411
+ try {
2412
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2413
+ }
2414
+ catch (error) {
2415
+ // ignore
2354
2416
  }
2355
- state.info.results = results;
2356
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2357
2417
  if (resultWithElementsFound.length === 0) {
2358
2418
  await _screenshot(state, this);
2359
2419
  return state.info;
@@ -2371,15 +2431,116 @@ class StableBrowser {
2371
2431
  _commandFinally(state, this);
2372
2432
  }
2373
2433
  }
2374
- _getServerUrl() {
2375
- let serviceUrl = "https://api.blinq.io";
2376
- if (process.env.NODE_ENV_BLINQ === "dev") {
2377
- serviceUrl = "https://dev.api.blinq.io";
2434
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
2435
+ textAnchor = unEscapeString(textAnchor);
2436
+ textToVerify = unEscapeString(textToVerify);
2437
+ const state = {
2438
+ text_search: textToVerify,
2439
+ options,
2440
+ world,
2441
+ locate: false,
2442
+ scroll: false,
2443
+ highlight: false,
2444
+ type: Types.VERIFY_TEXT_WITH_RELATION,
2445
+ text: `Verify text with relation to another text`,
2446
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2447
+ operation: "verify_text_with_relation",
2448
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2449
+ };
2450
+ const timeout = this._getFindElementTimeout(options);
2451
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2452
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
2453
+ if (newValue !== textAnchor) {
2454
+ this.logger.info(textAnchor + "=" + newValue);
2455
+ textAnchor = newValue;
2456
+ }
2457
+ newValue = await this._replaceWithLocalData(textToVerify, world);
2458
+ if (newValue !== textToVerify) {
2459
+ this.logger.info(textToVerify + "=" + newValue);
2460
+ textToVerify = newValue;
2461
+ }
2462
+ let dateAlternatives = findDateAlternatives(textToVerify);
2463
+ let numberAlternatives = findNumberAlternatives(textToVerify);
2464
+ let foundAncore = false;
2465
+ try {
2466
+ await _preCommand(state, this);
2467
+ state.info.text = textToVerify;
2468
+ let resultWithElementsFound = {
2469
+ length: 0,
2470
+ };
2471
+ while (true) {
2472
+ try {
2473
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2474
+ }
2475
+ catch (error) {
2476
+ // ignore
2477
+ }
2478
+ if (resultWithElementsFound.length === 0) {
2479
+ if (Date.now() - state.startTime > timeout) {
2480
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2481
+ }
2482
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2483
+ continue;
2484
+ }
2485
+ try {
2486
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2487
+ foundAncore = true;
2488
+ const result = resultWithElementsFound[i];
2489
+ const token = result.randomToken;
2490
+ const frame = result.frame;
2491
+ let css = `[data-blinq-id-${token}]`;
2492
+ const climbArray1 = [];
2493
+ for (let i = 0; i < climb; i++) {
2494
+ climbArray1.push("..");
2495
+ }
2496
+ let climbXpath = "xpath=" + climbArray1.join("/");
2497
+ css = css + " >> " + climbXpath;
2498
+ const count = await frame.locator(css).count();
2499
+ for (let j = 0; j < count; j++) {
2500
+ const continer = await frame.locator(css).nth(j);
2501
+ const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2502
+ if (result.elementCount > 0) {
2503
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2504
+ await this._highlightElements(frame, dataAttribute);
2505
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2506
+ // if (world && world.screenshot && !world.screenshotPath) {
2507
+ // console.log(`Highlighting for vtrt while running from recorder`);
2508
+ // this._highlightElements(frame, dataAttribute)
2509
+ // .then(async () => {
2510
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2511
+ // this._unhighlightElements(frame, dataAttribute).then(
2512
+ // () => {}
2513
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2514
+ // );
2515
+ // })
2516
+ // .catch(e);
2517
+ // }
2518
+ //await this._highlightElements(frame, cssAnchor);
2519
+ const element = await frame.locator(dataAttribute).first();
2520
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2521
+ // await this._unhighlightElements(frame, dataAttribute);
2522
+ if (element) {
2523
+ await this.scrollIfNeeded(element, state.info);
2524
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2525
+ }
2526
+ await _screenshot(state, this);
2527
+ return state.info;
2528
+ }
2529
+ }
2530
+ }
2531
+ }
2532
+ catch (error) {
2533
+ console.error(error);
2534
+ }
2535
+ }
2536
+ // await expect(element).toHaveCount(1, { timeout: 10000 });
2378
2537
  }
2379
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2380
- serviceUrl = "https://stage.api.blinq.io";
2538
+ catch (e) {
2539
+ await _commandError(state, e, this);
2540
+ }
2541
+ finally {
2542
+ _commandFinally(state, this);
2381
2543
  }
2382
- return serviceUrl;
2383
2544
  }
2384
2545
  async visualVerification(text, options = {}, world = null) {
2385
2546
  const startTime = Date.now();
@@ -2395,14 +2556,17 @@ class StableBrowser {
2395
2556
  throw new Error("TOKEN is not set");
2396
2557
  }
2397
2558
  try {
2398
- let serviceUrl = this._getServerUrl();
2559
+ let serviceUrl = _getServerUrl();
2399
2560
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2400
2561
  info.screenshotPath = screenshotPath;
2401
2562
  const screenshot = await this.takeScreenshot();
2402
- const request = {
2403
- method: "POST",
2563
+ let request = {
2564
+ method: "post",
2565
+ maxBodyLength: Infinity,
2404
2566
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2405
2567
  headers: {
2568
+ "x-bvt-project-id": path.basename(this.project_path),
2569
+ "x-source": "aaa",
2406
2570
  "Content-Type": "application/json",
2407
2571
  Authorization: `Bearer ${process.env.TOKEN}`,
2408
2572
  },
@@ -2411,7 +2575,7 @@ class StableBrowser {
2411
2575
  screenshot: screenshot,
2412
2576
  }),
2413
2577
  };
2414
- let result = await this.context.api.request(request);
2578
+ const result = await axios.request(request);
2415
2579
  if (result.data.status !== true) {
2416
2580
  throw new Error("Visual validation failed");
2417
2581
  }
@@ -2439,6 +2603,7 @@ class StableBrowser {
2439
2603
  _reportToWorld(world, {
2440
2604
  type: Types.VERIFY_VISUAL,
2441
2605
  text: "Visual verification",
2606
+ _text: "Visual verification of " + text,
2442
2607
  screenshotId,
2443
2608
  result: error
2444
2609
  ? {
@@ -2560,7 +2725,7 @@ class StableBrowser {
2560
2725
  info.operation = "analyzeTable";
2561
2726
  info.selectors = selectors;
2562
2727
  info.query = query;
2563
- query = this._fixUsingParams(query, _params);
2728
+ query = _fixUsingParams(query, _params);
2564
2729
  info.query_fixed = query;
2565
2730
  info.operator = operator;
2566
2731
  info.value = value;
@@ -2705,6 +2870,32 @@ class StableBrowser {
2705
2870
  }
2706
2871
  return timeout;
2707
2872
  }
2873
+ _getFindElementTimeout(options) {
2874
+ if (options && options.timeout) {
2875
+ return options.timeout;
2876
+ }
2877
+ if (this.configuration.find_element_timeout) {
2878
+ return this.configuration.find_element_timeout;
2879
+ }
2880
+ return 30000;
2881
+ }
2882
+ async saveStoreState(path = null, world = null) {
2883
+ const storageState = await this.page.context().storageState();
2884
+ //const testDataFile = _getDataFile(world, this.context, this);
2885
+ if (path) {
2886
+ // save { storageState: storageState } into the path
2887
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
2888
+ }
2889
+ else {
2890
+ await this.setTestData({ storageState: storageState }, world);
2891
+ }
2892
+ }
2893
+ async restoreSaveState(path = null, world = null) {
2894
+ await refreshBrowser(this, path, world);
2895
+ this.registerEventListeners(this.context);
2896
+ registerNetworkEvents(this.world, this, this.context, this.page);
2897
+ registerDownloadEvent(this.page, this.world, this.context);
2898
+ }
2708
2899
  async waitForPageLoad(options = {}, world = null) {
2709
2900
  let timeout = this._getLoadTimeout(options);
2710
2901
  const promiseArray = [];
@@ -2772,6 +2963,7 @@ class StableBrowser {
2772
2963
  highlight: false,
2773
2964
  type: Types.CLOSE_PAGE,
2774
2965
  text: `Close page`,
2966
+ _text: `Close the page`,
2775
2967
  operation: "closePage",
2776
2968
  log: "***** close page *****\n",
2777
2969
  throwError: false,
@@ -2788,8 +2980,95 @@ class StableBrowser {
2788
2980
  _commandFinally(state, this);
2789
2981
  }
2790
2982
  }
2983
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
2984
+ let operation = null;
2985
+ if (!options || !options.operation) {
2986
+ throw new Error("operation is not defined");
2987
+ }
2988
+ operation = options.operation;
2989
+ // validate operation is one of the supported operations
2990
+ if (operation != "click" && operation != "hover+click") {
2991
+ throw new Error("operation is not supported");
2992
+ }
2993
+ const state = {
2994
+ options,
2995
+ world,
2996
+ locate: false,
2997
+ scroll: false,
2998
+ highlight: false,
2999
+ type: Types.TABLE_OPERATION,
3000
+ text: `Table operation`,
3001
+ _text: `Table ${operation} operation`,
3002
+ operation: operation,
3003
+ log: "***** Table operation *****\n",
3004
+ };
3005
+ const timeout = this._getFindElementTimeout(options);
3006
+ try {
3007
+ await _preCommand(state, this);
3008
+ const start = Date.now();
3009
+ let cellArea = null;
3010
+ while (true) {
3011
+ try {
3012
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3013
+ if (cellArea) {
3014
+ break;
3015
+ }
3016
+ }
3017
+ catch (e) {
3018
+ // ignore
3019
+ }
3020
+ if (Date.now() - start > timeout) {
3021
+ throw new Error(`Cell not found in table`);
3022
+ }
3023
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3024
+ }
3025
+ switch (operation) {
3026
+ case "click":
3027
+ if (!options.css) {
3028
+ // will click in the center of the cell
3029
+ let xOffset = 0;
3030
+ let yOffset = 0;
3031
+ if (options.xOffset) {
3032
+ xOffset = options.xOffset;
3033
+ }
3034
+ if (options.yOffset) {
3035
+ yOffset = options.yOffset;
3036
+ }
3037
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3038
+ }
3039
+ else {
3040
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3041
+ if (results.length === 0) {
3042
+ throw new Error(`Element not found in cell area`);
3043
+ }
3044
+ state.element = results[0];
3045
+ await performAction("click", state.element, options, this, state, _params);
3046
+ }
3047
+ break;
3048
+ case "hover+click":
3049
+ if (!options.css) {
3050
+ throw new Error("css is not defined");
3051
+ }
3052
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3053
+ if (results.length === 0) {
3054
+ throw new Error(`Element not found in cell area`);
3055
+ }
3056
+ state.element = results[0];
3057
+ await performAction("hover+click", state.element, options, this, state, _params);
3058
+ break;
3059
+ default:
3060
+ throw new Error("operation is not supported");
3061
+ }
3062
+ }
3063
+ catch (e) {
3064
+ await _commandError(state, e, this);
3065
+ }
3066
+ finally {
3067
+ _commandFinally(state, this);
3068
+ }
3069
+ }
2791
3070
  saveTestDataAsGlobal(options, world) {
2792
- const dataFile = this._getDataFile(world);
3071
+ const dataFile = _getDataFile(world, this.context, this);
2793
3072
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2794
3073
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2795
3074
  }
@@ -2819,6 +3098,7 @@ class StableBrowser {
2819
3098
  _reportToWorld(world, {
2820
3099
  type: Types.SET_VIEWPORT,
2821
3100
  text: "set viewport size to " + width + "x" + hight,
3101
+ _text: "Set the viewport size to " + width + "x" + hight,
2822
3102
  screenshotId,
2823
3103
  result: error
2824
3104
  ? {
@@ -2890,14 +3170,25 @@ class StableBrowser {
2890
3170
  }
2891
3171
  }
2892
3172
  async beforeStep(world, step) {
2893
- this.stepName = step.pickleStep.text;
2894
- this.logger.info("step: " + this.stepName);
2895
3173
  if (this.stepIndex === undefined) {
2896
3174
  this.stepIndex = 0;
2897
3175
  }
2898
3176
  else {
2899
3177
  this.stepIndex++;
2900
3178
  }
3179
+ if (step && step.pickleStep && step.pickleStep.text) {
3180
+ this.stepName = step.pickleStep.text;
3181
+ this.logger.info("step: " + this.stepName);
3182
+ }
3183
+ else if (step && step.text) {
3184
+ this.stepName = step.text;
3185
+ }
3186
+ else {
3187
+ this.stepName = "step " + this.stepIndex;
3188
+ }
3189
+ if (this.context) {
3190
+ this.context.examplesRow = extractStepExampleParameters(step);
3191
+ }
2901
3192
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2902
3193
  if (this.context.browserObject.context) {
2903
3194
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2910,6 +3201,41 @@ class StableBrowser {
2910
3201
  this.saveTestDataAsGlobal({}, world);
2911
3202
  }
2912
3203
  }
3204
+ if (this.initSnapshotTaken === false) {
3205
+ this.initSnapshotTaken = true;
3206
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3207
+ const snapshot = await this.getAriaSnapshot();
3208
+ if (snapshot) {
3209
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3210
+ }
3211
+ }
3212
+ }
3213
+ }
3214
+ async getAriaSnapshot() {
3215
+ try {
3216
+ // find the page url
3217
+ const url = await this.page.url();
3218
+ // extract the path from the url
3219
+ const path = new URL(url).pathname;
3220
+ // get the page title
3221
+ const title = await this.page.title();
3222
+ // go over other frams
3223
+ const frames = this.page.frames();
3224
+ const snapshots = [];
3225
+ const content = [`- path: ${path}`, `- title: ${title}`];
3226
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3227
+ for (let i = 0; i < frames.length; i++) {
3228
+ content.push(`- frame: ${i}`);
3229
+ const frame = frames[i];
3230
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3231
+ content.push(snapshot);
3232
+ }
3233
+ return content.join("\n");
3234
+ }
3235
+ catch (e) {
3236
+ console.error(e);
3237
+ }
3238
+ return null;
2913
3239
  }
2914
3240
  async afterStep(world, step) {
2915
3241
  this.stepName = null;
@@ -2920,6 +3246,16 @@ class StableBrowser {
2920
3246
  });
2921
3247
  }
2922
3248
  }
3249
+ if (this.context) {
3250
+ this.context.examplesRow = null;
3251
+ }
3252
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3253
+ const snapshot = await this.getAriaSnapshot();
3254
+ if (snapshot) {
3255
+ const obj = {};
3256
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3257
+ }
3258
+ }
2923
3259
  }
2924
3260
  }
2925
3261
  function createTimedPromise(promise, label) {
@@ -2927,156 +3263,5 @@ function createTimedPromise(promise, label) {
2927
3263
  .then((result) => ({ status: "fulfilled", label, result }))
2928
3264
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2929
3265
  }
2930
- const KEYBOARD_EVENTS = [
2931
- "ALT",
2932
- "AltGraph",
2933
- "CapsLock",
2934
- "Control",
2935
- "Fn",
2936
- "FnLock",
2937
- "Hyper",
2938
- "Meta",
2939
- "NumLock",
2940
- "ScrollLock",
2941
- "Shift",
2942
- "Super",
2943
- "Symbol",
2944
- "SymbolLock",
2945
- "Enter",
2946
- "Tab",
2947
- "ArrowDown",
2948
- "ArrowLeft",
2949
- "ArrowRight",
2950
- "ArrowUp",
2951
- "End",
2952
- "Home",
2953
- "PageDown",
2954
- "PageUp",
2955
- "Backspace",
2956
- "Clear",
2957
- "Copy",
2958
- "CrSel",
2959
- "Cut",
2960
- "Delete",
2961
- "EraseEof",
2962
- "ExSel",
2963
- "Insert",
2964
- "Paste",
2965
- "Redo",
2966
- "Undo",
2967
- "Accept",
2968
- "Again",
2969
- "Attn",
2970
- "Cancel",
2971
- "ContextMenu",
2972
- "Escape",
2973
- "Execute",
2974
- "Find",
2975
- "Finish",
2976
- "Help",
2977
- "Pause",
2978
- "Play",
2979
- "Props",
2980
- "Select",
2981
- "ZoomIn",
2982
- "ZoomOut",
2983
- "BrightnessDown",
2984
- "BrightnessUp",
2985
- "Eject",
2986
- "LogOff",
2987
- "Power",
2988
- "PowerOff",
2989
- "PrintScreen",
2990
- "Hibernate",
2991
- "Standby",
2992
- "WakeUp",
2993
- "AllCandidates",
2994
- "Alphanumeric",
2995
- "CodeInput",
2996
- "Compose",
2997
- "Convert",
2998
- "Dead",
2999
- "FinalMode",
3000
- "GroupFirst",
3001
- "GroupLast",
3002
- "GroupNext",
3003
- "GroupPrevious",
3004
- "ModeChange",
3005
- "NextCandidate",
3006
- "NonConvert",
3007
- "PreviousCandidate",
3008
- "Process",
3009
- "SingleCandidate",
3010
- "HangulMode",
3011
- "HanjaMode",
3012
- "JunjaMode",
3013
- "Eisu",
3014
- "Hankaku",
3015
- "Hiragana",
3016
- "HiraganaKatakana",
3017
- "KanaMode",
3018
- "KanjiMode",
3019
- "Katakana",
3020
- "Romaji",
3021
- "Zenkaku",
3022
- "ZenkakuHanaku",
3023
- "F1",
3024
- "F2",
3025
- "F3",
3026
- "F4",
3027
- "F5",
3028
- "F6",
3029
- "F7",
3030
- "F8",
3031
- "F9",
3032
- "F10",
3033
- "F11",
3034
- "F12",
3035
- "Soft1",
3036
- "Soft2",
3037
- "Soft3",
3038
- "Soft4",
3039
- "ChannelDown",
3040
- "ChannelUp",
3041
- "Close",
3042
- "MailForward",
3043
- "MailReply",
3044
- "MailSend",
3045
- "MediaFastForward",
3046
- "MediaPause",
3047
- "MediaPlay",
3048
- "MediaPlayPause",
3049
- "MediaRecord",
3050
- "MediaRewind",
3051
- "MediaStop",
3052
- "MediaTrackNext",
3053
- "MediaTrackPrevious",
3054
- "AudioBalanceLeft",
3055
- "AudioBalanceRight",
3056
- "AudioBassBoostDown",
3057
- "AudioBassBoostToggle",
3058
- "AudioBassBoostUp",
3059
- "AudioFaderFront",
3060
- "AudioFaderRear",
3061
- "AudioSurroundModeNext",
3062
- "AudioTrebleDown",
3063
- "AudioTrebleUp",
3064
- "AudioVolumeDown",
3065
- "AudioVolumeMute",
3066
- "AudioVolumeUp",
3067
- "MicrophoneToggle",
3068
- "MicrophoneVolumeDown",
3069
- "MicrophoneVolumeMute",
3070
- "MicrophoneVolumeUp",
3071
- "TV",
3072
- "TV3DMode",
3073
- "TVAntennaCable",
3074
- "TVAudioDescription",
3075
- ];
3076
- function unEscapeString(str) {
3077
- const placeholder = "__NEWLINE__";
3078
- str = str.replace(new RegExp(placeholder, "g"), "\n");
3079
- return str;
3080
- }
3081
3266
  export { StableBrowser };
3082
3267
  //# sourceMappingURL=stable_browser.js.map