automation_model 1.0.371-dev.0 → 1.0.371-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,8 @@ 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",
42
+ SET_INPUT: "set_input",
38
43
  };
39
44
  class StableBrowser {
40
45
  constructor(browser, page, logger = null, context = null) {
@@ -80,9 +85,15 @@ class StableBrowser {
80
85
  this.page = page;
81
86
  context.page = page;
82
87
  context.pages.push(page);
83
- this.webLogFile = this.getWebLogFile(logFolder);
84
- this.registerConsoleLogListener(page, context, this.webLogFile);
85
- this.registerRequestListener();
88
+ page.on("close", async () => {
89
+ if (this.context && this.context.pages && this.context.pages.length > 0) {
90
+ this.context.pages.pop();
91
+ this.page = this.context.pages[this.context.pages.length - 1];
92
+ this.context.page = this.page;
93
+ let title = await this.page.title();
94
+ console.log("Switched to page " + title);
95
+ }
96
+ });
86
97
  try {
87
98
  await this.waitForPageLoad();
88
99
  console.log("Switch page: " + (await page.title()));
@@ -91,7 +102,7 @@ class StableBrowser {
91
102
  this.logger.error("error on page load " + e);
92
103
  }
93
104
  context.pageLoading.status = false;
94
- });
105
+ }.bind(this));
95
106
  }
96
107
  getWebLogFile(logFolder) {
97
108
  if (!fs.existsSync(logFolder)) {
@@ -168,25 +179,85 @@ class StableBrowser {
168
179
  return text;
169
180
  }
170
181
  for (let key in _params) {
171
- text = text.replaceAll(new RegExp("{" + key + "}", "g"), _params[key]);
182
+ let regValue = key;
183
+ if (key.startsWith("_")) {
184
+ // remove the _ prefix
185
+ regValue = key.substring(1);
186
+ }
187
+ text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
172
188
  }
173
189
  return text;
174
190
  }
191
+ _fixLocatorUsingParams(locator, _params) {
192
+ // check if not null
193
+ if (!locator) {
194
+ return locator;
195
+ }
196
+ // clone the locator
197
+ locator = JSON.parse(JSON.stringify(locator));
198
+ this.scanAndManipulate(locator, _params);
199
+ return locator;
200
+ }
201
+ _isObject(value) {
202
+ return value && typeof value === "object" && value.constructor === Object;
203
+ }
204
+ scanAndManipulate(currentObj, _params) {
205
+ for (const key in currentObj) {
206
+ if (typeof currentObj[key] === "string") {
207
+ // Perform string manipulation
208
+ currentObj[key] = this._fixUsingParams(currentObj[key], _params);
209
+ }
210
+ else if (this._isObject(currentObj[key])) {
211
+ // Recursively scan nested objects
212
+ this.scanAndManipulate(currentObj[key], _params);
213
+ }
214
+ }
215
+ }
175
216
  _getLocator(locator, scope, _params) {
217
+ locator = this._fixLocatorUsingParams(locator, _params);
218
+ let locatorReturn;
176
219
  if (locator.role) {
177
220
  if (locator.role[1].nameReg) {
178
221
  locator.role[1].name = reg_parser(locator.role[1].nameReg);
179
222
  delete locator.role[1].nameReg;
180
223
  }
181
- if (locator.role[1].name) {
182
- locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
183
- }
184
- return scope.getByRole(locator.role[0], locator.role[1]);
224
+ // if (locator.role[1].name) {
225
+ // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
226
+ // }
227
+ locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
185
228
  }
186
229
  if (locator.css) {
187
- return scope.locator(this._fixUsingParams(locator.css, _params));
230
+ locatorReturn = scope.locator(locator.css);
231
+ }
232
+ // handle role/name locators
233
+ // locator.selector will be something like: textbox[name="Username"i]
234
+ if (locator.engine === "internal:role") {
235
+ // extract the role, name and the i/s flags using regex
236
+ const match = locator.selector.match(/(.*)\[(.*)="(.*)"(.*)\]/);
237
+ if (match) {
238
+ const role = match[1];
239
+ const name = match[3];
240
+ const flags = match[4];
241
+ locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
242
+ }
188
243
  }
189
- throw new Error("unknown locator type");
244
+ if (locator === null || locator === void 0 ? void 0 : locator.engine) {
245
+ if (locator.engine === "css") {
246
+ locatorReturn = scope.locator(locator.selector);
247
+ }
248
+ else {
249
+ let selector = locator.selector;
250
+ if (locator.engine === "internal:attr") {
251
+ selector = `[${selector}]`;
252
+ }
253
+ locatorReturn = scope.locator(`${locator.engine}=${selector}`);
254
+ }
255
+ }
256
+ if (!locatorReturn) {
257
+ console.error(locator);
258
+ throw new Error("Locator undefined");
259
+ }
260
+ return locatorReturn;
190
261
  }
191
262
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
192
263
  let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
@@ -309,10 +380,13 @@ class StableBrowser {
309
380
  }
310
381
  async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
311
382
  let locatorSearch = selectorHierarchy[index];
312
- info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
383
+ //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
313
384
  let locator = null;
314
385
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
315
386
  let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
387
+ if (!locatorString) {
388
+ return;
389
+ }
316
390
  locator = this._getLocator({ css: locatorString }, scope, _params);
317
391
  }
