automation_model 1.0.365-dev.0 → 1.0.365-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.
@@ -12,6 +12,8 @@ import drawRectangle from "./drawRect.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
13
  import objectPath from "object-path";
14
14
  import { decrypt } from "./utils.js";
15
+ import csv from "csv-parser";
16
+ import { Readable } from "node:stream";
15
17
  const Types = {
16
18
  CLICK: "click_element",
17
19
  NAVIGATE: "navigate",
@@ -27,6 +29,7 @@ const Types = {
27
29
  SELECT: "select_combobox",
28
30
  VERIFY_PAGE_PATH: "verify_page_path",
29
31
  TYPE_PRESS: "type_press",
32
+ PRESS: "press_key",
30
33
  HOVER: "hover_element",
31
34
  CHECK: "check_element",
32
35
  UNCHECK: "uncheck_element",
@@ -35,6 +38,7 @@ const Types = {
35
38
  SET_DATE_TIME: "set_date_time",
36
39
  SET_VIEWPORT: "set_viewport",
37
40
  VERIFY_VISUAL: "verify_visual",
41
+ LOAD_DATA: "load_data",
38
42
  };
39
43
  class StableBrowser {
40
44
  constructor(browser, page, logger = null, context = null) {
@@ -80,9 +84,15 @@ class StableBrowser {
80
84
  this.page = page;
81
85
  context.page = page;
82
86
  context.pages.push(page);
83
- this.webLogFile = this.getWebLogFile(logFolder);
84
- this.registerConsoleLogListener(page, context, this.webLogFile);
85
- this.registerRequestListener();
87
+ page.on("close", async () => {
88
+ if (this.context && this.context.pages && this.context.pages.length > 0) {
89
+ this.context.pages.pop();
90
+ this.page = this.context.pages[this.context.pages.length - 1];
91
+ this.context.page = this.page;
92
+ let title = await this.page.title();
93
+ console.log("Switched to page " + title);
94
+ }
95
+ });
86
96
  try {
87
97
  await this.waitForPageLoad();
88
98
  console.log("Switch page: " + (await page.title()));
@@ -91,7 +101,7 @@ class StableBrowser {
91
101
  this.logger.error("error on page load " + e);
92
102
  }
93
103
  context.pageLoading.status = false;
94
- });
104
+ }.bind(this));
95
105
  }
96
106
  getWebLogFile(logFolder) {
97
107
  if (!fs.existsSync(logFolder)) {
@@ -168,23 +178,65 @@ class StableBrowser {
168
178
  return text;
169
179
  }
170
180
  for (let key in _params) {
171
- text = text.replaceAll(new RegExp("{" + key + "}", "g"), _params[key]);
181
+ let regValue = key;
182
+ if (key.startsWith("_")) {
183
+ // remove the _ prefix
184
+ regValue = key.substring(1);
185
+ }
186
+ text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
172
187
  }
173
188
  return text;
174
189
  }
190
+ _fixLocatorUsingParams(locator, _params) {
191
+ // check if not null
192
+ if (!locator) {
193
+ return locator;
194
+ }
195
+ // clone the locator
196
+ locator = JSON.parse(JSON.stringify(locator));
197
+ this.scanAndManipulate(locator, _params);
198
+ return locator;
199
+ }
200
+ _isObject(value) {
201
+ return value && typeof value === "object" && value.constructor === Object;
202
+ }
203
+ scanAndManipulate(currentObj, _params) {
204
+ for (const key in currentObj) {
205
+ if (typeof currentObj[key] === "string") {
206
+ // Perform string manipulation
207
+ currentObj[key] = this._fixUsingParams(currentObj[key], _params);
208
+ }
209
+ else if (this._isObject(currentObj[key])) {
210
+ // Recursively scan nested objects
211
+ this.scanAndManipulate(currentObj[key], _params);
212
+ }
213
+ }
214
+ }
175
215
  _getLocator(locator, scope, _params) {
216
+ locator = this._fixLocatorUsingParams(locator, _params);
217
+ if (locator.type === "pw_selector") {
218
+ return scope.locator(locator.selector);
219
+ }
176
220
  if (locator.role) {
177
221
  if (locator.role[1].nameReg) {
178
222
  locator.role[1].name = reg_parser(locator.role[1].nameReg);
179
223
  delete locator.role[1].nameReg;
180
224
  }
181
- if (locator.role[1].name) {
182
- locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
183
- }
225
+ // if (locator.role[1].name) {
226
+ // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
227
+ // }
184
228
  return scope.getByRole(locator.role[0], locator.role[1]);
185
229
  }
186
230
  if (locator.css) {
187
- return scope.locator(this._fixUsingParams(locator.css, _params));
231
+ return scope.locator(locator.css);
232
+ }
233
+ if ((locator === null || locator === void 0 ? void 0 : locator.engine) && (locator === null || locator === void 0 ? void 0 : locator.score) <= 520) {
234
+ let selector = locator.selector.replace(/"/g, '\\"');
235
+ if (locator.engine === "internal:att") {
236
+ selector = `[${selector}]`;
237
+ }
238
+ const locator = scope.locator(`${locator.engine}="${selector}"`);
239
+ return locator;
188
240
  }
189
241
  throw new Error("unknown locator type");
190
242
  }
@@ -309,10 +361,13 @@ class StableBrowser {
309
361
  }
310
362
  async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
311
363
  let locatorSearch = selectorHierarchy[index];
312
- info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
364
+ //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
313
365
  let locator = null;
314
366
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
315
367
  let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
368
+ if (!locatorString) {
369
+ return;
370
+ }
316
371
  locator = this._getLocator({ css: locatorString }, scope, _params);
317
372
  }
318
373
  else if (locatorSearch.text) {
@@ -334,7 +389,7 @@ class StableBrowser {
334
389
  // cssHref = true;
335
390
  // }
336
391
  let count = await locator.count();
337
- info.log += "total elements found " + count + "\n";
392
+ //info.log += "total elements found " + count + "\n";
338
393
  //let visibleCount = 0;
339
394
  let visibleLocator = null;
340
395
  if (locatorSearch.index && locatorSearch.index < count) {
@@ -344,16 +399,20 @@ class StableBrowser {
344
399
  for (let j = 0; j < count; j++) {
345
400
  let visible = await locator.nth(j).isVisible();
346
401
  const enabled = await locator.nth(j).isEnabled();
347
- info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
348
402
  if (!visibleOnly) {
349
403
  visible = true;
350
404
  }
351
405
  if (visible && enabled) {
352
406
  foundLocators.push(locator.nth(j));
353
- // if (cssHref) {
354
- // info.log += "css href locator found, will ignore all others" + "\n";
355
- // break;
356
- // }
407
+ }
408
+ else {
409
+ if (!info.printMessages) {
410
+ info.printMessages = {};
411
+ }
412
+ if (!info.printMessages[j.toString()]) {
413
+ info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
414
+ info.printMessages[j.toString()] = true;
415
+ }
357
416
  }
358
417
  }
359
418
  }
@@ -379,15 +438,30 @@ class StableBrowser {
379
438
  }
380
439
  if (result.foundElements.length > 0) {
381
440
  // need to handle popup
382
- let dialogCloseLocator = this._getLocator(this.configuration.popupHandlers[result.locatorIndex].close_dialog_locator, scope, _params);
383
- await dialogCloseLocator.click();
384
- return { rerun: true };
441
+ const closeHandlerGroup = [];
442
+ closeHandlerGroup.push(this.configuration.popupHandlers[result.locatorIndex].close_dialog_locator);
443
+ for (let i = 0; i < scopes.length; i++) {
444
+ result = await this._scanLocatorsGroup(closeHandlerGroup, scopes[i], _params, info, true);
445
+ if (result.foundElements.length > 0) {
446
+ break;
447
+ }
448
+ }
449
+ if (result.foundElements.length > 0) {
450
+ let dialogCloseLocator = result.foundElements[0].locator;
451
+ await dialogCloseLocator.click();
452
+ return { rerun: true };
453
+ }
385
454
  }
386
455
  }
387
456
  return { rerun: false };
388
457
  }
389
458
  async _locate(selectors, info, _params, timeout = 30000) {
390
459
  for (let i = 0; i < 3; i++) {
460
+ info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
461
+ for (let j = 0; j < selectors.locators.length; j++) {
462
+ let selector = selectors.locators[j];
463
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
464
+ }
391
465
  let element = await this._locate_internal(selectors, info, _params, timeout);
392
466
  if (!element.rerun) {
393
467
  return element;
@@ -403,6 +477,7 @@ class StableBrowser {
403
477
  //let arrayMode = Array.isArray(selectors);
404
478
  let scope = this.page;
405
479
  if (selectors.iframe_src || selectors.frameLocators) {
480
+ info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
406
481
  while (true) {
407
482
  let frameFound = false;
408
483
  if (selectors.frameLocators) {
@@ -463,19 +538,25 @@ class StableBrowser {
463
538
  if (popupResult.rerun) {
464
539
  return popupResult;
465
540
  }
466
- info.log += "scanning locators in priority 1" + "\n";
541
+ // info.log += "scanning locators in priority 1" + "\n";
542
+ let onlyPriority3 = selectorsLocators[0].priority === 3;
467
543
  result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
468
544
  if (result.foundElements.length === 0) {
469
- info.log += "scanning locators in priority 2" + "\n";
545
+ // info.log += "scanning locators in priority 2" + "\n";
470
546
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
471
547
  }
472
- if (result.foundElements.length === 0 && !highPriorityOnly) {
473
- info.log += "scanning locators in priority 3" + "\n";
548
+ if (result.foundElements.length === 0 && onlyPriority3) {
474
549
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
475
550
  }
551
+ else {
552
+ if (result.foundElements.length === 0 && !highPriorityOnly) {
553
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
554
+ }
555
+ }
476
556
  let foundElements = result.foundElements;
477
557
  if (foundElements.length === 1 && foundElements[0].unique) {
478
558
  info.box = foundElements[0].box;
559
+ info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
479
560
  return foundElements[0].locator;
480
561
  }
481
562
  //info.log += "total elements found " + foundElements.length);
@@ -505,6 +586,7 @@ class StableBrowser {
505
586
  }
506
587
  }
507
588
  if (maxCountElement) {
589
+ info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
508
590
  info.box = await maxCountElement.locator.boundingBox();
509
591
  return maxCountElement.locator;
510
592
  }
@@ -513,9 +595,11 @@ class StableBrowser {
513
595
  break;
514
596
  }
515
597
  if (performance.now() - startTime > highPriorityTimeout) {
598
+ info.log += "high priority timeout, will try all elements" + "\n";
516
599
  highPriorityOnly = false;
517
600
  }
518
601
  if (performance.now() - startTime > visibleOnlyTimeout) {
602
+ info.log += "visible only timeout, will try all elements" + "\n";
519
603
  visibleOnly = false;
520
604
  }
521
605
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -560,7 +644,7 @@ class StableBrowser {
560
644
  this._validateSelectors(selectors);
561
645
  const startTime = Date.now();
562
646
  const info = {};
563
- info.log = "";
647
+ info.log = "***** click on " + selectors.element_name + " *****\n";
564
648
  info.operation = "click";
565
649
  info.selectors = selectors;
566
650
  let error = null;
@@ -905,12 +989,12 @@ class StableBrowser {
905
989
  await this.waitForPageLoad();
906
990
  }
907
991
  }
908
- catch (error) {
992
+ catch (err) {
909
993
  //await this.closeUnexpectedPopups();
910
994
  this.logger.error("setting date time input failed " + JSON.stringify(info));
911
995
  this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
912
996
  info.screenshotPath = screenshotPath;
913
- Object.assign(error, { info: info });
997
+ Object.assign(err, { info: info });
914
998
  await element.click();
915
999
  await new Promise((resolve) => setTimeout(resolve, 500));
916
1000
  if (format) {
@@ -931,76 +1015,7 @@ class StableBrowser {
931
1015
  }
932
1016
  }
933
1017
  }
934
- catch (error) {
935
- error = e;
936
- throw e;
937
- }
938
- finally {
939
- const endTime = Date.now();
940
- this._reportToWorld(world, {
941
- element_name: selectors.element_name,
942
- type: Types.SET_DATE_TIME,
943
- screenshotId,
944
- value: value,
945
- text: `setDateTime input with value: ${value}`,
946
- result: error
947
- ? {
948
- status: "FAILED",
949
- startTime,
950
- endTime,
951
- message: error === null || error === void 0 ? void 0 : error.message,
952
- }
953
- : {
954
- status: "PASSED",
955
- startTime,
956
- endTime,
957
- },
958
- info: info,
959
- });
960
- }
961
- }
962
- async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
963
- this._validateSelectors(selectors);
964
- const startTime = Date.now();
965
- let error = null;
966
- let screenshotId = null;
967
- let screenshotPath = null;
968
- const info = {};
969
- info.log = "";
970
- info.operation = Types.SET_DATE_TIME;
971
- info.selectors = selectors;
972
- info.value = value;
973
- try {
974
- let element = await this._locate(selectors, info, _params);
975
- //insert red border around the element
976
- await this.scrollIfNeeded(element, info);
977
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
978
- await this._highlightElements(element);
979
- try {
980
- await element.click();
981
- await new Promise((resolve) => setTimeout(resolve, 500));
982
- const dateTimeValue = await getDateTimeValue({ value, element });
983
- await element.evaluateHandle((el, dateTimeValue) => {
984
- el.value = ""; // clear input
985
- el.value = dateTimeValue;
986
- }, dateTimeValue);
987
- }
988
- catch (error) {
989
- //await this.closeUnexpectedPopups();
990
- this.logger.error("setting date time input failed " + JSON.stringify(info));
991
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
992
- info.screenshotPath = screenshotPath;
993
- Object.assign(error, { info: info });
994
- await element.click();
995
- await new Promise((resolve) => setTimeout(resolve, 500));
996
- const dateTimeValue = await getDateTimeValue({ value, element });
997
- await element.evaluateHandle((el, dateTimeValue) => {
998
- el.value = ""; // clear input
999
- el.value = dateTimeValue;
1000
- }, dateTimeValue);
1001
- }
1002
- }
1003
- catch (error) {
1018
+ catch (e) {
1004
1019
  error = e;
1005
1020
  throw e;
1006
1021
  }
@@ -1035,7 +1050,7 @@ class StableBrowser {
1035
1050
  let screenshotId = null;
1036
1051
  let screenshotPath = null;
1037
1052
  const info = {};
1038
- info.log = "";
1053
+ info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1039
1054
  info.operation = "clickType";
1040
1055
  info.selectors = selectors;
1041
1056
  const newValue = await this._replaceWithLocalData(_value, world);
@@ -1050,20 +1065,32 @@ class StableBrowser {
1050
1065
  await this.scrollIfNeeded(element, info);
1051
1066
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1052
1067
  await this._highlightElements(element);
1053
- try {
1054
- let currentValue = await element.inputValue();
1055
- if (currentValue) {
1056
- await element.fill("");
1068
+ if (options === null || options === undefined || !options.press) {
1069
+ try {
1070
+ let currentValue = await element.inputValue();
1071
+ if (currentValue) {
1072
+ await element.fill("");
1073
+ }
1074
+ }
1075
+ catch (e) {
1076
+ this.logger.info("unable to clear input value");
1057
1077
  }
1058
1078
  }
1059
- catch (e) {
1060
- this.logger.info("unable to clear input value");
1061
- }
1062
- try {
1063
- await element.click({ timeout: 5000 });
1079
+ if (options === null || options === undefined || options.press) {
1080
+ try {
1081
+ await element.click({ timeout: 5000 });
1082
+ }
1083
+ catch (e) {
1084
+ await element.dispatchEvent("click");
1085
+ }
1064
1086
  }
1065
- catch (e) {
1066
- await element.dispatchEvent("click");
1087
+ else {
1088
+ try {
1089
+ await element.focus();
1090
+ }
1091
+ catch (e) {
1092
+ await element.dispatchEvent("focus");
1093
+ }
1067
1094
  }
1068
1095
  await new Promise((resolve) => setTimeout(resolve, 500));
1069
1096
  const valueSegment = _value.split("&&");
@@ -1143,7 +1170,7 @@ class StableBrowser {
1143
1170
  let screenshotId = null;
1144
1171
  let screenshotPath = null;
1145
1172
  const info = {};
1146
- info.log = "";
1173
+ info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1147
1174
  info.operation = "fill";
1148
1175
  info.selectors = selectors;
1149
1176
  info.value = value;
@@ -1259,7 +1286,8 @@ class StableBrowser {
1259
1286
  let screenshotId = null;
1260
1287
  let screenshotPath = null;
1261
1288
  const info = {};
1262
- info.log = "";
1289
+ info.log =
1290
+ "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1263
1291
  info.operation = "containsPattern";
1264
1292
  info.selectors = selectors;
1265
1293
  info.value = text;
@@ -1325,7 +1353,7 @@ class StableBrowser {
1325
1353
  let screenshotId = null;
1326
1354
  let screenshotPath = null;
1327
1355
  const info = {};
1328
- info.log = "";
1356
+ info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1329
1357
  info.operation = "containsText";
1330
1358
  info.selectors = selectors;
1331
1359
  const newValue = await this._replaceWithLocalData(text, world);
@@ -1429,15 +1457,62 @@ class StableBrowser {
1429
1457
  // save the data to the file
1430
1458
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1431
1459
  }
1460
+ _getDataFilePath(fileName) {
1461
+ let dataFile = path.join(this.project_path, "data", fileName);
1462
+ if (fs.existsSync(dataFile)) {
1463
+ return dataFile;
1464
+ }
1465
+ dataFile = path.join(this.project_path, fileName);
1466
+ if (fs.existsSync(dataFile)) {
1467
+ return dataFile;
1468
+ }
1469
+ throw new Error("data file not found " + fileName);
1470
+ }
1471
+ _parseCSVSync(filePath) {
1472
+ const data = fs.readFileSync(filePath, "utf8");
1473
+ const results = [];
1474
+ return new Promise((resolve, reject) => {
1475
+ const readableStream = new Readable();
1476
+ readableStream._read = () => { }; // _read is required but you can noop it
1477
+ readableStream.push(data);
1478
+ readableStream.push(null);
1479
+ readableStream
1480
+ .pipe(csv())
1481
+ .on("data", (data) => results.push(data))
1482
+ .on("end", () => resolve(results))
1483
+ .on("error", (error) => reject(error));
1484
+ });
1485
+ }
1432
1486
  loadTestData(type, dataSelector, world = null) {
1433
1487
  switch (type) {
1434
1488
  case "users":
1435
- // check if file users.json exists
1436
- if (!fs.existsSync(path.join(this.project_path, "users.json"))) {
1437
- throw new Error("users.json file not found");
1489
+ // get the users.json file path
1490
+ let dataFile = this._getDataFilePath("users.json");
1491
+ // read the file and return the data
1492
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1493
+ for (let i = 0; i < users.length; i++) {
1494
+ if (users[i].username === dataSelector) {
1495
+ const userObj = {
1496
+ username: users[i].username,
1497
+ password: "secret:" + users[i].password,
1498
+ totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
1499
+ };
1500
+ this.setTestData(userObj, world);
1501
+ return userObj;
1502
+ }
1438
1503
  }
1504
+ throw new Error("user not found " + dataSelector);
1505
+ default:
1506
+ throw new Error("unknown type " + type);
1507
+ }
1508
+ }
1509
+ async loadTestDataAsync(type, dataSelector, world = null) {
1510
+ switch (type) {
1511
+ case "users": {
1512
+ // get the users.json file path
1513
+ let dataFile = this._getDataFilePath("users.json");
1439
1514
  // read the file and return the data
1440
- const users = JSON.parse(fs.readFileSync(path.join(this.project_path, "users.json"), "utf8"));
1515
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1441
1516
  for (let i = 0; i < users.length; i++) {
1442
1517
  if (users[i].username === dataSelector) {
1443
1518
  const userObj = {
@@ -1450,6 +1525,29 @@ class StableBrowser {
1450
1525
  }
1451
1526
  }
1452
1527
  throw new Error("user not found " + dataSelector);
1528
+ }
1529
+ case "csv": {
1530
+ // the dataSelector should start with the file name followed by the row number: data.csv:1, if no row number is provided, it will default to 1
1531
+ const parts = dataSelector.split(":");
1532
+ let rowNumber = 0;
1533
+ if (parts.length > 1) {
1534
+ rowNumber = parseInt(parts[1]);
1535
+ }
1536
+ let dataFile = this._getDataFilePath(parts[0]);
1537
+ const results = await this._parseCSVSync(dataFile);
1538
+ // result stracture:
1539
+ // [
1540
+ // { NAME: 'Daffy Duck', AGE: '24' },
1541
+ // { NAME: 'Bugs Bunny', AGE: '22' }
1542
+ // ]
1543
+ // verify the row number is within the range
1544
+ if (rowNumber >= results.length) {
1545
+ throw new Error("row number is out of range " + rowNumber);
1546
+ }
1547
+ const data = results[rowNumber];
1548
+ this.setTestData(data, world);
1549
+ return data;
1550
+ }
1453
1551
  default:
1454
1552
  throw new Error("unknown type " + type);
1455
1553
  }
@@ -1588,7 +1686,7 @@ class StableBrowser {
1588
1686
  let screenshotPath = null;
1589
1687
  await new Promise((resolve) => setTimeout(resolve, 2000));
1590
1688
  const info = {};
1591
- info.log = "";
1689
+ info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1592
1690
  info.operation = "verify";
1593
1691
  info.selectors = selectors;
1594
1692
  try {
@@ -1641,7 +1739,7 @@ class StableBrowser {
1641
1739
  let screenshotPath = null;
1642
1740
  await new Promise((resolve) => setTimeout(resolve, 2000));
1643
1741
  const info = {};
1644
- info.log = "";
1742
+ info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1645
1743
  info.operation = "extract";
1646
1744
  info.selectors = selectors;
1647
1745
  try {
@@ -1666,7 +1764,8 @@ class StableBrowser {
1666
1764
  if (world) {
1667
1765
  world[variable] = info.value;
1668
1766
  }
1669
- this.logger.info("world." + variable + "=" + info.value);
1767
+ this.setTestData({ [variable]: info.value }, world);
1768
+ this.logger.info("set test data: " + variable + "=" + info.value);
1670
1769
  return info;
1671
1770
  }
1672
1771
  catch (e) {
@@ -1703,6 +1802,91 @@ class StableBrowser {
1703
1802
  });
1704
1803
  }
1705
1804
  }
1805
+ async extractEmailData(emailAddress, options, world) {
1806
+ if (!emailAddress) {
1807
+ throw new Error("email address is null");
1808
+ }
1809
+ // check if address contain @
1810
+ if (emailAddress.indexOf("@") === -1) {
1811
+ emailAddress = emailAddress + "@blinq-mail.io";
1812
+ }
1813
+ else {
1814
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1815
+ throw new Error("email address should end with @blinq-mail.io");
1816
+ }
1817
+ }
1818
+ const startTime = Date.now();
1819
+ let timeout = 60000;
1820
+ if (options && options.timeout) {
1821
+ timeout = options.timeout;
1822
+ }
1823
+ const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1824
+ const request = {
1825
+ method: "POST",
1826
+ url: serviceUrl,
1827
+ headers: {
1828
+ "Content-Type": "application/json",
1829
+ Authorization: `Bearer ${process.env.TOKEN}`,
1830
+ },
1831
+ data: JSON.stringify({
1832
+ email: emailAddress,
1833
+ }),
1834
+ };
1835
+ let errorCount = 0;
1836
+ while (true) {
1837
+ try {
1838
+ let result = await this.context.api.request(request);
1839
+ // the response body expected to be the following:
1840
+ // {
1841
+ // "status": true,
1842
+ // "content": {
1843
+ // "url": "",
1844
+ // "code": "112112",
1845
+ // "name": "generate_link_or_code"
1846
+ // }
1847
+ //}
1848
+ if ((result && result.data, result.data.status === true)) {
1849
+ let codeOrUrlFound = false;
1850
+ let emailCode = null;
1851
+ let emailUrl = null;
1852
+ // check if a code is returned
1853
+ if (result.data.content && result.data.content.code) {
1854
+ let code = result.data.content.code;
1855
+ this.setTestData({ emailCode: code }, world);
1856
+ this.logger.info("set test data: emailCode = " + code);
1857
+ emailCode = code;
1858
+ codeOrUrlFound = true;
1859
+ }
1860
+ // check if a url is returned
1861
+ if (result.data.content && result.data.content.url) {
1862
+ let url = result.data.content.url;
1863
+ this.setTestData({ emailUrl: url }, world);
1864
+ this.logger.info("set test data: emailUrl = " + url);
1865
+ emailUrl = url;
1866
+ codeOrUrlFound = true;
1867
+ }
1868
+ if (codeOrUrlFound) {
1869
+ return { emailUrl, emailCode };
1870
+ }
1871
+ else {
1872
+ this.logger.info("an email received but no code or url found");
1873
+ }
1874
+ }
1875
+ }
1876
+ catch (e) {
1877
+ errorCount++;
1878
+ if (errorCount > 3) {
1879
+ throw e;
1880
+ }
1881
+ // ignore
1882
+ }
1883
+ // check if the timeout is reached
1884
+ if (Date.now() - startTime > timeout) {
1885
+ throw new Error("timeout reached");
1886
+ }
1887
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1888
+ }
1889
+ }
1706
1890
  async _highlightElements(scope, css) {
1707
1891
  try {
1708
1892
  if (!scope) {
@@ -1774,7 +1958,7 @@ class StableBrowser {
1774
1958
  let screenshotPath = null;
1775
1959
  await new Promise((resolve) => setTimeout(resolve, 2000));
1776
1960
  const info = {};
1777
- info.log = "";
1961
+ info.log = "***** verify page path " + pathPart + " *****\n";
1778
1962
  info.operation = "verifyPagePath";
1779
1963
  const newValue = await this._replaceWithLocalData(pathPart, world);
1780
1964
  if (newValue !== pathPart) {
@@ -1835,7 +2019,7 @@ class StableBrowser {
1835
2019
  let screenshotPath = null;
1836
2020
  await new Promise((resolve) => setTimeout(resolve, 2000));
1837
2021
  const info = {};
1838
- info.log = "";
2022
+ info.log = "***** verify text " + text + " exists in page *****\n";
1839
2023
  info.operation = "verifyTextExistInPage";
1840
2024
  const newValue = await this._replaceWithLocalData(text, world);
1841
2025
  if (newValue !== text) {
@@ -1884,8 +2068,10 @@ class StableBrowser {
1884
2068
  const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
1885
2069
  await this._highlightElements(frame, dataAttribute);
1886
2070
  const element = await frame.$(dataAttribute);
1887
- await this.scrollIfNeeded(element, info);
1888
- await element.dispatchEvent("bvt_verify_page_contains_text");
2071
+ if (element) {
2072
+ await this.scrollIfNeeded(element, info);
2073
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2074
+ }
1889
2075
  }
1890
2076
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1891
2077
  return info;
@@ -1923,6 +2109,16 @@ class StableBrowser {
1923
2109
  });
1924
2110
  }
1925
2111
  }
2112
+ _getServerUrl() {
2113
+ let serviceUrl = "https://api.blinq.io";
2114
+ if (process.env.NODE_ENV_BLINQ === "dev") {
2115
+ serviceUrl = "https://dev.api.blinq.io";
2116
+ }
2117
+ else if (process.env.NODE_ENV_BLINQ === "stage") {
2118
+ serviceUrl = "https://stage.api.blinq.io";
2119
+ }
2120
+ return serviceUrl;
2121
+ }
1926
2122
  async visualVerification(text, options = {}, world = null) {
1927
2123
  const startTime = Date.now();
1928
2124
  let error = null;
@@ -1937,13 +2133,7 @@ class StableBrowser {
1937
2133
  throw new Error("TOKEN is not set");
1938
2134
  }
1939
2135
  try {
1940
- let serviceUrl = "https://api.blinq.io";
1941
- if (process.env.NODE_ENV_BLINQ === "dev") {
1942
- serviceUrl = "https://dev.api.blinq.io";
1943
- }
1944
- else if (process.env.NODE_ENV_BLINQ === "stage") {
1945
- serviceUrl = "https://stage.api.blinq.io";
1946
- }
2136
+ let serviceUrl = this._getServerUrl();
1947
2137
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1948
2138
  info.screenshotPath = screenshotPath;
1949
2139
  const screenshot = await this.takeScreenshot();
@@ -2092,7 +2282,16 @@ class StableBrowser {
2092
2282
  let screenshotId = null;
2093
2283
  let screenshotPath = null;
2094
2284
  const info = {};
2095
- info.log = "";
2285
+ info.log =
2286
+ "***** analyze table " +
2287
+ selectors.element_name +
2288
+ " query " +
2289
+ query +
2290
+ " operator " +
2291
+ operator +
2292
+ " value " +
2293
+ value +
2294
+ " *****\n";
2096
2295
  info.operation = "analyzeTable";
2097
2296
  info.selectors = selectors;
2098
2297
  info.query = query;
@@ -2326,13 +2525,6 @@ class StableBrowser {
2326
2525
  const info = {};
2327
2526
  try {
2328
2527
  await this.page.close();
2329
- if (this.context && this.context.pages && this.context.pages.length > 0) {
2330
- this.context.pages.pop();
2331
- this.page = this.context.pages[this.context.pages.length - 1];
2332
- this.context.page = this.page;
2333
- let title = await this.page.title();
2334
- console.log("Switched to page " + title);
2335
- }
2336
2528
  }
2337
2529
  catch (e) {
2338
2530
  console.log(".");