automation_model 1.0.550-dev → 1.0.550-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, } 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,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,15 +946,16 @@ 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
- }
956
+ // if (state.options && state.options.context) {
957
+ // state.selectors.locators[0].text = state.options.context;
958
+ // }
1047
959
  try {
1048
960
  await state.element.click();
1049
961
  // await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1064,6 +976,38 @@ class StableBrowser {
1064
976
  _commandFinally(state, this);
1065
977
  }
1066
978
  }
979
+ async waitForElement(selectors, _params, options = {}, world = null) {
980
+ const timeout = this._getFindElementTimeout(options);
981
+ const state = {
982
+ selectors,
983
+ _params,
984
+ options,
985
+ world,
986
+ text: "Wait for element",
987
+ _text: "Wait for " + selectors.element_name,
988
+ type: Types.WAIT_ELEMENT,
989
+ operation: "waitForElement",
990
+ log: "***** wait for " + selectors.element_name + " *****\n",
991
+ };
992
+ let found = false;
993
+ try {
994
+ await _preCommand(state, this);
995
+ // if (state.options && state.options.context) {
996
+ // state.selectors.locators[0].text = state.options.context;
997
+ // }
998
+ await state.element.waitFor({ timeout: timeout });
999
+ found = true;
1000
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1001
+ }
1002
+ catch (e) {
1003
+ console.error("Error on waitForElement", e);
1004
+ // await _commandError(state, e, this);
1005
+ }
1006
+ finally {
1007
+ _commandFinally(state, this);
1008
+ }
1009
+ return found;
1010
+ }
1067
1011
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
1068
1012
  const state = {
1069
1013
  selectors,
@@ -1072,6 +1016,7 @@ class StableBrowser {
1072
1016
  world,
1073
1017
  type: checked ? Types.CHECK : Types.UNCHECK,
1074
1018
  text: checked ? `Check element` : `Uncheck element`,
1019
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
1075
1020
  operation: "setCheck",
1076
1021
  log: "***** check " + selectors.element_name + " *****\n",
1077
1022
  };
@@ -1081,9 +1026,15 @@ class StableBrowser {
1081
1026
  // let element = await this._locate(selectors, info, _params);
1082
1027
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1083
1028
  try {
1084
- // await this._highlightElements(element);
1029
+ // if (world && world.screenshot && !world.screenshotPath) {
1030
+ // console.log(`Highlighting while running from recorder`);
1031
+ await this._highlightElements(element);
1085
1032
  await state.element.setChecked(checked);
1086
1033
  await new Promise((resolve) => setTimeout(resolve, 1000));
1034
+ // await this._unHighlightElements(element);
1035
+ // }
1036
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1037
+ // await this._unHighlightElements(element);
1087
1038
  }
1088
1039
  catch (e) {
1089
1040
  if (e.message && e.message.includes("did not change its state")) {
@@ -1115,6 +1066,7 @@ class StableBrowser {
1115
1066
  world,
1116
1067
  type: Types.HOVER,
1117
1068
  text: `Hover element`,
1069
+ _text: `Hover on ${selectors.element_name}`,
1118
1070
  operation: "hover",
1119
1071
  log: "***** hover " + selectors.element_name + " *****\n",
1120
1072
  };
@@ -1122,6 +1074,7 @@ class StableBrowser {
1122
1074
  await _preCommand(state, this);
1123
1075
  try {
1124
1076
  await state.element.hover();
1077
+ // await _screenshot(state, this);
1125
1078
  await new Promise((resolve) => setTimeout(resolve, 1000));
1126
1079
  }
1127
1080
  catch (e) {
@@ -1129,6 +1082,7 @@ class StableBrowser {
1129
1082
  state.info.log += "hover failed, will try again" + "\n";
1130
1083
  state.element = await this._locate(selectors, state.info, _params);
1131
1084
  await state.element.hover({ timeout: 10000 });
1085
+ // await _screenshot(state, this);
1132
1086
  await new Promise((resolve) => setTimeout(resolve, 1000));
1133
1087
  }
1134
1088
  await _screenshot(state, this);
@@ -1154,6 +1108,7 @@ class StableBrowser {
1154
1108
  value: values.toString(),
1155
1109
  type: Types.SELECT,
1156
1110
  text: `Select option: ${values}`,
1111
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1157
1112
  operation: "selectOption",
1158
1113
  log: "***** select option " + selectors.element_name + " *****\n",
1159
1114
  };
@@ -1188,6 +1143,7 @@ class StableBrowser {
1188
1143
  highlight: false,
1189
1144
  type: Types.TYPE_PRESS,
1190
1145
  text: `Type value: ${_value}`,
1146
+ _text: `Type value: ${_value}`,
1191
1147
  operation: "type",
1192
1148
  log: "",
1193
1149
  };
@@ -1267,6 +1223,7 @@ class StableBrowser {
1267
1223
  world,
1268
1224
  type: Types.SET_DATE_TIME,
1269
1225
  text: `Set date time value: ${value}`,
1226
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1270
1227
  operation: "setDateTime",
1271
1228
  log: "***** set date time value " + selectors.element_name + " *****\n",
1272
1229
  throwError: false,
@@ -1338,6 +1295,7 @@ class StableBrowser {
1338
1295
  world,
1339
1296
  type: Types.FILL,
1340
1297
  text: `Click type input with value: ${_value}`,
1298
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1341
1299
  operation: "clickType",
1342
1300
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1343
1301
  };
@@ -1403,7 +1361,12 @@ class StableBrowser {
1403
1361
  await this.waitForPageLoad();
1404
1362
  }
1405
1363
  else if (enter === false) {
1406
- await state.element.dispatchEvent("change");
1364
+ try {
1365
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1366
+ }
1367
+ catch (e) {
1368
+ // ignore
1369
+ }
1407
1370
  //await this.page.keyboard.press("Tab");
1408
1371
  }
1409
1372
  else {
@@ -1455,6 +1418,7 @@ class StableBrowser {
1455
1418
  return await this._getText(selectors, 0, _params, options, info, world);
1456
1419
  }
1457
1420
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1421
+ const timeout = this._getFindElementTimeout(options);
1458
1422
  _validateSelectors(selectors);
1459
1423
  let screenshotId = null;
1460
1424
  let screenshotPath = null;
@@ -1464,7 +1428,7 @@ class StableBrowser {
1464
1428
  }
1465
1429
  info.operation = "getText";
1466
1430
  info.selectors = selectors;
1467
- let element = await this._locate(selectors, info, _params);
1431
+ let element = await this._locate(selectors, info, _params, timeout);
1468
1432
  if (climb > 0) {
1469
1433
  const climbArray = [];
1470
1434
  for (let i = 0; i < climb; i++) {
@@ -1483,6 +1447,18 @@ class StableBrowser {
1483
1447
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1484
1448
  try {
1485
1449
  await this._highlightElements(element);
1450
+ // if (world && world.screenshot && !world.screenshotPath) {
1451
+ // // console.log(`Highlighting for get text while running from recorder`);
1452
+ // this._highlightElements(element)
1453
+ // .then(async () => {
1454
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1455
+ // this._unhighlightElements(element).then(
1456
+ // () => {}
1457
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1458
+ // );
1459
+ // })
1460
+ // .catch(e);
1461
+ // }
1486
1462
  const elementText = await element.innerText();
1487
1463
  return {
1488
1464
  text: elementText,
@@ -1494,7 +1470,7 @@ class StableBrowser {
1494
1470
  }
1495
1471
  catch (e) {
1496
1472
  //await this.closeUnexpectedPopups();
1497
- this.logger.info("no innerText will use textContent");
1473
+ this.logger.info("no innerText, will use textContent");
1498
1474
  const elementText = await element.textContent();
1499
1475
  return { text: elementText, screenshotId, screenshotPath, value: value };
1500
1476
  }
@@ -1519,6 +1495,7 @@ class StableBrowser {
1519
1495
  highlight: false,
1520
1496
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1521
1497
  text: `Verify element contains pattern: ${pattern}`,
1498
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1522
1499
  operation: "containsPattern",
1523
1500
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1524
1501
  };
@@ -1554,6 +1531,8 @@ class StableBrowser {
1554
1531
  }
1555
1532
  }
1556
1533
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1534
+ const timeout = this._getFindElementTimeout(options);
1535
+ const startTime = Date.now();
1557
1536
  const state = {
1558
1537
  selectors,
1559
1538
  _params,
@@ -1580,62 +1559,54 @@ class StableBrowser {
1580
1559
  }
1581
1560
  let foundObj = null;
1582
1561
  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;
1562
+ while (Date.now() - startTime < timeout) {
1563
+ try {
1564
+ await _preCommand(state, this);
1565
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1566
+ if (foundObj && foundObj.element) {
1567
+ await this.scrollIfNeeded(foundObj.element, state.info);
1596
1568
  }
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])) {
1569
+ await _screenshot(state, this);
1570
+ const dateAlternatives = findDateAlternatives(text);
1571
+ const numberAlternatives = findNumberAlternatives(text);
1572
+ if (dateAlternatives.date) {
1573
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1574
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1575
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1576
+ return state.info;
1577
+ }
1578
+ }
1579
+ }
1580
+ else if (numberAlternatives.number) {
1581
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1582
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1583
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1584
+ return state.info;
1585
+ }
1586
+ }
1587
+ }
1588
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1604
1589
  return state.info;
1605
1590
  }
1606
1591
  }
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);
1592
+ catch (e) {
1593
+ // Log error but continue retrying until timeout is reached
1594
+ this.logger.warn("Retrying containsText due to: " + e.message);
1595
+ }
1596
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1613
1597
  }
1614
- return state.info;
1598
+ state.info.foundText = foundObj?.text;
1599
+ state.info.value = foundObj?.value;
1600
+ throw new Error("element doesn't contain text " + text);
1615
1601
  }
1616
1602
  catch (e) {
1617
1603
  await _commandError(state, e, this);
1604
+ throw e;
1618
1605
  }
1619
1606
  finally {
1620
1607
  _commandFinally(state, this);
1621
1608
  }
1622
1609
  }
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
1610
  async waitForUserInput(message, world = null) {
1640
1611
  if (!message) {
1641
1612
  message = "# Wait for user input. Press any key to continue";
@@ -1664,7 +1635,7 @@ class StableBrowser {
1664
1635
  return;
1665
1636
  }
1666
1637
  // if data file exists, load it
1667
- const dataFile = this._getDataFile(world);
1638
+ const dataFile = _getDataFile(world, this.context, this);
1668
1639
  let data = this.getTestData(world);
1669
1640
  // merge the testData with the existing data
1670
1641
  Object.assign(data, testData);
@@ -1767,7 +1738,7 @@ class StableBrowser {
1767
1738
  }
1768
1739
  }
1769
1740
  getTestData(world = null) {
1770
- const dataFile = this._getDataFile(world);
1741
+ const dataFile = _getDataFile(world, this.context, this);
1771
1742
  let data = {};
1772
1743
  if (fs.existsSync(dataFile)) {
1773
1744
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1854,6 +1825,15 @@ class StableBrowser {
1854
1825
  document.documentElement.clientWidth,
1855
1826
  ])));
1856
1827
  let screenshotBuffer = null;
1828
+ // if (focusedElement) {
1829
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1830
+ // await this._unhighlightElements(focusedElement);
1831
+ // await new Promise((resolve) => setTimeout(resolve, 100));
1832
+ // console.log(`Unhighlighted previous element`);
1833
+ // }
1834
+ // if (focusedElement) {
1835
+ // await this._highlightElements(focusedElement);
1836
+ // }
1857
1837
  if (this.context.browserName === "chromium") {
1858
1838
  const client = await playContext.newCDPSession(this.page);
1859
1839
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1875,6 +1855,10 @@ class StableBrowser {
1875
1855
  else {
1876
1856
  screenshotBuffer = await this.page.screenshot();
1877
1857
  }
1858
+ // if (focusedElement) {
1859
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1860
+ // await this._unhighlightElements(focusedElement);
1861
+ // }
1878
1862
  let image = await Jimp.read(screenshotBuffer);
1879
1863
  // Get the image dimensions
1880
1864
  const { width, height } = image.bitmap;
@@ -1887,6 +1871,7 @@ class StableBrowser {
1887
1871
  else {
1888
1872
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1889
1873
  }
1874
+ return screenshotBuffer;
1890
1875
  }
1891
1876
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1892
1877
  const state = {
@@ -1922,8 +1907,10 @@ class StableBrowser {
1922
1907
  world,
1923
1908
  type: Types.EXTRACT,
1924
1909
  text: `Extract attribute from element`,
1910
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1925
1911
  operation: "extractAttribute",
1926
1912
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1913
+ allowDisabled: true,
1927
1914
  };
1928
1915
  await new Promise((resolve) => setTimeout(resolve, 2000));
1929
1916
  try {
@@ -1945,6 +1932,7 @@ class StableBrowser {
1945
1932
  state.info.value = state.value;
1946
1933
  this.setTestData({ [variable]: state.value }, world);
1947
1934
  this.logger.info("set test data: " + variable + "=" + state.value);
1935
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1948
1936
  return state.info;
1949
1937
  }
1950
1938
  catch (e) {
@@ -1963,14 +1951,21 @@ class StableBrowser {
1963
1951
  options,
1964
1952
  world,
1965
1953
  type: Types.VERIFY_ATTRIBUTE,
1954
+ highlight: true,
1955
+ screenshot: true,
1966
1956
  text: `Verify element attribute`,
1957
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1967
1958
  operation: "verifyAttribute",
1968
1959
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1960
+ allowDisabled: true,
1969
1961
  };
1970
1962
  await new Promise((resolve) => setTimeout(resolve, 2000));
1971
1963
  let val;
1964
+ let expectedValue;
1972
1965
  try {
1973
1966
  await _preCommand(state, this);
1967
+ expectedValue = state.value;
1968
+ state.info.expectedValue = expectedValue;
1974
1969
  switch (attribute) {
1975
1970
  case "innerText":
1976
1971
  val = String(await state.element.innerText());
@@ -1984,23 +1979,30 @@ class StableBrowser {
1984
1979
  case "disabled":
1985
1980
  val = String(await state.element.isDisabled());
1986
1981
  break;
1982
+ case "readOnly":
1983
+ const isEditable = await state.element.isEditable();
1984
+ val = String(!isEditable);
1985
+ break;
1987
1986
  default:
1988
1987
  val = String(await state.element.getAttribute(attribute));
1989
1988
  break;
1990
1989
  }
1990
+ state.info.value = val;
1991
1991
  let regex;
1992
- if (value.startsWith("/") && value.endsWith("/")) {
1993
- const patternBody = value.slice(1, -1);
1992
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1993
+ const patternBody = expectedValue.slice(1, -1);
1994
1994
  regex = new RegExp(patternBody, "g");
1995
1995
  }
1996
1996
  else {
1997
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1997
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1998
1998
  regex = new RegExp(escapedPattern, "g");
1999
1999
  }
2000
2000
  if (!val.match(regex)) {
2001
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
2001
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2002
+ state.info.failCause.assertionFailed = true;
2003
+ state.info.failCause.lastError = errorMessage;
2004
+ throw new Error(errorMessage);
2002
2005
  }
2003
- state.info.value = val;
2004
2006
  return state.info;
2005
2007
  }
2006
2008
  catch (e) {
@@ -2028,7 +2030,7 @@ class StableBrowser {
2028
2030
  if (options && options.timeout) {
2029
2031
  timeout = options.timeout;
2030
2032
  }
2031
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2033
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2032
2034
  const request = {
2033
2035
  method: "POST",
2034
2036
  url: serviceUrl,
@@ -2099,27 +2101,32 @@ class StableBrowser {
2099
2101
  async _highlightElements(scope, css) {
2100
2102
  try {
2101
2103
  if (!scope) {
2104
+ // console.log(`Scope is not defined`);
2102
2105
  return;
2103
2106
  }
2104
2107
  if (!css) {
2105
2108
  scope
2106
2109
  .evaluate((node) => {
2107
2110
  if (node && node.style) {
2108
- let originalBorder = node.style.border;
2109
- node.style.border = "2px solid red";
2111
+ let originalOutline = node.style.outline;
2112
+ // console.log(`Original outline was: ${originalOutline}`);
2113
+ // node.__previousOutline = originalOutline;
2114
+ node.style.outline = "2px solid red";
2115
+ // console.log(`New outline is: ${node.style.outline}`);
2110
2116
  if (window) {
2111
2117
  window.addEventListener("beforeunload", function (e) {
2112
- node.style.border = originalBorder;
2118
+ node.style.outline = originalOutline;
2113
2119
  });
2114
2120
  }
2115
2121
  setTimeout(function () {
2116
- node.style.border = originalBorder;
2122
+ node.style.outline = originalOutline;
2117
2123
  }, 2000);
2118
2124
  }
2119
2125
  })
2120
2126
  .then(() => { })
2121
2127
  .catch((e) => {
2122
2128
  // ignore
2129
+ // console.error(`Could not highlight node : ${e}`);
2123
2130
  });
2124
2131
  }
2125
2132
  else {
@@ -2135,17 +2142,18 @@ class StableBrowser {
2135
2142
  if (!element.style) {
2136
2143
  return;
2137
2144
  }
2138
- var originalBorder = element.style.border;
2145
+ let originalOutline = element.style.outline;
2146
+ element.__previousOutline = originalOutline;
2139
2147
  // Set the new border to be red and 2px solid
2140
- element.style.border = "2px solid red";
2148
+ element.style.outline = "2px solid red";
2141
2149
  if (window) {
2142
2150
  window.addEventListener("beforeunload", function (e) {
2143
- element.style.border = originalBorder;
2151
+ element.style.outline = originalOutline;
2144
2152
  });
2145
2153
  }
2146
2154
  // Set a timeout to revert to the original border after 2 seconds
2147
2155
  setTimeout(function () {
2148
- element.style.border = originalBorder;
2156
+ element.style.outline = originalOutline;
2149
2157
  }, 2000);
2150
2158
  }
2151
2159
  return;
@@ -2153,6 +2161,7 @@ class StableBrowser {
2153
2161
  .then(() => { })
2154
2162
  .catch((e) => {
2155
2163
  // ignore
2164
+ // console.error(`Could not highlight css: ${e}`);
2156
2165
  });
2157
2166
  }
2158
2167
  }
@@ -2160,6 +2169,54 @@ class StableBrowser {
2160
2169
  console.debug(error);
2161
2170
  }
2162
2171
  }
2172
+ // async _unhighlightElements(scope, css) {
2173
+ // try {
2174
+ // if (!scope) {
2175
+ // return;
2176
+ // }
2177
+ // if (!css) {
2178
+ // scope
2179
+ // .evaluate((node) => {
2180
+ // if (node && node.style) {
2181
+ // if (!node.__previousOutline) {
2182
+ // node.style.outline = "";
2183
+ // } else {
2184
+ // node.style.outline = node.__previousOutline;
2185
+ // }
2186
+ // }
2187
+ // })
2188
+ // .then(() => {})
2189
+ // .catch((e) => {
2190
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2191
+ // });
2192
+ // } else {
2193
+ // scope
2194
+ // .evaluate(([css]) => {
2195
+ // if (!css) {
2196
+ // return;
2197
+ // }
2198
+ // let elements = Array.from(document.querySelectorAll(css));
2199
+ // for (i = 0; i < elements.length; i++) {
2200
+ // let element = elements[i];
2201
+ // if (!element.style) {
2202
+ // return;
2203
+ // }
2204
+ // if (!element.__previousOutline) {
2205
+ // element.style.outline = "";
2206
+ // } else {
2207
+ // element.style.outline = element.__previousOutline;
2208
+ // }
2209
+ // }
2210
+ // })
2211
+ // .then(() => {})
2212
+ // .catch((e) => {
2213
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2214
+ // });
2215
+ // }
2216
+ // } catch (error) {
2217
+ // // console.debug(error);
2218
+ // }
2219
+ // }
2163
2220
  async verifyPagePath(pathPart, options = {}, world = null) {
2164
2221
  const startTime = Date.now();
2165
2222
  let error = null;
@@ -2204,6 +2261,7 @@ class StableBrowser {
2204
2261
  _reportToWorld(world, {
2205
2262
  type: Types.VERIFY_PAGE_PATH,
2206
2263
  text: "Verify page path",
2264
+ _text: "Verify the page path contains " + pathPart,
2207
2265
  screenshotId,
2208
2266
  result: error
2209
2267
  ? {
@@ -2221,6 +2279,35 @@ class StableBrowser {
2221
2279
  });
2222
2280
  }
2223
2281
  }
2282
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2283
+ const frames = this.page.frames();
2284
+ let results = [];
2285
+ let ignoreCase = false;
2286
+ for (let i = 0; i < frames.length; i++) {
2287
+ if (dateAlternatives.date) {
2288
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
2289
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2290
+ result.frame = frames[i];
2291
+ results.push(result);
2292
+ }
2293
+ }
2294
+ else if (numberAlternatives.number) {
2295
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2296
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2297
+ result.frame = frames[i];
2298
+ results.push(result);
2299
+ }
2300
+ }
2301
+ else {
2302
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2303
+ result.frame = frames[i];
2304
+ results.push(result);
2305
+ }
2306
+ }
2307
+ state.info.results = results;
2308
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2309
+ return resultWithElementsFound;
2310
+ }
2224
2311
  async verifyTextExistInPage(text, options = {}, world = null) {
2225
2312
  text = unEscapeString(text);
2226
2313
  const state = {
@@ -2230,12 +2317,16 @@ class StableBrowser {
2230
2317
  locate: false,
2231
2318
  scroll: false,
2232
2319
  highlight: false,
2233
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2320
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2234
2321
  text: `Verify text exists in page`,
2322
+ _text: `Verify the text '${text}' exists in page`,
2235
2323
  operation: "verifyTextExistInPage",
2236
2324
  log: "***** verify text " + text + " exists in page *****\n",
2237
2325
  };
2238
- const timeout = this._getLoadTimeout(options);
2326
+ if (testForRegex(text)) {
2327
+ text = text.replace(/\\"/g, '"');
2328
+ }
2329
+ const timeout = this._getFindElementTimeout(options);
2239
2330
  await new Promise((resolve) => setTimeout(resolve, 2000));
2240
2331
  const newValue = await this._replaceWithLocalData(text, world);
2241
2332
  if (newValue !== text) {
@@ -2248,31 +2339,15 @@ class StableBrowser {
2248
2339
  await _preCommand(state, this);
2249
2340
  state.info.text = text;
2250
2341
  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
- }
2342
+ let resultWithElementsFound = {
2343
+ length: 0,
2344
+ };
2345
+ try {
2346
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2347
+ }
2348
+ catch (error) {
2349
+ // ignore
2273
2350
  }
2274
- state.info.results = results;
2275
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2276
2351
  if (resultWithElementsFound.length === 0) {
2277
2352
  if (Date.now() - state.startTime > timeout) {
2278
2353
  throw new Error(`Text ${text} not found in page`);
@@ -2280,18 +2355,40 @@ class StableBrowser {
2280
2355
  await new Promise((resolve) => setTimeout(resolve, 1000));
2281
2356
  continue;
2282
2357
  }
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");
2358
+ try {
2359
+ if (resultWithElementsFound[0].randomToken) {
2360
+ const frame = resultWithElementsFound[0].frame;
2361
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2362
+ await this._highlightElements(frame, dataAttribute);
2363
+ // if (world && world.screenshot && !world.screenshotPath) {
2364
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2365
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2366
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2367
+ // this._unhighlightElements(frame, dataAttribute)
2368
+ // .then(async () => {
2369
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2370
+ // })
2371
+ // .catch(
2372
+ // (e) => {}
2373
+ // console.error(e)
2374
+ // );
2375
+ // });
2376
+ // }
2377
+ const element = await frame.locator(dataAttribute).first();
2378
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2379
+ // await this._unhighlightElements(frame, dataAttribute);
2380
+ if (element) {
2381
+ await this.scrollIfNeeded(element, state.info);
2382
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2383
+ // await _screenshot(state, this, element);
2384
+ }
2291
2385
  }
2386
+ await _screenshot(state, this);
2387
+ return state.info;
2388
+ }
2389
+ catch (error) {
2390
+ console.error(error);
2292
2391
  }
2293
- await _screenshot(state, this);
2294
- return state.info;
2295
2392
  }
2296
2393
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2297
2394
  }
@@ -2313,10 +2410,14 @@ class StableBrowser {
2313
2410
  highlight: false,
2314
2411
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2315
2412
  text: `Verify text does not exist in page`,
2413
+ _text: `Verify the text '${text}' does not exist in page`,
2316
2414
  operation: "verifyTextNotExistInPage",
2317
2415
  log: "***** verify text " + text + " does not exist in page *****\n",
2318
2416
  };
2319
- const timeout = this._getLoadTimeout(options);
2417
+ if (testForRegex(text)) {
2418
+ text = text.replace(/\\"/g, '"');
2419
+ }
2420
+ const timeout = this._getFindElementTimeout(options);
2320
2421
  await new Promise((resolve) => setTimeout(resolve, 2000));
2321
2422
  const newValue = await this._replaceWithLocalData(text, world);
2322
2423
  if (newValue !== text) {
@@ -2328,32 +2429,16 @@ class StableBrowser {
2328
2429
  try {
2329
2430
  await _preCommand(state, this);
2330
2431
  state.info.text = text;
2432
+ let resultWithElementsFound = {
2433
+ length: null, // initial cannot be 0
2434
+ };
2331
2435
  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
- }
2436
+ try {
2437
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2438
+ }
2439
+ catch (error) {
2440
+ // ignore
2354
2441
  }
2355
- state.info.results = results;
2356
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2357
2442
  if (resultWithElementsFound.length === 0) {
2358
2443
  await _screenshot(state, this);
2359
2444
  return state.info;
@@ -2371,15 +2456,116 @@ class StableBrowser {
2371
2456
  _commandFinally(state, this);
2372
2457
  }
2373
2458
  }
2374
- _getServerUrl() {
2375
- let serviceUrl = "https://api.blinq.io";
2376
- if (process.env.NODE_ENV_BLINQ === "dev") {
2377
- serviceUrl = "https://dev.api.blinq.io";
2459
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
2460
+ textAnchor = unEscapeString(textAnchor);
2461
+ textToVerify = unEscapeString(textToVerify);
2462
+ const state = {
2463
+ text_search: textToVerify,
2464
+ options,
2465
+ world,
2466
+ locate: false,
2467
+ scroll: false,
2468
+ highlight: false,
2469
+ type: Types.VERIFY_TEXT_WITH_RELATION,
2470
+ text: `Verify text with relation to another text`,
2471
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2472
+ operation: "verify_text_with_relation",
2473
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2474
+ };
2475
+ const timeout = this._getFindElementTimeout(options);
2476
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2477
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
2478
+ if (newValue !== textAnchor) {
2479
+ this.logger.info(textAnchor + "=" + newValue);
2480
+ textAnchor = newValue;
2481
+ }
2482
+ newValue = await this._replaceWithLocalData(textToVerify, world);
2483
+ if (newValue !== textToVerify) {
2484
+ this.logger.info(textToVerify + "=" + newValue);
2485
+ textToVerify = newValue;
2486
+ }
2487
+ let dateAlternatives = findDateAlternatives(textToVerify);
2488
+ let numberAlternatives = findNumberAlternatives(textToVerify);
2489
+ let foundAncore = false;
2490
+ try {
2491
+ await _preCommand(state, this);
2492
+ state.info.text = textToVerify;
2493
+ let resultWithElementsFound = {
2494
+ length: 0,
2495
+ };
2496
+ while (true) {
2497
+ try {
2498
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2499
+ }
2500
+ catch (error) {
2501
+ // ignore
2502
+ }
2503
+ if (resultWithElementsFound.length === 0) {
2504
+ if (Date.now() - state.startTime > timeout) {
2505
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2506
+ }
2507
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2508
+ continue;
2509
+ }
2510
+ try {
2511
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2512
+ foundAncore = true;
2513
+ const result = resultWithElementsFound[i];
2514
+ const token = result.randomToken;
2515
+ const frame = result.frame;
2516
+ let css = `[data-blinq-id-${token}]`;
2517
+ const climbArray1 = [];
2518
+ for (let i = 0; i < climb; i++) {
2519
+ climbArray1.push("..");
2520
+ }
2521
+ let climbXpath = "xpath=" + climbArray1.join("/");
2522
+ css = css + " >> " + climbXpath;
2523
+ const count = await frame.locator(css).count();
2524
+ for (let j = 0; j < count; j++) {
2525
+ const continer = await frame.locator(css).nth(j);
2526
+ const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2527
+ if (result.elementCount > 0) {
2528
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2529
+ await this._highlightElements(frame, dataAttribute);
2530
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2531
+ // if (world && world.screenshot && !world.screenshotPath) {
2532
+ // console.log(`Highlighting for vtrt while running from recorder`);
2533
+ // this._highlightElements(frame, dataAttribute)
2534
+ // .then(async () => {
2535
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2536
+ // this._unhighlightElements(frame, dataAttribute).then(
2537
+ // () => {}
2538
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2539
+ // );
2540
+ // })
2541
+ // .catch(e);
2542
+ // }
2543
+ //await this._highlightElements(frame, cssAnchor);
2544
+ const element = await frame.locator(dataAttribute).first();
2545
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2546
+ // await this._unhighlightElements(frame, dataAttribute);
2547
+ if (element) {
2548
+ await this.scrollIfNeeded(element, state.info);
2549
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2550
+ }
2551
+ await _screenshot(state, this);
2552
+ return state.info;
2553
+ }
2554
+ }
2555
+ }
2556
+ }
2557
+ catch (error) {
2558
+ console.error(error);
2559
+ }
2560
+ }
2561
+ // await expect(element).toHaveCount(1, { timeout: 10000 });
2378
2562
  }
2379
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2380
- serviceUrl = "https://stage.api.blinq.io";
2563
+ catch (e) {
2564
+ await _commandError(state, e, this);
2565
+ }
2566
+ finally {
2567
+ _commandFinally(state, this);
2381
2568
  }
2382
- return serviceUrl;
2383
2569
  }
2384
2570
  async visualVerification(text, options = {}, world = null) {
2385
2571
  const startTime = Date.now();
@@ -2395,14 +2581,17 @@ class StableBrowser {
2395
2581
  throw new Error("TOKEN is not set");
2396
2582
  }
2397
2583
  try {
2398
- let serviceUrl = this._getServerUrl();
2584
+ let serviceUrl = _getServerUrl();
2399
2585
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2400
2586
  info.screenshotPath = screenshotPath;
2401
2587
  const screenshot = await this.takeScreenshot();
2402
- const request = {
2403
- method: "POST",
2588
+ let request = {
2589
+ method: "post",
2590
+ maxBodyLength: Infinity,
2404
2591
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2405
2592
  headers: {
2593
+ "x-bvt-project-id": path.basename(this.project_path),
2594
+ "x-source": "aaa",
2406
2595
  "Content-Type": "application/json",
2407
2596
  Authorization: `Bearer ${process.env.TOKEN}`,
2408
2597
  },
@@ -2411,7 +2600,7 @@ class StableBrowser {
2411
2600
  screenshot: screenshot,
2412
2601
  }),
2413
2602
  };
2414
- let result = await this.context.api.request(request);
2603
+ const result = await axios.request(request);
2415
2604
  if (result.data.status !== true) {
2416
2605
  throw new Error("Visual validation failed");
2417
2606
  }
@@ -2439,6 +2628,7 @@ class StableBrowser {
2439
2628
  _reportToWorld(world, {
2440
2629
  type: Types.VERIFY_VISUAL,
2441
2630
  text: "Visual verification",
2631
+ _text: "Visual verification of " + text,
2442
2632
  screenshotId,
2443
2633
  result: error
2444
2634
  ? {
@@ -2560,7 +2750,7 @@ class StableBrowser {
2560
2750
  info.operation = "analyzeTable";
2561
2751
  info.selectors = selectors;
2562
2752
  info.query = query;
2563
- query = this._fixUsingParams(query, _params);
2753
+ query = _fixUsingParams(query, _params);
2564
2754
  info.query_fixed = query;
2565
2755
  info.operator = operator;
2566
2756
  info.value = value;
@@ -2705,6 +2895,32 @@ class StableBrowser {
2705
2895
  }
2706
2896
  return timeout;
2707
2897
  }
2898
+ _getFindElementTimeout(options) {
2899
+ if (options && options.timeout) {
2900
+ return options.timeout;
2901
+ }
2902
+ if (this.configuration.find_element_timeout) {
2903
+ return this.configuration.find_element_timeout;
2904
+ }
2905
+ return 30000;
2906
+ }
2907
+ async saveStoreState(path = null, world = null) {
2908
+ const storageState = await this.page.context().storageState();
2909
+ //const testDataFile = _getDataFile(world, this.context, this);
2910
+ if (path) {
2911
+ // save { storageState: storageState } into the path
2912
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
2913
+ }
2914
+ else {
2915
+ await this.setTestData({ storageState: storageState }, world);
2916
+ }
2917
+ }
2918
+ async restoreSaveState(path = null, world = null) {
2919
+ await refreshBrowser(this, path, world);
2920
+ this.registerEventListeners(this.context);
2921
+ registerNetworkEvents(this.world, this, this.context, this.page);
2922
+ registerDownloadEvent(this.page, this.world, this.context);
2923
+ }
2708
2924
  async waitForPageLoad(options = {}, world = null) {
2709
2925
  let timeout = this._getLoadTimeout(options);
2710
2926
  const promiseArray = [];
@@ -2772,6 +2988,7 @@ class StableBrowser {
2772
2988
  highlight: false,
2773
2989
  type: Types.CLOSE_PAGE,
2774
2990
  text: `Close page`,
2991
+ _text: `Close the page`,
2775
2992
  operation: "closePage",
2776
2993
  log: "***** close page *****\n",
2777
2994
  throwError: false,
@@ -2788,8 +3005,97 @@ class StableBrowser {
2788
3005
  _commandFinally(state, this);
2789
3006
  }
2790
3007
  }
3008
+ async tableCellOperation(headerText, rowText, options, world = null) {
3009
+ let operation = null;
3010
+ if (!options || !options.operation) {
3011
+ throw new Error("operation is not defined");
3012
+ }
3013
+ operation = options.operation;
3014
+ // validate operation is one of the supported operations
3015
+ if (operation != "click" && operation != "hover+click") {
3016
+ throw new Error("operation is not supported");
3017
+ }
3018
+ const state = {
3019
+ options,
3020
+ world,
3021
+ locate: false,
3022
+ scroll: false,
3023
+ highlight: false,
3024
+ type: Types.TABLE_OPERATION,
3025
+ text: `Table operation`,
3026
+ _text: `Table ${operation} operation`,
3027
+ operation: operation,
3028
+ log: "***** Table operation *****\n",
3029
+ };
3030
+ const timeout = this._getFindElementTimeout(options);
3031
+ try {
3032
+ await _preCommand(state, this);
3033
+ const start = Date.now();
3034
+ let cellArea = null;
3035
+ while (true) {
3036
+ try {
3037
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3038
+ if (cellArea) {
3039
+ break;
3040
+ }
3041
+ }
3042
+ catch (e) {
3043
+ // ignore
3044
+ }
3045
+ if (Date.now() - start > timeout) {
3046
+ throw new Error(`Cell not found in table`);
3047
+ }
3048
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3049
+ }
3050
+ switch (operation) {
3051
+ case "click":
3052
+ if (!options.css) {
3053
+ // will click in the center of the cell
3054
+ let xOffset = 0;
3055
+ let yOffset = 0;
3056
+ if (options.xOffset) {
3057
+ xOffset = options.xOffset;
3058
+ }
3059
+ if (options.yOffset) {
3060
+ yOffset = options.yOffset;
3061
+ }
3062
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3063
+ }
3064
+ else {
3065
+ const results = await findElementsInArea(options.css, cellArea, this.page, this, options);
3066
+ if (results.length === 0) {
3067
+ throw new Error(`Element not found in cell area`);
3068
+ }
3069
+ state.element = results[0];
3070
+ await results[0].click();
3071
+ }
3072
+ break;
3073
+ case "hover+click":
3074
+ if (!options.css) {
3075
+ throw new Error("css is not defined");
3076
+ }
3077
+ const results = await findElementsInArea(options.css, cellArea, this.page, this, options);
3078
+ if (results.length === 0) {
3079
+ throw new Error(`Element not found in cell area`);
3080
+ }
3081
+ state.element = results[0];
3082
+ await results[0].hover();
3083
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3084
+ await results[0].click();
3085
+ break;
3086
+ default:
3087
+ throw new Error("operation is not supported");
3088
+ }
3089
+ }
3090
+ catch (e) {
3091
+ await _commandError(state, e, this);
3092
+ }
3093
+ finally {
3094
+ _commandFinally(state, this);
3095
+ }
3096
+ }
2791
3097
  saveTestDataAsGlobal(options, world) {
2792
- const dataFile = this._getDataFile(world);
3098
+ const dataFile = _getDataFile(world, this.context, this);
2793
3099
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2794
3100
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2795
3101
  }
@@ -2819,6 +3125,7 @@ class StableBrowser {
2819
3125
  _reportToWorld(world, {
2820
3126
  type: Types.SET_VIEWPORT,
2821
3127
  text: "set viewport size to " + width + "x" + hight,
3128
+ _text: "Set the viewport size to " + width + "x" + hight,
2822
3129
  screenshotId,
2823
3130
  result: error
2824
3131
  ? {
@@ -2890,14 +3197,25 @@ class StableBrowser {
2890
3197
  }
2891
3198
  }
2892
3199
  async beforeStep(world, step) {
2893
- this.stepName = step.pickleStep.text;
2894
- this.logger.info("step: " + this.stepName);
2895
3200
  if (this.stepIndex === undefined) {
2896
3201
  this.stepIndex = 0;
2897
3202
  }
2898
3203
  else {
2899
3204
  this.stepIndex++;
2900
3205
  }
3206
+ if (step && step.pickleStep && step.pickleStep.text) {
3207
+ this.stepName = step.pickleStep.text;
3208
+ this.logger.info("step: " + this.stepName);
3209
+ }
3210
+ else if (step && step.text) {
3211
+ this.stepName = step.text;
3212
+ }
3213
+ else {
3214
+ this.stepName = "step " + this.stepIndex;
3215
+ }
3216
+ if (this.context) {
3217
+ this.context.examplesRow = extractStepExampleParameters(step);
3218
+ }
2901
3219
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2902
3220
  if (this.context.browserObject.context) {
2903
3221
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2910,6 +3228,41 @@ class StableBrowser {
2910
3228
  this.saveTestDataAsGlobal({}, world);
2911
3229
  }
2912
3230
  }
3231
+ if (this.initSnapshotTaken === false) {
3232
+ this.initSnapshotTaken = true;
3233
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3234
+ const snapshot = await this.getAriaSnapshot();
3235
+ if (snapshot) {
3236
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3237
+ }
3238
+ }
3239
+ }
3240
+ }
3241
+ async getAriaSnapshot() {
3242
+ try {
3243
+ // find the page url
3244
+ const url = await this.page.url();
3245
+ // extract the path from the url
3246
+ const path = new URL(url).pathname;
3247
+ // get the page title
3248
+ const title = await this.page.title();
3249
+ // go over other frams
3250
+ const frames = this.page.frames();
3251
+ const snapshots = [];
3252
+ const content = [`- path: ${path}`, `- title: ${title}`];
3253
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3254
+ for (let i = 0; i < frames.length; i++) {
3255
+ content.push(`- frame: ${i}`);
3256
+ const frame = frames[i];
3257
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3258
+ content.push(snapshot);
3259
+ }
3260
+ return content.join("\n");
3261
+ }
3262
+ catch (e) {
3263
+ console.error(e);
3264
+ }
3265
+ return null;
2913
3266
  }
2914
3267
  async afterStep(world, step) {
2915
3268
  this.stepName = null;
@@ -2920,6 +3273,16 @@ class StableBrowser {
2920
3273
  });
2921
3274
  }
2922
3275
  }
3276
+ if (this.context) {
3277
+ this.context.examplesRow = null;
3278
+ }
3279
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3280
+ const snapshot = await this.getAriaSnapshot();
3281
+ if (snapshot) {
3282
+ const obj = {};
3283
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3284
+ }
3285
+ }
2923
3286
  }
2924
3287
  }
2925
3288
  function createTimedPromise(promise, label) {
@@ -2927,156 +3290,5 @@ function createTimedPromise(promise, label) {
2927
3290
  .then((result) => ({ status: "fulfilled", label, result }))
2928
3291
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2929
3292
  }
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
3293
  export { StableBrowser };
3082
3294
  //# sourceMappingURL=stable_browser.js.map