automation_model 1.0.525-dev → 1.0.525-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,15 +10,17 @@ 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, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, } from "./utils.js";
14
14
  import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
17
  import { getContext } from "./init_browser.js";
18
18
  import { locate_element } from "./locate_element.js";
19
+ import { randomUUID } from "crypto";
19
20
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
20
21
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
21
- const Types = {
22
+ import { LocatorLog } from "./locator_log.js";
23
+ export const Types = {
22
24
  CLICK: "click_element",
23
25
  NAVIGATE: "navigate",
24
26
  FILL: "fill_element",
@@ -29,6 +31,8 @@ const Types = {
29
31
  GET_PAGE_STATUS: "get_page_status",
30
32
  CLICK_ROW_ACTION: "click_row_action",
31
33
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
34
+ VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
35
+ VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
32
36
  ANALYZE_TABLE: "analyze_table",
33
37
  SELECT: "select_combobox",
34
38
  VERIFY_PAGE_PATH: "verify_page_path",
@@ -45,8 +49,13 @@ const Types = {
45
49
  LOAD_DATA: "load_data",
46
50
  SET_INPUT: "set_input",
47
51
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
52
+ VERIFY_ATTRIBUTE: "verify_element_attribute",
53
+ VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
48
54
  };
49
55
  export const apps = {};
56
+ const formatElementName = (elementName) => {
57
+ return elementName ? JSON.stringify(elementName) : "element";
58
+ };
50
59
  class StableBrowser {
51
60
  browser;
52
61
  page;
@@ -89,17 +98,17 @@ class StableBrowser {
89
98
  catch (e) {
90
99
  this.logger.error("unable to read ai_config.json");
91
100
  }
92
- context.pageLoading = { status: false };
93
- context.pages = [this.page];
94
101
  const logFolder = path.join(this.project_path, "logs", "web");
95
102
  this.world = world;
103
+ context.pages = [this.page];
104
+ context.pageLoading = { status: false };
96
105
  this.registerEventListeners(this.context);
97
106
  registerNetworkEvents(this.world, this, this.context, this.page);
98
107
  registerDownloadEvent(this.page, this.world, this.context);
99
108
  }
100
109
  registerEventListeners(context) {
101
110
  this.registerConsoleLogListener(this.page, context);
102
- this.registerRequestListener(this.page, context, this.webLogFile);
111
+ // this.registerRequestListener(this.page, context, this.webLogFile);
103
112
  if (!context.pageLoading) {
104
113
  context.pageLoading = { status: false };
105
114
  }
@@ -144,7 +153,7 @@ class StableBrowser {
144
153
  if (this.appName === appName) {
145
154
  return;
146
155
  }
147
- let newContextCreated = false;
156
+ let navigate = false;
148
157
  if (!apps[appName]) {
149
158
  let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
150
159
  newContextCreated = true;
@@ -155,32 +164,15 @@ class StableBrowser {
155
164
  };
156
165
  }
157
166
  const tempContext = {};
158
- this._copyContext(this, tempContext);
159
- this._copyContext(apps[appName], this);
167
+ _copyContext(this, tempContext);
168
+ _copyContext(apps[appName], this);
160
169
  apps[this.appName] = tempContext;
161
170
  this.appName = appName;
162
- if (newContextCreated) {
163
- this.registerEventListeners(this.context);
171
+ if (navigate) {
164
172
  await this.goto(this.context.environment.baseUrl);
165
173
  await this.waitForPageLoad();
166
174
  }
167
175
  }
168
- _copyContext(from, to) {
169
- to.browser = from.browser;
170
- to.page = from.page;
171
- to.context = from.context;
172
- }
173
- getWebLogFile(logFolder) {
174
- if (!fs.existsSync(logFolder)) {
175
- fs.mkdirSync(logFolder, { recursive: true });
176
- }
177
- let nextIndex = 1;
178
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
179
- nextIndex++;
180
- }
181
- const fileName = nextIndex + ".json";
182
- return path.join(logFolder, fileName);
183
- }
184
176
  registerConsoleLogListener(page, context) {
185
177
  if (!this.context.webLogger) {
186
178
  this.context.webLogger = [];
@@ -244,55 +236,48 @@ class StableBrowser {
244
236
  // async closeUnexpectedPopups() {
245
237
  // await closeUnexpectedPopups(this.page);
246
238
  // }
247
- async goto(url) {
239
+ async goto(url, world = null) {
248
240
  if (!url.startsWith("http")) {
249
241
  url = "https://" + url;
250
242
  }
251
- await this.page.goto(url, {
252
- timeout: 60000,
253
- });
254
- }
255
- _fixUsingParams(text, _params) {
256
- if (!_params || typeof text !== "string") {
257
- return text;
243
+ const state = {
244
+ value: url,
245
+ world: world,
246
+ type: Types.NAVIGATE,
247
+ text: `Navigate Page to: ${url}`,
248
+ operation: "goto",
249
+ log: "***** navigate page to " + url + " *****\n",
250
+ info: {},
251
+ locate: false,
252
+ scroll: false,
253
+ screenshot: false,
254
+ highlight: false,
255
+ };
256
+ try {
257
+ await _preCommand(state, this);
258
+ await this.page.goto(url, {
259
+ timeout: 60000,
260
+ });
261
+ await _screenshot(state, this);
258
262
  }
259
- for (let key in _params) {
260
- let regValue = key;
261
- if (key.startsWith("_")) {
262
- // remove the _ prefix
263
- regValue = key.substring(1);
264
- }
265
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
263
+ catch (error) {
264
+ console.error("Error on goto", error);
265
+ _commandError(state, error, this);
266
266
  }
267
- return text;
268
- }
269
- _fixLocatorUsingParams(locator, _params) {
270
- // check if not null
271
- if (!locator) {
272
- return locator;
267
+ finally {
268
+ _commandFinally(state, this);
273
269
  }
274
- // clone the locator
275
- locator = JSON.parse(JSON.stringify(locator));
276
- this.scanAndManipulate(locator, _params);
277
- return locator;
278
- }
279
- _isObject(value) {
280
- return value && typeof value === "object" && value.constructor === Object;
281
270
  }
282
- scanAndManipulate(currentObj, _params) {
283
- for (const key in currentObj) {
284
- if (typeof currentObj[key] === "string") {
285
- // Perform string manipulation
286
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
287
- }
288
- else if (this._isObject(currentObj[key])) {
289
- // Recursively scan nested objects
290
- this.scanAndManipulate(currentObj[key], _params);
271
+ async _getLocator(locator, scope, _params) {
272
+ locator = _fixLocatorUsingParams(locator, _params);
273
+ // locator = await this._replaceWithLocalData(locator);
274
+ for (let key in locator) {
275
+ if (typeof locator[key] !== "string")
276
+ continue;
277
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
278
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
291
279
  }
292
280
  }
293
- }
294
- _getLocator(locator, scope, _params) {
295
- locator = this._fixLocatorUsingParams(locator, _params);
296
281
  let locatorReturn;
297
282
  if (locator.role) {
298
283
  if (locator.role[1].nameReg) {
@@ -300,7 +285,7 @@ class StableBrowser {
300
285
  delete locator.role[1].nameReg;
301
286
  }
302
287
  // if (locator.role[1].name) {
303
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
288
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
304
289
  // }
305
290
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
306
291
  }
@@ -343,140 +328,54 @@ class StableBrowser {
343
328
  if (css && css.locator) {
344
329
  css = css.locator;
345
330
  }
346
- let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*:not(script, style, head)", false, false, _params);
331
+ let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
347
332
  if (result.elementCount === 0) {
348
333
  return;
349
334
  }
350
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
335
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
351
336
  // css climb to parent element
352
337
  const climbArray = [];
353
338
  for (let i = 0; i < climb; i++) {
354
339
  climbArray.push("..");
355
340
  }
356
341
  let climbXpath = "xpath=" + climbArray.join("/");
357
- return textElementCss + " >> " + climbXpath + " >> " + css;
358
- }
359
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
360
- //const stringifyText = JSON.stringify(text);
361
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
362
- function isParent(parent, child) {
363
- let currentNode = child.parentNode;
364
- while (currentNode !== null) {
365
- if (currentNode === parent) {
366
- return true;
342
+ let resultCss = textElementCss + " >> " + climbXpath;
343
+ if (css) {
344
+ resultCss = resultCss + " >> " + css;
345
+ }
346
+ return resultCss;
347
+ }
348
+ async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
349
+ const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
350
+ const locator = scope.locator(query);
351
+ const count = await locator.count();
352
+ if (!tag1) {
353
+ tag1 = "*";
354
+ }
355
+ const randomToken = Math.random().toString(36).substring(7);
356
+ let tagCount = 0;
357
+ for (let i = 0; i < count; i++) {
358
+ const element = locator.nth(i);
359
+ // check if the tag matches
360
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
361
+ if (!tag.startsWith("*")) {
362
+ if (el.tagName.toLowerCase() !== tag) {
363
+ return false;
367
364
  }
368
- currentNode = currentNode.parentNode;
369
- }
370
- return false;
371
- }
372
- document.isParent = isParent;
373
- function getRegex(str) {
374
- const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
375
- if (!match) {
376
- return null;
377
- }
378
- let [_, pattern, flags] = match;
379
- return new RegExp(pattern, flags);
380
- }
381
- document.getRegex = getRegex;
382
- function collectAllShadowDomElements(element, result = []) {
383
- // Check and add the element if it has a shadow root
384
- if (element.shadowRoot) {
385
- result.push(element);
386
- // Also search within the shadow root
387
- document.collectAllShadowDomElements(element.shadowRoot, result);
388
- }
389
- // Iterate over child nodes
390
- element.childNodes.forEach((child) => {
391
- // Recursively call the function for each child node
392
- document.collectAllShadowDomElements(child, result);
393
- });
394
- return result;
395
- }
396
- document.collectAllShadowDomElements = collectAllShadowDomElements;
397
- if (!tag) {
398
- tag = "*:not(script, style, head)";
399
- }
400
- let regexpSearch = document.getRegex(text);
401
- if (regexpSearch) {
402
- regex = true;
403
- }
404
- let elements = Array.from(document.querySelectorAll(tag));
405
- let shadowHosts = [];
406
- document.collectAllShadowDomElements(document, shadowHosts);
407
- for (let i = 0; i < shadowHosts.length; i++) {
408
- let shadowElement = shadowHosts[i].shadowRoot;
409
- if (!shadowElement) {
410
- console.log("shadowElement is null, for host " + shadowHosts[i]);
411
- continue;
412
365
  }
413
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
414
- elements = elements.concat(shadowElements);
415
- }
416
- let randomToken = null;
417
- const foundElements = [];
418
- if (regex) {
419
- if (!regexpSearch) {
420
- regexpSearch = new RegExp(text, "im");
421
- }
422
- for (let i = 0; i < elements.length; i++) {
423
- const element = elements[i];
424
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
425
- (element.value && regexpSearch.test(element.value))) {
426
- foundElements.push(element);
427
- }
366
+ if (!el.setAttribute) {
367
+ el = el.parentElement;
428
368
  }
369
+ el.setAttribute("data-blinq-id-" + randomToken, "");
370
+ return true;
371
+ }, [tag1, randomToken]))) {
372
+ continue;
429
373
  }
430
- else {
431
- text = text.trim();
432
- for (let i = 0; i < elements.length; i++) {
433
- const element = elements[i];
434
- if (partial) {
435
- if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
436
- (element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
437
- foundElements.push(element);
438
- }
439
- }
440
- else {
441
- if ((element.innerText && element.innerText.trim() === text) ||
442
- (element.value && element.value === text)) {
443
- foundElements.push(element);
444
- }
445
- }
446
- }
447
- }
448
- let noChildElements = [];
449
- for (let i = 0; i < foundElements.length; i++) {
450
- let element = foundElements[i];
451
- let hasChild = false;
452
- for (let j = 0; j < foundElements.length; j++) {
453
- if (i === j) {
454
- continue;
455
- }
456
- if (isParent(element, foundElements[j])) {
457
- hasChild = true;
458
- break;
459
- }
460
- }
461
- if (!hasChild) {
462
- noChildElements.push(element);
463
- }
464
- }
465
- let elementCount = 0;
466
- if (noChildElements.length > 0) {
467
- for (let i = 0; i < noChildElements.length; i++) {
468
- if (randomToken === null) {
469
- randomToken = Math.random().toString(36).substring(7);
470
- }
471
- let element = noChildElements[i];
472
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
473
- elementCount++;
474
- }
475
- }
476
- return { elementCount: elementCount, randomToken: randomToken };
477
- }, [text1, tag1, regex1, partial1]);
374
+ tagCount++;
375
+ }
376
+ return { elementCount: tagCount, randomToken };
478
377
  }
479
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
378
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
480
379
  if (!info) {
481
380
  info = {};
482
381
  }
@@ -485,10 +384,13 @@ class StableBrowser {
485
384
  }
486
385
  if (!info.log) {
487
386
  info.log = "";
387
+ info.locatorLog = new LocatorLog(selectorHierarchy);
488
388
  }
489
389
  let locatorSearch = selectorHierarchy[index];
390
+ let originalLocatorSearch = "";
490
391
  try {
491
- locatorSearch = JSON.parse(this._fixUsingParams(JSON.stringify(locatorSearch), _params));
392
+ originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
393
+ locatorSearch = JSON.parse(originalLocatorSearch);
492
394
  }
493
395
  catch (e) {
494
396
  console.error(e);
@@ -496,30 +398,31 @@ class StableBrowser {
496
398
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
497
399
  let locator = null;
498
400
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
499
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
401
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
402
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
500
403
  if (!locatorString) {
501
404
  info.failCause.textNotFound = true;
502
- info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
405
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
503
406
  return;
504
407
  }
505
- locator = this._getLocator({ css: locatorString }, scope, _params);
408
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
506
409
  }
507
410
  else if (locatorSearch.text) {
508
- let text = this._fixUsingParams(locatorSearch.text, _params);
509
- let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, _params);
411
+ let text = _fixUsingParams(locatorSearch.text, _params);
412
+ let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
510
413
  if (result.elementCount === 0) {
511
414
  info.failCause.textNotFound = true;
512
- info.failCause.lastError = "failed to locate element by text: " + text;
415
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
513
416
  return;
514
417
  }
515
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
418
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
516
419
  if (locatorSearch.childCss) {
517
420
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
518
421
  }
519
- locator = this._getLocator(locatorSearch, scope, _params);
422
+ locator = await this._getLocator(locatorSearch, scope, _params);
520
423
  }
521
424
  else {
522
- locator = this._getLocator(locatorSearch, scope, _params);
425
+ locator = await this._getLocator(locatorSearch, scope, _params);
523
426
  }
524
427
  // let cssHref = false;
525
428
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
@@ -532,18 +435,27 @@ class StableBrowser {
532
435
  //info.log += "total elements found " + count + "\n";
533
436
  //let visibleCount = 0;
534
437
  let visibleLocator = null;
535
- if (locatorSearch.index && locatorSearch.index < count) {
438
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
536
439
  foundLocators.push(locator.nth(locatorSearch.index));
440
+ if (info.locatorLog) {
441
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
442
+ }
537
443
  return;
538
444
  }
445
+ if (info.locatorLog && count === 0) {
446
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
447
+ }
539
448
  for (let j = 0; j < count; j++) {
540
449
  let visible = await locator.nth(j).isVisible();
541
450
  const enabled = await locator.nth(j).isEnabled();
542
451
  if (!visibleOnly) {
543
452
  visible = true;
544
453
  }
545
- if (visible && enabled) {
454
+ if (visible && (allowDisabled || enabled)) {
546
455
  foundLocators.push(locator.nth(j));
456
+ if (info.locatorLog) {
457
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
458
+ }
547
459
  }
548
460
  else {
549
461
  info.failCause.visible = visible;
@@ -551,8 +463,16 @@ class StableBrowser {
551
463
  if (!info.printMessages) {
552
464
  info.printMessages = {};
553
465
  }
466
+ if (info.locatorLog && !visible) {
467
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
468
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
469
+ }
470
+ if (info.locatorLog && !enabled) {
471
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
472
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
473
+ }
554
474
  if (!info.printMessages[j.toString()]) {
555
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
475
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
556
476
  info.printMessages[j.toString()] = true;
557
477
  }
558
478
  }
@@ -568,7 +488,7 @@ class StableBrowser {
568
488
  if (!info) {
569
489
  info = {};
570
490
  }
571
- info.log += "scan for popup handlers" + "\n";
491
+ //info.log += "scan for popup handlers" + "\n";
572
492
  const handlerGroup = [];
573
493
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
574
494
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
@@ -595,16 +515,28 @@ class StableBrowser {
595
515
  }
596
516
  if (result.foundElements.length > 0) {
597
517
  let dialogCloseLocator = result.foundElements[0].locator;
598
- await dialogCloseLocator.click();
599
- // wait for the dialog to close
600
- await dialogCloseLocator.waitFor({ state: "hidden" });
518
+ try {
519
+ await scope?.evaluate(() => {
520
+ window.__isClosingPopups = true;
521
+ });
522
+ await dialogCloseLocator.click();
523
+ // wait for the dialog to close
524
+ await dialogCloseLocator.waitFor({ state: "hidden" });
525
+ }
526
+ catch (e) {
527
+ }
528
+ finally {
529
+ await scope?.evaluate(() => {
530
+ window.__isClosingPopups = false;
531
+ });
532
+ }
601
533
  return { rerun: true };
602
534
  }
603
535
  }
604
536
  }
605
537
  return { rerun: false };
606
538
  }
607
- async _locate(selectors, info, _params, timeout) {
539
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
608
540
  if (!timeout) {
609
541
  timeout = 30000;
610
542
  }
@@ -614,9 +546,15 @@ class StableBrowser {
614
546
  let selector = selectors.locators[j];
615
547
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
616
548
  }
617
- let element = await this._locate_internal(selectors, info, _params, timeout);
549
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
618
550
  if (!element.rerun) {
619
- return element;
551
+ const randomToken = Math.random().toString(36).substring(7);
552
+ element.evaluate((el, randomToken) => {
553
+ el.setAttribute("data-blinq-id-" + randomToken, "");
554
+ }, randomToken);
555
+ const scope = element._frame ?? element.page();
556
+ const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
557
+ return newSelector;
620
558
  }
621
559
  }
622
560
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -627,6 +565,7 @@ class StableBrowser {
627
565
  info.failCause = {};
628
566
  info.log = "";
629
567
  }
568
+ let startTime = Date.now();
630
569
  let scope = this.page;
631
570
  if (selectors.frame) {
632
571
  return selectors.frame;
@@ -657,9 +596,11 @@ class StableBrowser {
657
596
  }
658
597
  return framescope;
659
598
  };
599
+ let fLocator = null;
660
600
  while (true) {
661
601
  let frameFound = false;
662
602
  if (selectors.nestFrmLoc) {
603
+ fLocator = selectors.nestFrmLoc;
663
604
  scope = await findFrame(selectors.nestFrmLoc, scope);
664
605
  frameFound = true;
665
606
  break;
@@ -668,6 +609,7 @@ class StableBrowser {
668
609
  for (let i = 0; i < selectors.frameLocators.length; i++) {
669
610
  let frameLocator = selectors.frameLocators[i];
670
611
  if (frameLocator.css) {
612
+ fLocator = frameLocator.css;
671
613
  scope = scope.frameLocator(frameLocator.css);
672
614
  frameFound = true;
673
615
  break;
@@ -675,18 +617,25 @@ class StableBrowser {
675
617
  }
676
618
  }
677
619
  if (!frameFound && selectors.iframe_src) {
620
+ fLocator = selectors.iframe_src;
678
621
  scope = this.page.frame({ url: selectors.iframe_src });
679
622
  }
680
623
  if (!scope) {
681
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
682
- if (performance.now() - startTime > timeout) {
624
+ if (info && info.locatorLog) {
625
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
626
+ }
627
+ //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
628
+ if (Date.now() - startTime > timeout) {
683
629
  info.failCause.iframeNotFound = true;
684
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
630
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
685
631
  throw new Error("unable to locate iframe " + selectors.iframe_src);
686
632
  }
687
633
  await new Promise((resolve) => setTimeout(resolve, 1000));
688
634
  }
689
635
  else {
636
+ if (info && info.locatorLog) {
637
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
638
+ }
690
639
  break;
691
640
  }
692
641
  }
@@ -703,16 +652,18 @@ class StableBrowser {
703
652
  return bodyContent;
704
653
  });
705
654
  }
706
- async _locate_internal(selectors, info, _params, timeout = 30000) {
655
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
707
656
  if (!info) {
708
657
  info = {};
709
658
  info.failCause = {};
710
659
  info.log = "";
660
+ info.locatorLog = new LocatorLog(selectors);
711
661
  }
712
662
  let highPriorityTimeout = 5000;
713
663
  let visibleOnlyTimeout = 6000;
714
- let startTime = performance.now();
664
+ let startTime = Date.now();
715
665
  let locatorsCount = 0;
666
+ let lazy_scroll = false;
716
667
  //let arrayMode = Array.isArray(selectors);
717
668
  let scope = await this._findFrameScope(selectors, timeout, info);
718
669
  let selectorsLocators = null;
@@ -750,17 +701,17 @@ class StableBrowser {
750
701
  }
751
702
  // info.log += "scanning locators in priority 1" + "\n";
752
703
  let onlyPriority3 = selectorsLocators[0].priority === 3;
753
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
704
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
754
705
  if (result.foundElements.length === 0) {
755
706
  // info.log += "scanning locators in priority 2" + "\n";
756
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
707
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
757
708
  }
758
709
  if (result.foundElements.length === 0 && onlyPriority3) {
759
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
710
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
760
711
  }
761
712
  else {
762
713
  if (result.foundElements.length === 0 && !highPriorityOnly) {
763
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
714
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
764
715
  }
765
716
  }
766
717
  let foundElements = result.foundElements;
@@ -801,26 +752,38 @@ class StableBrowser {
801
752
  return maxCountElement.locator;
802
753
  }
803
754
  }
804
- if (performance.now() - startTime > timeout) {
755
+ if (Date.now() - startTime > timeout) {
805
756
  break;
806
757
  }
807
- if (performance.now() - startTime > highPriorityTimeout) {
808
- info.log += "high priority timeout, will try all elements" + "\n";
758
+ if (Date.now() - startTime > highPriorityTimeout) {
759
+ //info.log += "high priority timeout, will try all elements" + "\n";
809
760
  highPriorityOnly = false;
761
+ if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
762
+ lazy_scroll = true;
763
+ await scrollPageToLoadLazyElements(this.page);
764
+ }
810
765
  }
811
- if (performance.now() - startTime > visibleOnlyTimeout) {
812
- info.log += "visible only timeout, will try all elements" + "\n";
766
+ if (Date.now() - startTime > visibleOnlyTimeout) {
767
+ //info.log += "visible only timeout, will try all elements" + "\n";
813
768
  visibleOnly = false;
814
769
  }
815
770
  await new Promise((resolve) => setTimeout(resolve, 1000));
816
771
  }
817
772
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
818
- info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
773
+ // if (info.locatorLog) {
774
+ // const lines = info.locatorLog.toString().split("\n");
775
+ // for (let line of lines) {
776
+ // this.logger.debug(line);
777
+ // }
778
+ // }
779
+ //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
819
780
  info.failCause.locatorNotFound = true;
820
- info.failCause.lastError = "failed to locate unique element";
781
+ if (!info?.failCause?.lastError) {
782
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
783
+ }
821
784
  throw new Error("failed to locate first element no elements found, " + info.log);
822
785
  }
823
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
786
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
824
787
  let foundElements = [];
825
788
  const result = {
826
789
  foundElements: foundElements,
@@ -828,14 +791,15 @@ class StableBrowser {
828
791
  for (let i = 0; i < locatorsGroup.length; i++) {
829
792
  let foundLocators = [];
830
793
  try {
831
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
794
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
832
795
  }
833
796
  catch (e) {
834
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
835
- this.logger.debug(e);
797
+ // this call can fail it the browser is navigating
798
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
799
+ // this.logger.debug(e);
836
800
  foundLocators = [];
837
801
  try {
838
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
802
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
839
803
  }
840
804
  catch (e) {
841
805
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -851,11 +815,27 @@ class StableBrowser {
851
815
  }
852
816
  if (foundLocators.length > 1) {
853
817
  info.failCause.foundMultiple = true;
818
+ if (info.locatorLog) {
819
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
820
+ }
854
821
  }
855
822
  }
856
823
  return result;
857
824
  }
858
825
  async simpleClick(elementDescription, _params, options = {}, world = null) {
826
+ const state = {
827
+ locate: false,
828
+ scroll: false,
829
+ highlight: false,
830
+ _params,
831
+ options,
832
+ world,
833
+ type: Types.CLICK,
834
+ text: "Click element",
835
+ operation: "simpleClick",
836
+ log: "***** click on " + elementDescription + " *****\n",
837
+ };
838
+ _preCommand(state, this);
859
839
  const startTime = Date.now();
860
840
  let timeout = 30000;
861
841
  if (options && options.timeout) {
@@ -880,13 +860,31 @@ class StableBrowser {
880
860
  catch (e) {
881
861
  if (performance.now() - startTime > timeout) {
882
862
  // throw e;
883
- await _commandError({ text: "simpleClick", operation: "simpleClick", elementDescription, info: {} }, e, this);
863
+ try {
864
+ await _commandError(state, "timeout looking for " + elementDescription, this);
865
+ }
866
+ finally {
867
+ _commandFinally(state, this);
868
+ }
884
869
  }
885
870
  }
886
871
  await new Promise((resolve) => setTimeout(resolve, 3000));
887
872
  }
888
873
  }
889
874
  async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
875
+ const state = {
876
+ locate: false,
877
+ scroll: false,
878
+ highlight: false,
879
+ _params,
880
+ options,
881
+ world,
882
+ type: Types.FILL,
883
+ text: "Fill element",
884
+ operation: "simpleClickType",
885
+ log: "***** click type on " + elementDescription + " *****\n",
886
+ };
887
+ _preCommand(state, this);
890
888
  const startTime = Date.now();
891
889
  let timeout = 30000;
892
890
  if (options && options.timeout) {
@@ -911,7 +909,12 @@ class StableBrowser {
911
909
  catch (e) {
912
910
  if (performance.now() - startTime > timeout) {
913
911
  // throw e;
914
- await _commandError({ text: "simpleClickType", operation: "simpleClickType", value, elementDescription, info: {} }, e, this);
912
+ try {
913
+ await _commandError(state, "timeout looking for " + elementDescription, this);
914
+ }
915
+ finally {
916
+ _commandFinally(state, this);
917
+ }
915
918
  }
916
919
  }
917
920
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -930,9 +933,9 @@ class StableBrowser {
930
933
  };
931
934
  try {
932
935
  await _preCommand(state, this);
933
- if (state.options && state.options.context) {
934
- state.selectors.locators[0].text = state.options.context;
935
- }
936
+ // if (state.options && state.options.context) {
937
+ // state.selectors.locators[0].text = state.options.context;
938
+ // }
936
939
  try {
937
940
  await state.element.click();
938
941
  // await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -970,9 +973,15 @@ class StableBrowser {
970
973
  // let element = await this._locate(selectors, info, _params);
971
974
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
972
975
  try {
973
- // await this._highlightElements(element);
976
+ // if (world && world.screenshot && !world.screenshotPath) {
977
+ // console.log(`Highlighting while running from recorder`);
978
+ await this._highlightElements(element);
974
979
  await state.element.setChecked(checked);
975
980
  await new Promise((resolve) => setTimeout(resolve, 1000));
981
+ // await this._unHighlightElements(element);
982
+ // }
983
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
984
+ // await this._unHighlightElements(element);
976
985
  }
977
986
  catch (e) {
978
987
  if (e.message && e.message.includes("did not change its state")) {
@@ -1011,6 +1020,7 @@ class StableBrowser {
1011
1020
  await _preCommand(state, this);
1012
1021
  try {
1013
1022
  await state.element.hover();
1023
+ // await _screenshot(state, this);
1014
1024
  await new Promise((resolve) => setTimeout(resolve, 1000));
1015
1025
  }
1016
1026
  catch (e) {
@@ -1018,6 +1028,7 @@ class StableBrowser {
1018
1028
  state.info.log += "hover failed, will try again" + "\n";
1019
1029
  state.element = await this._locate(selectors, state.info, _params);
1020
1030
  await state.element.hover({ timeout: 10000 });
1031
+ // await _screenshot(state, this);
1021
1032
  await new Promise((resolve) => setTimeout(resolve, 1000));
1022
1033
  }
1023
1034
  await _screenshot(state, this);
@@ -1292,7 +1303,12 @@ class StableBrowser {
1292
1303
  await this.waitForPageLoad();
1293
1304
  }
1294
1305
  else if (enter === false) {
1295
- await state.element.dispatchEvent("change");
1306
+ try {
1307
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1308
+ }
1309
+ catch (e) {
1310
+ // ignore
1311
+ }
1296
1312
  //await this.page.keyboard.press("Tab");
1297
1313
  }
1298
1314
  else {
@@ -1349,6 +1365,7 @@ class StableBrowser {
1349
1365
  let screenshotPath = null;
1350
1366
  if (!info.log) {
1351
1367
  info.log = "";
1368
+ info.locatorLog = new LocatorLog(selectors);
1352
1369
  }
1353
1370
  info.operation = "getText";
1354
1371
  info.selectors = selectors;
@@ -1371,6 +1388,18 @@ class StableBrowser {
1371
1388
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1372
1389
  try {
1373
1390
  await this._highlightElements(element);
1391
+ // if (world && world.screenshot && !world.screenshotPath) {
1392
+ // // console.log(`Highlighting for get text while running from recorder`);
1393
+ // this._highlightElements(element)
1394
+ // .then(async () => {
1395
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1396
+ // this._unhighlightElements(element).then(
1397
+ // () => {}
1398
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1399
+ // );
1400
+ // })
1401
+ // .catch(e);
1402
+ // }
1374
1403
  const elementText = await element.innerText();
1375
1404
  return {
1376
1405
  text: elementText,
@@ -1382,7 +1411,7 @@ class StableBrowser {
1382
1411
  }
1383
1412
  catch (e) {
1384
1413
  //await this.closeUnexpectedPopups();
1385
- this.logger.info("no innerText will use textContent");
1414
+ this.logger.info("no innerText, will use textContent");
1386
1415
  const elementText = await element.textContent();
1387
1416
  return { text: elementText, screenshotId, screenshotPath, value: value };
1388
1417
  }
@@ -1508,22 +1537,6 @@ class StableBrowser {
1508
1537
  _commandFinally(state, this);
1509
1538
  }
1510
1539
  }
1511
- _getDataFile(world = null) {
1512
- let dataFile = null;
1513
- if (world && world.reportFolder) {
1514
- dataFile = path.join(world.reportFolder, "data.json");
1515
- }
1516
- else if (this.reportFolder) {
1517
- dataFile = path.join(this.reportFolder, "data.json");
1518
- }
1519
- else if (this.context && this.context.reportFolder) {
1520
- dataFile = path.join(this.context.reportFolder, "data.json");
1521
- }
1522
- else {
1523
- dataFile = "data.json";
1524
- }
1525
- return dataFile;
1526
- }
1527
1540
  async waitForUserInput(message, world = null) {
1528
1541
  if (!message) {
1529
1542
  message = "# Wait for user input. Press any key to continue";
@@ -1552,7 +1565,7 @@ class StableBrowser {
1552
1565
  return;
1553
1566
  }
1554
1567
  // if data file exists, load it
1555
- const dataFile = this._getDataFile(world);
1568
+ const dataFile = _getDataFile(world, this.context, this);
1556
1569
  let data = this.getTestData(world);
1557
1570
  // merge the testData with the existing data
1558
1571
  Object.assign(data, testData);
@@ -1655,7 +1668,7 @@ class StableBrowser {
1655
1668
  }
1656
1669
  }
1657
1670
  getTestData(world = null) {
1658
- const dataFile = this._getDataFile(world);
1671
+ const dataFile = _getDataFile(world, this.context, this);
1659
1672
  let data = {};
1660
1673
  if (fs.existsSync(dataFile)) {
1661
1674
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1687,11 +1700,9 @@ class StableBrowser {
1687
1700
  if (!fs.existsSync(world.screenshotPath)) {
1688
1701
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1689
1702
  }
1690
- let nextIndex = 1;
1691
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1692
- nextIndex++;
1693
- }
1694
- const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
1703
+ // to make sure the path doesn't start with -
1704
+ const uuidStr = "id_" + randomUUID();
1705
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1695
1706
  try {
1696
1707
  await this.takeScreenshot(screenshotPath);
1697
1708
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1701,15 +1712,15 @@ class StableBrowser {
1701
1712
  // this.logger.info("unable to save screenshot " + screenshotPath);
1702
1713
  // }
1703
1714
  // });
1715
+ result.screenshotId = uuidStr;
1716
+ result.screenshotPath = screenshotPath;
1717
+ if (info && info.box) {
1718
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1719
+ }
1704
1720
  }
1705
1721
  catch (e) {
1706
1722
  this.logger.info("unable to take screenshot, ignored");
1707
1723
  }
1708
- result.screenshotId = nextIndex;
1709
- result.screenshotPath = screenshotPath;
1710
- if (info && info.box) {
1711
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1712
- }
1713
1724
  }
1714
1725
  else if (options && options.screenshot) {
1715
1726
  result.screenshotPath = options.screenshotPath;
@@ -1744,6 +1755,15 @@ class StableBrowser {
1744
1755
  document.documentElement.clientWidth,
1745
1756
  ])));
1746
1757
  let screenshotBuffer = null;
1758
+ // if (focusedElement) {
1759
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1760
+ // await this._unhighlightElements(focusedElement);
1761
+ // await new Promise((resolve) => setTimeout(resolve, 100));
1762
+ // console.log(`Unhighlighted previous element`);
1763
+ // }
1764
+ // if (focusedElement) {
1765
+ // await this._highlightElements(focusedElement);
1766
+ // }
1747
1767
  if (this.context.browserName === "chromium") {
1748
1768
  const client = await playContext.newCDPSession(this.page);
1749
1769
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1765,6 +1785,10 @@ class StableBrowser {
1765
1785
  else {
1766
1786
  screenshotBuffer = await this.page.screenshot();
1767
1787
  }
1788
+ // if (focusedElement) {
1789
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1790
+ // await this._unhighlightElements(focusedElement);
1791
+ // }
1768
1792
  let image = await Jimp.read(screenshotBuffer);
1769
1793
  // Get the image dimensions
1770
1794
  const { width, height } = image.bitmap;
@@ -1814,6 +1838,7 @@ class StableBrowser {
1814
1838
  text: `Extract attribute from element`,
1815
1839
  operation: "extractAttribute",
1816
1840
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1841
+ allowDisabled: true,
1817
1842
  };
1818
1843
  await new Promise((resolve) => setTimeout(resolve, 2000));
1819
1844
  try {
@@ -1835,6 +1860,70 @@ class StableBrowser {
1835
1860
  state.info.value = state.value;
1836
1861
  this.setTestData({ [variable]: state.value }, world);
1837
1862
  this.logger.info("set test data: " + variable + "=" + state.value);
1863
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1864
+ return state.info;
1865
+ }
1866
+ catch (e) {
1867
+ await _commandError(state, e, this);
1868
+ }
1869
+ finally {
1870
+ _commandFinally(state, this);
1871
+ }
1872
+ }
1873
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
1874
+ const state = {
1875
+ selectors,
1876
+ _params,
1877
+ attribute,
1878
+ value,
1879
+ options,
1880
+ world,
1881
+ type: Types.VERIFY_ATTRIBUTE,
1882
+ highlight: true,
1883
+ screenshot: true,
1884
+ text: `Verify element attribute`,
1885
+ operation: "verifyAttribute",
1886
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1887
+ allowDisabled: true,
1888
+ };
1889
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1890
+ let val;
1891
+ try {
1892
+ await _preCommand(state, this);
1893
+ switch (attribute) {
1894
+ case "innerText":
1895
+ val = String(await state.element.innerText());
1896
+ break;
1897
+ case "value":
1898
+ val = String(await state.element.inputValue());
1899
+ break;
1900
+ case "checked":
1901
+ val = String(await state.element.isChecked());
1902
+ break;
1903
+ case "disabled":
1904
+ val = String(await state.element.isDisabled());
1905
+ break;
1906
+ case "readOnly":
1907
+ const isEditable = await state.element.isEditable();
1908
+ val = String(!isEditable);
1909
+ break;
1910
+ default:
1911
+ val = String(await state.element.getAttribute(attribute));
1912
+ break;
1913
+ }
1914
+ state.info.expectedValue = val;
1915
+ let regex;
1916
+ if (value.startsWith("/") && value.endsWith("/")) {
1917
+ const patternBody = value.slice(1, -1);
1918
+ regex = new RegExp(patternBody, "g");
1919
+ }
1920
+ else {
1921
+ const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1922
+ regex = new RegExp(escapedPattern, "g");
1923
+ }
1924
+ if (!val.match(regex)) {
1925
+ throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
1926
+ }
1838
1927
  return state.info;
1839
1928
  }
1840
1929
  catch (e) {
@@ -1862,7 +1951,7 @@ class StableBrowser {
1862
1951
  if (options && options.timeout) {
1863
1952
  timeout = options.timeout;
1864
1953
  }
1865
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1954
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1866
1955
  const request = {
1867
1956
  method: "POST",
1868
1957
  url: serviceUrl,
@@ -1933,27 +2022,32 @@ class StableBrowser {
1933
2022
  async _highlightElements(scope, css) {
1934
2023
  try {
1935
2024
  if (!scope) {
2025
+ // console.log(`Scope is not defined`);
1936
2026
  return;
1937
2027
  }
1938
2028
  if (!css) {
1939
2029
  scope
1940
2030
  .evaluate((node) => {
1941
2031
  if (node && node.style) {
1942
- let originalBorder = node.style.border;
1943
- node.style.border = "2px solid red";
2032
+ let originalOutline = node.style.outline;
2033
+ // console.log(`Original outline was: ${originalOutline}`);
2034
+ // node.__previousOutline = originalOutline;
2035
+ node.style.outline = "2px solid red";
2036
+ // console.log(`New outline is: ${node.style.outline}`);
1944
2037
  if (window) {
1945
2038
  window.addEventListener("beforeunload", function (e) {
1946
- node.style.border = originalBorder;
2039
+ node.style.outline = originalOutline;
1947
2040
  });
1948
2041
  }
1949
2042
  setTimeout(function () {
1950
- node.style.border = originalBorder;
2043
+ node.style.outline = originalOutline;
1951
2044
  }, 2000);
1952
2045
  }
1953
2046
  })
1954
2047
  .then(() => { })
1955
2048
  .catch((e) => {
1956
2049
  // ignore
2050
+ // console.error(`Could not highlight node : ${e}`);
1957
2051
  });
1958
2052
  }
1959
2053
  else {
@@ -1969,17 +2063,18 @@ class StableBrowser {
1969
2063
  if (!element.style) {
1970
2064
  return;
1971
2065
  }
1972
- var originalBorder = element.style.border;
2066
+ let originalOutline = element.style.outline;
2067
+ element.__previousOutline = originalOutline;
1973
2068
  // Set the new border to be red and 2px solid
1974
- element.style.border = "2px solid red";
2069
+ element.style.outline = "2px solid red";
1975
2070
  if (window) {
1976
2071
  window.addEventListener("beforeunload", function (e) {
1977
- element.style.border = originalBorder;
2072
+ element.style.outline = originalOutline;
1978
2073
  });
1979
2074
  }
1980
2075
  // Set a timeout to revert to the original border after 2 seconds
1981
2076
  setTimeout(function () {
1982
- element.style.border = originalBorder;
2077
+ element.style.outline = originalOutline;
1983
2078
  }, 2000);
1984
2079
  }
1985
2080
  return;
@@ -1987,6 +2082,7 @@ class StableBrowser {
1987
2082
  .then(() => { })
1988
2083
  .catch((e) => {
1989
2084
  // ignore
2085
+ // console.error(`Could not highlight css: ${e}`);
1990
2086
  });
1991
2087
  }
1992
2088
  }
@@ -1994,6 +2090,54 @@ class StableBrowser {
1994
2090
  console.debug(error);
1995
2091
  }
1996
2092
  }
2093
+ // async _unhighlightElements(scope, css) {
2094
+ // try {
2095
+ // if (!scope) {
2096
+ // return;
2097
+ // }
2098
+ // if (!css) {
2099
+ // scope
2100
+ // .evaluate((node) => {
2101
+ // if (node && node.style) {
2102
+ // if (!node.__previousOutline) {
2103
+ // node.style.outline = "";
2104
+ // } else {
2105
+ // node.style.outline = node.__previousOutline;
2106
+ // }
2107
+ // }
2108
+ // })
2109
+ // .then(() => {})
2110
+ // .catch((e) => {
2111
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2112
+ // });
2113
+ // } else {
2114
+ // scope
2115
+ // .evaluate(([css]) => {
2116
+ // if (!css) {
2117
+ // return;
2118
+ // }
2119
+ // let elements = Array.from(document.querySelectorAll(css));
2120
+ // for (i = 0; i < elements.length; i++) {
2121
+ // let element = elements[i];
2122
+ // if (!element.style) {
2123
+ // return;
2124
+ // }
2125
+ // if (!element.__previousOutline) {
2126
+ // element.style.outline = "";
2127
+ // } else {
2128
+ // element.style.outline = element.__previousOutline;
2129
+ // }
2130
+ // }
2131
+ // })
2132
+ // .then(() => {})
2133
+ // .catch((e) => {
2134
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2135
+ // });
2136
+ // }
2137
+ // } catch (error) {
2138
+ // // console.debug(error);
2139
+ // }
2140
+ // }
1997
2141
  async verifyPagePath(pathPart, options = {}, world = null) {
1998
2142
  const startTime = Date.now();
1999
2143
  let error = null;
@@ -2055,6 +2199,35 @@ class StableBrowser {
2055
2199
  });
2056
2200
  }
2057
2201
  }
2202
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2203
+ const frames = this.page.frames();
2204
+ let results = [];
2205
+ let ignoreCase = false;
2206
+ for (let i = 0; i < frames.length; i++) {
2207
+ if (dateAlternatives.date) {
2208
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
2209
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2210
+ result.frame = frames[i];
2211
+ results.push(result);
2212
+ }
2213
+ }
2214
+ else if (numberAlternatives.number) {
2215
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2216
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2217
+ result.frame = frames[i];
2218
+ results.push(result);
2219
+ }
2220
+ }
2221
+ else {
2222
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2223
+ result.frame = frames[i];
2224
+ results.push(result);
2225
+ }
2226
+ }
2227
+ state.info.results = results;
2228
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2229
+ return resultWithElementsFound;
2230
+ }
2058
2231
  async verifyTextExistInPage(text, options = {}, world = null) {
2059
2232
  text = unEscapeString(text);
2060
2233
  const state = {
@@ -2064,7 +2237,7 @@ class StableBrowser {
2064
2237
  locate: false,
2065
2238
  scroll: false,
2066
2239
  highlight: false,
2067
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2240
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2068
2241
  text: `Verify text exists in page`,
2069
2242
  operation: "verifyTextExistInPage",
2070
2243
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2082,31 +2255,7 @@ class StableBrowser {
2082
2255
  await _preCommand(state, this);
2083
2256
  state.info.text = text;
2084
2257
  while (true) {
2085
- const frames = this.page.frames();
2086
- let results = [];
2087
- for (let i = 0; i < frames.length; i++) {
2088
- if (dateAlternatives.date) {
2089
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2090
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
2091
- result.frame = frames[i];
2092
- results.push(result);
2093
- }
2094
- }
2095
- else if (numberAlternatives.number) {
2096
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2097
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
2098
- result.frame = frames[i];
2099
- results.push(result);
2100
- }
2101
- }
2102
- else {
2103
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
2104
- result.frame = frames[i];
2105
- results.push(result);
2106
- }
2107
- }
2108
- state.info.results = results;
2109
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2258
+ const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2110
2259
  if (resultWithElementsFound.length === 0) {
2111
2260
  if (Date.now() - state.startTime > timeout) {
2112
2261
  throw new Error(`Text ${text} not found in page`);
@@ -2116,12 +2265,29 @@ class StableBrowser {
2116
2265
  }
2117
2266
  if (resultWithElementsFound[0].randomToken) {
2118
2267
  const frame = resultWithElementsFound[0].frame;
2119
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2268
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2120
2269
  await this._highlightElements(frame, dataAttribute);
2121
- const element = await frame.$(dataAttribute);
2270
+ // if (world && world.screenshot && !world.screenshotPath) {
2271
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2272
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2273
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2274
+ // this._unhighlightElements(frame, dataAttribute)
2275
+ // .then(async () => {
2276
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2277
+ // })
2278
+ // .catch(
2279
+ // (e) => {}
2280
+ // console.error(e)
2281
+ // );
2282
+ // });
2283
+ // }
2284
+ const element = await frame.locator(dataAttribute).first();
2285
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2286
+ // await this._unhighlightElements(frame, dataAttribute);
2122
2287
  if (element) {
2123
2288
  await this.scrollIfNeeded(element, state.info);
2124
2289
  await element.dispatchEvent("bvt_verify_page_contains_text");
2290
+ // await _screenshot(state, this, element);
2125
2291
  }
2126
2292
  }
2127
2293
  await _screenshot(state, this);
@@ -2163,31 +2329,7 @@ class StableBrowser {
2163
2329
  await _preCommand(state, this);
2164
2330
  state.info.text = text;
2165
2331
  while (true) {
2166
- const frames = this.page.frames();
2167
- let results = [];
2168
- for (let i = 0; i < frames.length; i++) {
2169
- if (dateAlternatives.date) {
2170
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2171
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
2172
- result.frame = frames[i];
2173
- results.push(result);
2174
- }
2175
- }
2176
- else if (numberAlternatives.number) {
2177
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2178
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
2179
- result.frame = frames[i];
2180
- results.push(result);
2181
- }
2182
- }
2183
- else {
2184
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
2185
- result.frame = frames[i];
2186
- results.push(result);
2187
- }
2188
- }
2189
- state.info.results = results;
2190
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2332
+ const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2191
2333
  if (resultWithElementsFound.length === 0) {
2192
2334
  await _screenshot(state, this);
2193
2335
  return state.info;
@@ -2205,15 +2347,102 @@ class StableBrowser {
2205
2347
  _commandFinally(state, this);
2206
2348
  }
2207
2349
  }
2208
- _getServerUrl() {
2209
- let serviceUrl = "https://api.blinq.io";
2210
- if (process.env.NODE_ENV_BLINQ === "dev") {
2211
- serviceUrl = "https://dev.api.blinq.io";
2350
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
2351
+ textAnchor = unEscapeString(textAnchor);
2352
+ textToVerify = unEscapeString(textToVerify);
2353
+ const state = {
2354
+ text_search: textToVerify,
2355
+ options,
2356
+ world,
2357
+ locate: false,
2358
+ scroll: false,
2359
+ highlight: false,
2360
+ type: Types.VERIFY_TEXT_WITH_RELATION,
2361
+ text: `Verify text with relation to another text`,
2362
+ operation: "verify_text_with_relation",
2363
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2364
+ };
2365
+ const timeout = this._getLoadTimeout(options);
2366
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2367
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
2368
+ if (newValue !== textAnchor) {
2369
+ this.logger.info(textAnchor + "=" + newValue);
2370
+ textAnchor = newValue;
2371
+ }
2372
+ newValue = await this._replaceWithLocalData(textToVerify, world);
2373
+ if (newValue !== textToVerify) {
2374
+ this.logger.info(textToVerify + "=" + newValue);
2375
+ textToVerify = newValue;
2376
+ }
2377
+ let dateAlternatives = findDateAlternatives(textToVerify);
2378
+ let numberAlternatives = findNumberAlternatives(textToVerify);
2379
+ let foundAncore = false;
2380
+ try {
2381
+ await _preCommand(state, this);
2382
+ state.info.text = textToVerify;
2383
+ while (true) {
2384
+ const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2385
+ if (resultWithElementsFound.length === 0) {
2386
+ if (Date.now() - state.startTime > timeout) {
2387
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2388
+ }
2389
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2390
+ continue;
2391
+ }
2392
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2393
+ foundAncore = true;
2394
+ const result = resultWithElementsFound[i];
2395
+ const token = result.randomToken;
2396
+ const frame = result.frame;
2397
+ let css = `[data-blinq-id-${token}]`;
2398
+ const climbArray1 = [];
2399
+ for (let i = 0; i < climb; i++) {
2400
+ climbArray1.push("..");
2401
+ }
2402
+ let climbXpath = "xpath=" + climbArray1.join("/");
2403
+ css = css + " >> " + climbXpath;
2404
+ const count = await frame.locator(css).count();
2405
+ for (let j = 0; j < count; j++) {
2406
+ const continer = await frame.locator(css).nth(j);
2407
+ const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2408
+ if (result.elementCount > 0) {
2409
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2410
+ await this._highlightElements(frame, dataAttribute);
2411
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2412
+ // if (world && world.screenshot && !world.screenshotPath) {
2413
+ // console.log(`Highlighting for vtrt while running from recorder`);
2414
+ // this._highlightElements(frame, dataAttribute)
2415
+ // .then(async () => {
2416
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2417
+ // this._unhighlightElements(frame, dataAttribute).then(
2418
+ // () => {}
2419
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2420
+ // );
2421
+ // })
2422
+ // .catch(e);
2423
+ // }
2424
+ //await this._highlightElements(frame, cssAnchor);
2425
+ const element = await frame.locator(dataAttribute).first();
2426
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2427
+ // await this._unhighlightElements(frame, dataAttribute);
2428
+ if (element) {
2429
+ await this.scrollIfNeeded(element, state.info);
2430
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2431
+ }
2432
+ await _screenshot(state, this);
2433
+ return state.info;
2434
+ }
2435
+ }
2436
+ }
2437
+ }
2438
+ // await expect(element).toHaveCount(1, { timeout: 10000 });
2439
+ }
2440
+ catch (e) {
2441
+ await _commandError(state, e, this);
2212
2442
  }
2213
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2214
- serviceUrl = "https://stage.api.blinq.io";
2443
+ finally {
2444
+ _commandFinally(state, this);
2215
2445
  }
2216
- return serviceUrl;
2217
2446
  }
2218
2447
  async visualVerification(text, options = {}, world = null) {
2219
2448
  const startTime = Date.now();
@@ -2229,7 +2458,7 @@ class StableBrowser {
2229
2458
  throw new Error("TOKEN is not set");
2230
2459
  }
2231
2460
  try {
2232
- let serviceUrl = this._getServerUrl();
2461
+ let serviceUrl = _getServerUrl();
2233
2462
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2234
2463
  info.screenshotPath = screenshotPath;
2235
2464
  const screenshot = await this.takeScreenshot();
@@ -2318,6 +2547,7 @@ class StableBrowser {
2318
2547
  let screenshotPath = null;
2319
2548
  const info = {};
2320
2549
  info.log = "";
2550
+ info.locatorLog = new LocatorLog(selectors);
2321
2551
  info.operation = "getTableData";
2322
2552
  info.selectors = selectors;
2323
2553
  try {
@@ -2393,7 +2623,7 @@ class StableBrowser {
2393
2623
  info.operation = "analyzeTable";
2394
2624
  info.selectors = selectors;
2395
2625
  info.query = query;
2396
- query = this._fixUsingParams(query, _params);
2626
+ query = _fixUsingParams(query, _params);
2397
2627
  info.query_fixed = query;
2398
2628
  info.operator = operator;
2399
2629
  info.value = value;
@@ -2538,6 +2768,17 @@ class StableBrowser {
2538
2768
  }
2539
2769
  return timeout;
2540
2770
  }
2771
+ async saveStoreState(path = null, world = null) {
2772
+ const storageState = await this.page.context().storageState();
2773
+ //const testDataFile = _getDataFile(world, this.context, this);
2774
+ if (path) {
2775
+ // save { storageState: storageState } into the path
2776
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
2777
+ }
2778
+ else {
2779
+ await this.setTestData({ storageState: storageState }, world);
2780
+ }
2781
+ }
2541
2782
  async waitForPageLoad(options = {}, world = null) {
2542
2783
  let timeout = this._getLoadTimeout(options);
2543
2784
  const promiseArray = [];
@@ -2723,14 +2964,22 @@ class StableBrowser {
2723
2964
  }
2724
2965
  }
2725
2966
  async beforeStep(world, step) {
2726
- this.stepName = step.pickleStep.text;
2727
- this.logger.info("step: " + this.stepName);
2728
2967
  if (this.stepIndex === undefined) {
2729
2968
  this.stepIndex = 0;
2730
2969
  }
2731
2970
  else {
2732
2971
  this.stepIndex++;
2733
2972
  }
2973
+ if (step && step.pickleStep && step.pickleStep.text) {
2974
+ this.stepName = step.pickleStep.text;
2975
+ this.logger.info("step: " + this.stepName);
2976
+ }
2977
+ else if (step && step.text) {
2978
+ this.stepName = step.text;
2979
+ }
2980
+ else {
2981
+ this.stepName = "step " + this.stepIndex;
2982
+ }
2734
2983
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2735
2984
  if (this.context.browserObject.context) {
2736
2985
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2760,156 +3009,5 @@ function createTimedPromise(promise, label) {
2760
3009
  .then((result) => ({ status: "fulfilled", label, result }))
2761
3010
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2762
3011
  }
2763
- const KEYBOARD_EVENTS = [
2764
- "ALT",
2765
- "AltGraph",
2766
- "CapsLock",
2767
- "Control",
2768
- "Fn",
2769
- "FnLock",
2770
- "Hyper",
2771
- "Meta",
2772
- "NumLock",
2773
- "ScrollLock",
2774
- "Shift",
2775
- "Super",
2776
- "Symbol",
2777
- "SymbolLock",
2778
- "Enter",
2779
- "Tab",
2780
- "ArrowDown",
2781
- "ArrowLeft",
2782
- "ArrowRight",
2783
- "ArrowUp",
2784
- "End",
2785
- "Home",
2786
- "PageDown",
2787
- "PageUp",
2788
- "Backspace",
2789
- "Clear",
2790
- "Copy",
2791
- "CrSel",
2792
- "Cut",
2793
- "Delete",
2794
- "EraseEof",
2795
- "ExSel",
2796
- "Insert",
2797
- "Paste",
2798
- "Redo",
2799
- "Undo",
2800
- "Accept",
2801
- "Again",
2802
- "Attn",
2803
- "Cancel",
2804
- "ContextMenu",
2805
- "Escape",
2806
- "Execute",
2807
- "Find",
2808
- "Finish",
2809
- "Help",
2810
- "Pause",
2811
- "Play",
2812
- "Props",
2813
- "Select",
2814
- "ZoomIn",
2815
- "ZoomOut",
2816
- "BrightnessDown",
2817
- "BrightnessUp",
2818
- "Eject",
2819
- "LogOff",
2820
- "Power",
2821
- "PowerOff",
2822
- "PrintScreen",
2823
- "Hibernate",
2824
- "Standby",
2825
- "WakeUp",
2826
- "AllCandidates",
2827
- "Alphanumeric",
2828
- "CodeInput",
2829
- "Compose",
2830
- "Convert",
2831
- "Dead",
2832
- "FinalMode",
2833
- "GroupFirst",
2834
- "GroupLast",
2835
- "GroupNext",
2836
- "GroupPrevious",
2837
- "ModeChange",
2838
- "NextCandidate",
2839
- "NonConvert",
2840
- "PreviousCandidate",
2841
- "Process",
2842
- "SingleCandidate",
2843
- "HangulMode",
2844
- "HanjaMode",
2845
- "JunjaMode",
2846
- "Eisu",
2847
- "Hankaku",
2848
- "Hiragana",
2849
- "HiraganaKatakana",
2850
- "KanaMode",
2851
- "KanjiMode",
2852
- "Katakana",
2853
- "Romaji",
2854
- "Zenkaku",
2855
- "ZenkakuHanaku",
2856
- "F1",
2857
- "F2",
2858
- "F3",
2859
- "F4",
2860
- "F5",
2861
- "F6",
2862
- "F7",
2863
- "F8",
2864
- "F9",
2865
- "F10",
2866
- "F11",
2867
- "F12",
2868
- "Soft1",
2869
- "Soft2",
2870
- "Soft3",
2871
- "Soft4",
2872
- "ChannelDown",
2873
- "ChannelUp",
2874
- "Close",
2875
- "MailForward",
2876
- "MailReply",
2877
- "MailSend",
2878
- "MediaFastForward",
2879
- "MediaPause",
2880
- "MediaPlay",
2881
- "MediaPlayPause",
2882
- "MediaRecord",
2883
- "MediaRewind",
2884
- "MediaStop",
2885
- "MediaTrackNext",
2886
- "MediaTrackPrevious",
2887
- "AudioBalanceLeft",
2888
- "AudioBalanceRight",
2889
- "AudioBassBoostDown",
2890
- "AudioBassBoostToggle",
2891
- "AudioBassBoostUp",
2892
- "AudioFaderFront",
2893
- "AudioFaderRear",
2894
- "AudioSurroundModeNext",
2895
- "AudioTrebleDown",
2896
- "AudioTrebleUp",
2897
- "AudioVolumeDown",
2898
- "AudioVolumeMute",
2899
- "AudioVolumeUp",
2900
- "MicrophoneToggle",
2901
- "MicrophoneVolumeDown",
2902
- "MicrophoneVolumeMute",
2903
- "MicrophoneVolumeUp",
2904
- "TV",
2905
- "TV3DMode",
2906
- "TVAntennaCable",
2907
- "TVAudioDescription",
2908
- ];
2909
- function unEscapeString(str) {
2910
- const placeholder = "__NEWLINE__";
2911
- str = str.replace(new RegExp(placeholder, "g"), "\n");
2912
- return str;
2913
- }
2914
3012
  export { StableBrowser };
2915
3013
  //# sourceMappingURL=stable_browser.js.map