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