318
392
  else if (locatorSearch.text) {
@@ -334,7 +408,7 @@ class StableBrowser {
334
408
  // cssHref = true;
335
409
  // }
336
410
  let count = await locator.count();
337
- info.log += "total elements found " + count + "\n";
411
+ //info.log += "total elements found " + count + "\n";
338
412
  //let visibleCount = 0;
339
413
  let visibleLocator = null;
340
414
  if (locatorSearch.index && locatorSearch.index < count) {
@@ -344,16 +418,20 @@ class StableBrowser {
344
418
  for (let j = 0; j < count; j++) {
345
419
  let visible = await locator.nth(j).isVisible();
346
420
  const enabled = await locator.nth(j).isEnabled();
347
- info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
348
421
  if (!visibleOnly) {
349
422
  visible = true;
350
423
  }
351
424
  if (visible && enabled) {
352
425
  foundLocators.push(locator.nth(j));
353
- // if (cssHref) {
354
- // info.log += "css href locator found, will ignore all others" + "\n";
355
- // break;
356
- // }
426
+ }
427
+ else {
428
+ if (!info.printMessages) {
429
+ info.printMessages = {};
430
+ }
431
+ if (!info.printMessages[j.toString()]) {
432
+ info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
433
+ info.printMessages[j.toString()] = true;
434
+ }
357
435
  }
358
436
  }
359
437
  }
@@ -399,6 +477,10 @@ class StableBrowser {
399
477
  async _locate(selectors, info, _params, timeout = 30000) {
400
478
  for (let i = 0; i < 3; i++) {
401
479
  info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
480
+ for (let j = 0; j < selectors.locators.length; j++) {
481
+ let selector = selectors.locators[j];
482
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
483
+ }
402
484
  let element = await this._locate_internal(selectors, info, _params, timeout);
403
485
  if (!element.rerun) {
404
486
  return element;
@@ -493,6 +575,7 @@ class StableBrowser {
493
575
  let foundElements = result.foundElements;
494
576
  if (foundElements.length === 1 && foundElements[0].unique) {
495
577
  info.box = foundElements[0].box;
578
+ info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
496
579
  return foundElements[0].locator;
497
580
  }
498
581
  //info.log += "total elements found " + foundElements.length);
@@ -522,6 +605,7 @@ class StableBrowser {
522
605
  }
523
606
  }
524
607
  if (maxCountElement) {
608
+ info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
525
609
  info.box = await maxCountElement.locator.boundingBox();
526
610
  return maxCountElement.locator;
527
611
  }
@@ -530,9 +614,11 @@ class StableBrowser {
530
614
  break;
531
615
  }
532
616
  if (performance.now() - startTime > highPriorityTimeout) {
617
+ info.log += "high priority timeout, will try all elements" + "\n";
533
618
  highPriorityOnly = false;
534
619
  }
535
620
  if (performance.now() - startTime > visibleOnlyTimeout) {
621
+ info.log += "visible only timeout, will try all elements" + "\n";
536
622
  visibleOnly = false;
537
623
  }
538
624
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -576,6 +662,9 @@ class StableBrowser {
576
662
  async click(selectors, _params, options = {}, world = null) {
577
663
  this._validateSelectors(selectors);
578
664
  const startTime = Date.now();
665
+ if (options && options.context) {
666
+ selectors.locators[0].text = options.context;
667
+ }
579
668
  const info = {};
580
669
  info.log = "***** click on " + selectors.element_name + " *****\n";
581
670
  info.operation = "click";
@@ -884,71 +973,45 @@ class StableBrowser {
884
973
  });
885
974
  }
886
975
  }
887
- async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
976
+ async setInputValue(selectors, value, _params = null, options = {}, world = null) {
977
+ // set input value for non fillable inputs like date, time, range, color, etc.
888
978
  this._validateSelectors(selectors);
889
979
  const startTime = Date.now();
890
- let error = null;
891
- let screenshotId = null;
892
- let screenshotPath = null;
893
980
  const info = {};
894
- info.log = "";
895
- info.operation = Types.SET_DATE_TIME;
981
+ info.log = "***** set input value " + selectors.element_name + " *****\n";
982
+ info.operation = "setInputValue";
896
983
  info.selectors = selectors;
984
+ value = this._fixUsingParams(value, _params);
897
985
  info.value = value;
986
+ let error = null;
987
+ let screenshotId = null;
988
+ let screenshotPath = null;
898
989
  try {
899
990
  value = await this._replaceWithLocalData(value, this);
900
991
  let element = await this._locate(selectors, info, _params);
901
- //insert red border around the element
902
992
  await this.scrollIfNeeded(element, info);
903
993
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
904
994
  await this._highlightElements(element);
905
995
  try {
906
- await element.click();
907
- await new Promise((resolve) => setTimeout(resolve, 500));
908
- if (format) {
909
- value = dayjs(value).format(format);
910
- await element.fill(value);
911
- }
912
- else {
913
- const dateTimeValue = await getDateTimeValue({ value, element });
914
- await element.evaluateHandle((el, dateTimeValue) => {
915
- el.value = ""; // clear input
916
- el.value = dateTimeValue;
917
- }, dateTimeValue);
918
- }
919
- if (enter) {
920
- await new Promise((resolve) => setTimeout(resolve, 2000));
921
- await this.page.keyboard.press("Enter");
922
- await this.waitForPageLoad();
923
- }
996
+ await element.evaluateHandle((el, value) => {
997
+ el.value = value;
998
+ }, value);
924
999
  }
925
1000
  catch (error) {
926
- //await this.closeUnexpectedPopups();
927
- this.logger.error("setting date time input failed " + JSON.stringify(info));
928
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
1001
+ this.logger.error("setInputValue failed, will try again");
1002
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
929
1003
  info.screenshotPath = screenshotPath;
930
1004
  Object.assign(error, { info: info });
931
- await element.click();
932
- await new Promise((resolve) => setTimeout(resolve, 500));
933
- if (format) {
934
- value = dayjs(value).format(format);
935
- await element.fill(value);
936
- }
937
- else {
938
- const dateTimeValue = await getDateTimeValue({ value, element });
939
- await element.evaluateHandle((el, dateTimeValue) => {
940
- el.value = ""; // clear input
941
- el.value = dateTimeValue;
942
- }, dateTimeValue);
943
- }
944
- if (enter) {
945
- await new Promise((resolve) => setTimeout(resolve, 2000));
946
- await this.page.keyboard.press("Enter");
947
- await this.waitForPageLoad();
948
- }
1005
+ await element.evaluateHandle((el, value) => {
1006
+ el.value = value;
1007
+ });
949
1008
  }
950
1009
  }
951
- catch (error) {
1010
+ catch (e) {
1011
+ this.logger.error("setInputValue failed " + info.log);
1012
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1013
+ info.screenshotPath = screenshotPath;
1014
+ Object.assign(e, { info: info });
952
1015
  error = e;
953
1016
  throw e;
954
1017
  }
@@ -956,10 +1019,10 @@ class StableBrowser {
956
1019
  const endTime = Date.now();
957
1020
  this._reportToWorld(world, {
958
1021
  element_name: selectors.element_name,
959
- type: Types.SET_DATE_TIME,
960
- screenshotId,
1022
+ type: Types.SET_INPUT,
1023
+ text: `Set input value`,
961
1024
  value: value,
962
- text: `setDateTime input with value: ${value}`,
1025
+ screenshotId,
963
1026
  result: error
964
1027
  ? {
965
1028
  status: "FAILED",
@@ -976,7 +1039,7 @@ class StableBrowser {
976
1039
  });
977
1040
  }
978
1041
  }
979
- async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
1042
+ async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
980
1043
  this._validateSelectors(selectors);
981
1044
  const startTime = Date.now();
982
1045
  let error = null;
@@ -988,6 +1051,7 @@ class StableBrowser {
988
1051
  info.selectors = selectors;
989
1052
  info.value = value;
990
1053
  try {
1054
+ value = await this._replaceWithLocalData(value, this);
991
1055
  let element = await this._locate(selectors, info, _params);
992
1056
  //insert red border around the element
993
1057
  await this.scrollIfNeeded(element, info);
@@ -996,28 +1060,51 @@ class StableBrowser {
996
1060
  try {
997
1061
  await element.click();
998
1062
  await new Promise((resolve) => setTimeout(resolve, 500));
999
- const dateTimeValue = await getDateTimeValue({ value, element });
1000
- await element.evaluateHandle((el, dateTimeValue) => {
1001
- el.value = ""; // clear input
1002
- el.value = dateTimeValue;
1003
- }, dateTimeValue);
1063
+ if (format) {
1064
+ value = dayjs(value).format(format);
1065
+ await element.fill(value);
1066
+ }
1067
+ else {
1068
+ const dateTimeValue = await getDateTimeValue({ value, element });
1069
+ await element.evaluateHandle((el, dateTimeValue) => {
1070
+ el.value = ""; // clear input
1071
+ el.value = dateTimeValue;
1072
+ }, dateTimeValue);
1073
+ }
1074
+ if (enter) {
1075
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1076
+ await this.page.keyboard.press("Enter");
1077
+ await this.waitForPageLoad();
1078
+ }
1004
1079
  }
1005
- catch (error) {
1080
+ catch (err) {
1006
1081
  //await this.closeUnexpectedPopups();
1007
1082
  this.logger.error("setting date time input failed " + JSON.stringify(info));
1008
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
1083
+ this.logger.info("Trying again");
1084
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1009
1085
  info.screenshotPath = screenshotPath;
1010
- Object.assign(error, { info: info });
1086
+ Object.assign(err, { info: info });
1011
1087
  await element.click();
1012
1088
  await new Promise((resolve) => setTimeout(resolve, 500));
1013
- const dateTimeValue = await getDateTimeValue({ value, element });
1014
- await element.evaluateHandle((el, dateTimeValue) => {
1015
- el.value = ""; // clear input
1016
- el.value = dateTimeValue;
1017
- }, dateTimeValue);
1089
+ if (format) {
1090
+ value = dayjs(value).format(format);
1091
+ await element.fill(value);
1092
+ }
1093
+ else {
1094
+ const dateTimeValue = await getDateTimeValue({ value, element });
1095
+ await element.evaluateHandle((el, dateTimeValue) => {
1096
+ el.value = ""; // clear input
1097
+ el.value = dateTimeValue;
1098
+ }, dateTimeValue);
1099
+ }
1100
+ if (enter) {
1101
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1102
+ await this.page.keyboard.press("Enter");
1103
+ await this.waitForPageLoad();
1104
+ }
1018
1105
  }
1019
1106
  }
1020
- catch (error) {
1107
+ catch (e) {
1021
1108
  error = e;
1022
1109
  throw e;
1023
1110
  }
@@ -1067,20 +1154,32 @@ class StableBrowser {
1067
1154
  await this.scrollIfNeeded(element, info);
1068
1155
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1069
1156
  await this._highlightElements(element);
1070
- try {
1071
- let currentValue = await element.inputValue();
1072
- if (currentValue) {
1073
- await element.fill("");
1157
+ if (options === null || options === undefined || !options.press) {
1158
+ try {
1159
+ let currentValue = await element.inputValue();
1160
+ if (currentValue) {
1161
+ await element.fill("");
1162
+ }
1163
+ }
1164
+ catch (e) {
1165
+ this.logger.info("unable to clear input value");
1074
1166
  }
1075
1167
  }
1076
- catch (e) {
1077
- this.logger.info("unable to clear input value");
1078
- }
1079
- try {
1080
- await element.click({ timeout: 5000 });
1168
+ if (options === null || options === undefined || options.press) {
1169
+ try {
1170
+ await element.click({ timeout: 5000 });
1171
+ }
1172
+ catch (e) {
1173
+ await element.dispatchEvent("click");
1174
+ }
1081
1175
  }
1082
- catch (e) {
1083
- await element.dispatchEvent("click");
1176
+ else {
1177
+ try {
1178
+ await element.focus();
1179
+ }
1180
+ catch (e) {
1181
+ await element.dispatchEvent("focus");
1182
+ }
1084
1183
  }
1085
1184
  await new Promise((resolve) => setTimeout(resolve, 500));
1086
1185
  const valueSegment = _value.split("&&");
@@ -1276,7 +1375,8 @@ class StableBrowser {
1276
1375
  let screenshotId = null;
1277
1376
  let screenshotPath = null;
1278
1377
  const info = {};
1279
- info.log = "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1378
+ info.log =
1379
+ "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1280
1380
  info.operation = "containsPattern";
1281
1381
  info.selectors = selectors;
1282
1382
  info.value = text;
@@ -1446,15 +1546,62 @@ class StableBrowser {
1446
1546
  // save the data to the file
1447
1547
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1448
1548
  }
1549
+ _getDataFilePath(fileName) {
1550
+ let dataFile = path.join(this.project_path, "data", fileName);
1551
+ if (fs.existsSync(dataFile)) {
1552
+ return dataFile;
1553
+ }
1554
+ dataFile = path.join(this.project_path, fileName);
1555
+ if (fs.existsSync(dataFile)) {
1556
+ return dataFile;
1557
+ }
1558
+ throw new Error("data file not found " + fileName);
1559
+ }
1560
+ _parseCSVSync(filePath) {
1561
+ const data = fs.readFileSync(filePath, "utf8");
1562
+ const results = [];
1563
+ return new Promise((resolve, reject) => {
1564
+ const readableStream = new Readable();
1565
+ readableStream._read = () => { }; // _read is required but you can noop it
1566
+ readableStream.push(data);
1567
+ readableStream.push(null);
1568
+ readableStream
1569
+ .pipe(csv())
1570
+ .on("data", (data) => results.push(data))
1571
+ .on("end", () => resolve(results))
1572
+ .on("error", (error) => reject(error));
1573
+ });
1574
+ }
1449
1575
  loadTestData(type, dataSelector, world = null) {
1450
1576
  switch (type) {
1451
1577
  case "users":
1452
- // check if file users.json exists
1453
- if (!fs.existsSync(path.join(this.project_path, "users.json"))) {
1454
- throw new Error("users.json file not found");
1578
+ // get the users.json file path
1579
+ let dataFile = this._getDataFilePath("users.json");
1580
+ // read the file and return the data
1581
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1582
+ for (let i = 0; i < users.length; i++) {
1583
+ if (users[i].username === dataSelector) {
1584
+ const userObj = {
1585
+ username: users[i].username,
1586
+ password: "secret:" + users[i].password,
1587
+ totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
1588
+ };
1589
+ this.setTestData(userObj, world);
1590
+ return userObj;
1591
+ }
1455
1592
  }
1593
+ throw new Error("user not found " + dataSelector);
1594
+ default:
1595
+ throw new Error("unknown type " + type);
1596
+ }
1597
+ }
1598
+ async loadTestDataAsync(type, dataSelector, world = null) {
1599
+ switch (type) {
1600
+ case "users": {
1601
+ // get the users.json file path
1602
+ let dataFile = this._getDataFilePath("users.json");
1456
1603
  // read the file and return the data
1457
- const users = JSON.parse(fs.readFileSync(path.join(this.project_path, "users.json"), "utf8"));
1604
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1458
1605
  for (let i = 0; i < users.length; i++) {
1459
1606
  if (users[i].username === dataSelector) {
1460
1607
  const userObj = {
@@ -1467,6 +1614,29 @@ class StableBrowser {
1467
1614
  }
1468
1615
  }
1469
1616
  throw new Error("user not found " + dataSelector);
1617
+ }
1618
+ case "csv": {
1619
+ // 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
1620
+ const parts = dataSelector.split(":");
1621
+ let rowNumber = 0;
1622
+ if (parts.length > 1) {
1623
+ rowNumber = parseInt(parts[1]);
1624
+ }
1625
+ let dataFile = this._getDataFilePath(parts[0]);
1626
+ const results = await this._parseCSVSync(dataFile);
1627
+ // result stracture:
1628
+ // [
1629
+ // { NAME: 'Daffy Duck', AGE: '24' },
1630
+ // { NAME: 'Bugs Bunny', AGE: '22' }
1631
+ // ]
1632
+ // verify the row number is within the range
1633
+ if (rowNumber >= results.length) {
1634
+ throw new Error("row number is out of range " + rowNumber);
1635
+ }
1636
+ const data = results[rowNumber];
1637
+ this.setTestData(data, world);
1638
+ return data;
1639
+ }
1470
1640
  default:
1471
1641
  throw new Error("unknown type " + type);
1472
1642
  }
@@ -1571,13 +1741,13 @@ class StableBrowser {
1571
1741
  ])));
1572
1742
  const { data } = await client.send("Page.captureScreenshot", {
1573
1743
  format: "png",
1574
- clip: {
1575
- x: 0,
1576
- y: 0,
1577
- width: viewportWidth,
1578
- height: viewportHeight,
1579
- scale: 1,
1580
- },
1744
+ // clip: {
1745
+ // x: 0,
1746
+ // y: 0,
1747
+ // width: viewportWidth,
1748
+ // height: viewportHeight,
1749
+ // scale: 1,
1750
+ // },
1581
1751
  });
1582
1752
  if (!screenshotPath) {
1583
1753
  return data;
@@ -1683,7 +1853,8 @@ class StableBrowser {
1683
1853
  if (world) {
1684
1854
  world[variable] = info.value;
1685
1855
  }
1686
- this.logger.info("world." + variable + "=" + info.value);
1856
+ this.setTestData({ [variable]: info.value }, world);
1857
+ this.logger.info("set test data: " + variable + "=" + info.value);
1687
1858
  return info;
1688
1859
  }
1689
1860
  catch (e) {
@@ -1720,6 +1891,91 @@ class StableBrowser {
1720
1891
  });
1721
1892
  }
1722
1893
  }
1894
+ async extractEmailData(emailAddress, options, world) {
1895
+ if (!emailAddress) {
1896
+ throw new Error("email address is null");
1897
+ }
1898
+ // check if address contain @
1899
+ if (emailAddress.indexOf("@") === -1) {
1900
+ emailAddress = emailAddress + "@blinq-mail.io";
1901
+ }
1902
+ else {
1903
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1904
+ throw new Error("email address should end with @blinq-mail.io");
1905
+ }
1906
+ }
1907
+ const startTime = Date.now();
1908
+ let timeout = 60000;
1909
+ if (options && options.timeout) {
1910
+ timeout = options.timeout;
1911
+ }
1912
+ const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1913
+ const request = {
1914
+ method: "POST",
1915
+ url: serviceUrl,
1916
+ headers: {
1917
+ "Content-Type": "application/json",
1918
+ Authorization: `Bearer ${process.env.TOKEN}`,
1919
+ },
1920
+ data: JSON.stringify({
1921
+ email: emailAddress,
1922
+ }),
1923
+ };
1924
+ let errorCount = 0;
1925
+ while (true) {
1926
+ try {
1927
+ let result = await this.context.api.request(request);
1928
+ // the response body expected to be the following:
1929
+ // {
1930
+ // "status": true,
1931
+ // "content": {
1932
+ // "url": "",
1933
+ // "code": "112112",
1934
+ // "name": "generate_link_or_code"
1935
+ // }
1936
+ //}
1937
+ if ((result && result.data, result.data.status === true)) {
1938
+ let codeOrUrlFound = false;
1939
+ let emailCode = null;
1940
+ let emailUrl = null;
1941
+ // check if a code is returned
1942
+ if (result.data.content && result.data.content.code) {
1943
+ let code = result.data.content.code;
1944
+ this.setTestData({ emailCode: code }, world);
1945
+ this.logger.info("set test data: emailCode = " + code);
1946
+ emailCode = code;
1947
+ codeOrUrlFound = true;
1948
+ }
1949
+ // check if a url is returned
1950
+ if (result.data.content && result.data.content.url) {
1951
+ let url = result.data.content.url;
1952
+ this.setTestData({ emailUrl: url }, world);
1953
+ this.logger.info("set test data: emailUrl = " + url);
1954
+ emailUrl = url;
1955
+ codeOrUrlFound = true;
1956
+ }
1957
+ if (codeOrUrlFound) {
1958
+ return { emailUrl, emailCode };
1959
+ }
1960
+ else {
1961
+ this.logger.info("an email received but no code or url found");
1962
+ }
1963
+ }
1964
+ }
1965
+ catch (e) {
1966
+ errorCount++;
1967
+ if (errorCount > 3) {
1968
+ throw e;
1969
+ }
1970
+ // ignore
1971
+ }
1972
+ // check if the timeout is reached
1973
+ if (Date.now() - startTime > timeout) {
1974
+ throw new Error("timeout reached");
1975
+ }
1976
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1977
+ }
1978
+ }
1723
1979
  async _highlightElements(scope, css) {
1724
1980
  try {
1725
1981
  if (!scope) {
@@ -1901,8 +2157,10 @@ class StableBrowser {
1901
2157
  const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
1902
2158
  await this._highlightElements(frame, dataAttribute);
1903
2159
  const element = await frame.$(dataAttribute);
1904
- await this.scrollIfNeeded(element, info);
1905
- await element.dispatchEvent("bvt_verify_page_contains_text");
2160
+ if (element) {
2161
+ await this.scrollIfNeeded(element, info);
2162
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2163
+ }
1906
2164
  }
1907
2165
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1908
2166
  return info;
@@ -1940,6 +2198,16 @@ class StableBrowser {
1940
2198
  });
1941
2199
  }
1942
2200
  }
2201
+ _getServerUrl() {
2202
+ let serviceUrl = "https://api.blinq.io";
2203
+ if (process.env.NODE_ENV_BLINQ === "dev") {
2204
+ serviceUrl = "https://dev.api.blinq.io";
2205
+ }
2206
+ else if (process.env.NODE_ENV_BLINQ === "stage") {
2207
+ serviceUrl = "https://stage.api.blinq.io";
2208
+ }
2209
+ return serviceUrl;
2210
+ }
1943
2211
  async visualVerification(text, options = {}, world = null) {
1944
2212
  const startTime = Date.now();
1945
2213
  let error = null;
@@ -1954,13 +2222,7 @@ class StableBrowser {
1954
2222
  throw new Error("TOKEN is not set");
1955
2223
  }
1956
2224
  try {
1957
- let serviceUrl = "https://api.blinq.io";
1958
- if (process.env.NODE_ENV_BLINQ === "dev") {
1959
- serviceUrl = "https://dev.api.blinq.io";
1960
- }
1961
- else if (process.env.NODE_ENV_BLINQ === "stage") {
1962
- serviceUrl = "https://stage.api.blinq.io";
1963
- }
2225
+ let serviceUrl = this._getServerUrl();
1964
2226
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1965
2227
  info.screenshotPath = screenshotPath;
1966
2228
  const screenshot = await this.takeScreenshot();
@@ -2109,7 +2371,16 @@ class StableBrowser {
2109
2371
  let screenshotId = null;
2110
2372
  let screenshotPath = null;
2111
2373
  const info = {};
2112
- info.log = "***** analyze table " + selectors.element_name + " query " + query + " operator " + operator + " value " + value + " *****\n";
2374
+ info.log =
2375
+ "***** analyze table " +
2376
+ selectors.element_name +
2377
+ " query " +
2378
+ query +
2379
+ " operator " +
2380
+ operator +
2381
+ " value " +
2382
+ value +
2383
+ " *****\n";
2113
2384
  info.operation = "analyzeTable";
2114
2385
  info.selectors = selectors;
2115
2386
  info.query = query;
@@ -2343,13 +2614,6 @@ class StableBrowser {
2343
2614
  const info = {};
2344
2615
  try {
2345
2616
  await this.page.close();
2346
- if (this.context && this.context.pages && this.context.pages.length > 0) {
2347
- this.context.pages.pop();
2348
- this.page = this.context.pages[this.context.pages.length - 1];
2349
- this.context.page = this.page;
2350
- let title = await this.page.title();
2351
- console.log("Switched to page " + title);
2352
- }
2353
2617
  }
2354
2618
  catch (e) {
2355
2619
  console.log(".");