automation_model 1.0.374-dev.0 → 1.0.374-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)) {
@@ -177,21 +188,78 @@ class StableBrowser {
177
188
  }
178
189
  return text;
179
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
+ }
180
216
  _getLocator(locator, scope, _params) {
217
+ locator = this._fixLocatorUsingParams(locator, _params);
218
+ let locatorReturn;
181
219
  if (locator.role) {
182
220
  if (locator.role[1].nameReg) {
183
221
  locator.role[1].name = reg_parser(locator.role[1].nameReg);
184
222
  delete locator.role[1].nameReg;
185
223
  }
186
- if (locator.role[1].name) {
187
- locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
188
- }
189
- 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]);
190
228
  }
191
229
  if (locator.css) {
192
- return scope.locator(this._fixUsingParams(locator.css, _params));
230
+ locatorReturn = scope.locator(locator.css);
193
231
  }
194
- throw new Error("unknown locator type");
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
+ }
243
+ }
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
+ if (!selector.startsWith("[")) {
252
+ selector = `[${selector}]`;
253
+ }
254
+ }
255
+ locatorReturn = scope.locator(`${locator.engine}=${selector}`);
256
+ }
257
+ }
258
+ if (!locatorReturn) {
259
+ console.error(locator);
260
+ throw new Error("Locator undefined");
261
+ }
262
+ return locatorReturn;
195
263
  }
196
264
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
197
265
  let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
@@ -314,7 +382,7 @@ class StableBrowser {
314
382
  }
315
383
  async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
316
384
  let locatorSearch = selectorHierarchy[index];
317
- info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
385
+ //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
318
386
  let locator = null;
319
387
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
320
388
  let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
@@ -342,7 +410,7 @@ class StableBrowser {
342
410
  // cssHref = true;
343
411
  // }
344
412
  let count = await locator.count();
345
- info.log += "total elements found " + count + "\n";
413
+ //info.log += "total elements found " + count + "\n";
346
414
  //let visibleCount = 0;
347
415
  let visibleLocator = null;
348
416
  if (locatorSearch.index && locatorSearch.index < count) {
@@ -352,16 +420,20 @@ class StableBrowser {
352
420
  for (let j = 0; j < count; j++) {
353
421
  let visible = await locator.nth(j).isVisible();
354
422
  const enabled = await locator.nth(j).isEnabled();
355
- info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
356
423
  if (!visibleOnly) {
357
424
  visible = true;
358
425
  }
359
426
  if (visible && enabled) {
360
427
  foundLocators.push(locator.nth(j));
361
- // if (cssHref) {
362
- // info.log += "css href locator found, will ignore all others" + "\n";
363
- // break;
364
- // }
428
+ }
429
+ else {
430
+ if (!info.printMessages) {
431
+ info.printMessages = {};
432
+ }
433
+ if (!info.printMessages[j.toString()]) {
434
+ info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
435
+ info.printMessages[j.toString()] = true;
436
+ }
365
437
  }
366
438
  }
367
439
  }
@@ -407,6 +479,10 @@ class StableBrowser {
407
479
  async _locate(selectors, info, _params, timeout = 30000) {
408
480
  for (let i = 0; i < 3; i++) {
409
481
  info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
482
+ for (let j = 0; j < selectors.locators.length; j++) {
483
+ let selector = selectors.locators[j];
484
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
485
+ }
410
486
  let element = await this._locate_internal(selectors, info, _params, timeout);
411
487
  if (!element.rerun) {
412
488
  return element;
@@ -501,6 +577,7 @@ class StableBrowser {
501
577
  let foundElements = result.foundElements;
502
578
  if (foundElements.length === 1 && foundElements[0].unique) {
503
579
  info.box = foundElements[0].box;
580
+ info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
504
581
  return foundElements[0].locator;
505
582
  }
506
583
  //info.log += "total elements found " + foundElements.length);
@@ -530,6 +607,7 @@ class StableBrowser {
530
607
  }
531
608
  }
532
609
  if (maxCountElement) {
610
+ info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
533
611
  info.box = await maxCountElement.locator.boundingBox();
534
612
  return maxCountElement.locator;
535
613
  }
@@ -538,9 +616,11 @@ class StableBrowser {
538
616
  break;
539
617
  }
540
618
  if (performance.now() - startTime > highPriorityTimeout) {
619
+ info.log += "high priority timeout, will try all elements" + "\n";
541
620
  highPriorityOnly = false;
542
621
  }
543
622
  if (performance.now() - startTime > visibleOnlyTimeout) {
623
+ info.log += "visible only timeout, will try all elements" + "\n";
544
624
  visibleOnly = false;
545
625
  }
546
626
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -584,6 +664,9 @@ class StableBrowser {
584
664
  async click(selectors, _params, options = {}, world = null) {
585
665
  this._validateSelectors(selectors);
586
666
  const startTime = Date.now();
667
+ if (options && options.context) {
668
+ selectors.locators[0].text = options.context;
669
+ }
587
670
  const info = {};
588
671
  info.log = "***** click on " + selectors.element_name + " *****\n";
589
672
  info.operation = "click";
@@ -892,71 +975,45 @@ class StableBrowser {
892
975
  });
893
976
  }
894
977
  }
895
- async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
978
+ async setInputValue(selectors, value, _params = null, options = {}, world = null) {
979
+ // set input value for non fillable inputs like date, time, range, color, etc.
896
980
  this._validateSelectors(selectors);
897
981
  const startTime = Date.now();
898
- let error = null;
899
- let screenshotId = null;
900
- let screenshotPath = null;
901
982
  const info = {};
902
- info.log = "";
903
- info.operation = Types.SET_DATE_TIME;
983
+ info.log = "***** set input value " + selectors.element_name + " *****\n";
984
+ info.operation = "setInputValue";
904
985
  info.selectors = selectors;
986
+ value = this._fixUsingParams(value, _params);
905
987
  info.value = value;
988
+ let error = null;
989
+ let screenshotId = null;
990
+ let screenshotPath = null;
906
991
  try {
907
992
  value = await this._replaceWithLocalData(value, this);
908
993
  let element = await this._locate(selectors, info, _params);
909
- //insert red border around the element
910
994
  await this.scrollIfNeeded(element, info);
911
995
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
912
996
  await this._highlightElements(element);
913
997
  try {
914
- await element.click();
915
- await new Promise((resolve) => setTimeout(resolve, 500));
916
- if (format) {
917
- value = dayjs(value).format(format);
918
- await element.fill(value);
919
- }
920
- else {
921
- const dateTimeValue = await getDateTimeValue({ value, element });
922
- await element.evaluateHandle((el, dateTimeValue) => {
923
- el.value = ""; // clear input
924
- el.value = dateTimeValue;
925
- }, dateTimeValue);
926
- }
927
- if (enter) {
928
- await new Promise((resolve) => setTimeout(resolve, 2000));
929
- await this.page.keyboard.press("Enter");
930
- await this.waitForPageLoad();
931
- }
998
+ await element.evaluateHandle((el, value) => {
999
+ el.value = value;
1000
+ }, value);
932
1001
  }
933
1002
  catch (error) {
934
- //await this.closeUnexpectedPopups();
935
- this.logger.error("setting date time input failed " + JSON.stringify(info));
936
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
1003
+ this.logger.error("setInputValue failed, will try again");
1004
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
937
1005
  info.screenshotPath = screenshotPath;
938
1006
  Object.assign(error, { info: info });
939
- await element.click();
940
- await new Promise((resolve) => setTimeout(resolve, 500));
941
- if (format) {
942
- value = dayjs(value).format(format);
943
- await element.fill(value);
944
- }
945
- else {
946
- const dateTimeValue = await getDateTimeValue({ value, element });
947
- await element.evaluateHandle((el, dateTimeValue) => {
948
- el.value = ""; // clear input
949
- el.value = dateTimeValue;
950
- }, dateTimeValue);
951
- }
952
- if (enter) {
953
- await new Promise((resolve) => setTimeout(resolve, 2000));
954
- await this.page.keyboard.press("Enter");
955
- await this.waitForPageLoad();
956
- }
1007
+ await element.evaluateHandle((el, value) => {
1008
+ el.value = value;
1009
+ });
957
1010
  }
958
1011
  }
959
- catch (error) {
1012
+ catch (e) {
1013
+ this.logger.error("setInputValue failed " + info.log);
1014
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1015
+ info.screenshotPath = screenshotPath;
1016
+ Object.assign(e, { info: info });
960
1017
  error = e;
961
1018
  throw e;
962
1019
  }
@@ -964,10 +1021,10 @@ class StableBrowser {
964
1021
  const endTime = Date.now();
965
1022
  this._reportToWorld(world, {
966
1023
  element_name: selectors.element_name,
967
- type: Types.SET_DATE_TIME,
968
- screenshotId,
1024
+ type: Types.SET_INPUT,
1025
+ text: `Set input value`,
969
1026
  value: value,
970
- text: `setDateTime input with value: ${value}`,
1027
+ screenshotId,
971
1028
  result: error
972
1029
  ? {
973
1030
  status: "FAILED",
@@ -984,7 +1041,7 @@ class StableBrowser {
984
1041
  });
985
1042
  }
986
1043
  }
987
- async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
1044
+ async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
988
1045
  this._validateSelectors(selectors);
989
1046
  const startTime = Date.now();
990
1047
  let error = null;
@@ -996,6 +1053,7 @@ class StableBrowser {
996
1053
  info.selectors = selectors;
997
1054
  info.value = value;
998
1055
  try {
1056
+ value = await this._replaceWithLocalData(value, this);
999
1057
  let element = await this._locate(selectors, info, _params);
1000
1058
  //insert red border around the element
1001
1059
  await this.scrollIfNeeded(element, info);
@@ -1004,28 +1062,51 @@ class StableBrowser {
1004
1062
  try {
1005
1063
  await element.click();
1006
1064
  await new Promise((resolve) => setTimeout(resolve, 500));
1007
- const dateTimeValue = await getDateTimeValue({ value, element });
1008
- await element.evaluateHandle((el, dateTimeValue) => {
1009
- el.value = ""; // clear input
1010
- el.value = dateTimeValue;
1011
- }, dateTimeValue);
1065
+ if (format) {
1066
+ value = dayjs(value).format(format);
1067
+ await element.fill(value);
1068
+ }
1069
+ else {
1070
+ const dateTimeValue = await getDateTimeValue({ value, element });
1071
+ await element.evaluateHandle((el, dateTimeValue) => {
1072
+ el.value = ""; // clear input
1073
+ el.value = dateTimeValue;
1074
+ }, dateTimeValue);
1075
+ }
1076
+ if (enter) {
1077
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1078
+ await this.page.keyboard.press("Enter");
1079
+ await this.waitForPageLoad();
1080
+ }
1012
1081
  }
1013
- catch (error) {
1082
+ catch (err) {
1014
1083
  //await this.closeUnexpectedPopups();
1015
1084
  this.logger.error("setting date time input failed " + JSON.stringify(info));
1016
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
1085
+ this.logger.info("Trying again");
1086
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1017
1087
  info.screenshotPath = screenshotPath;
1018
- Object.assign(error, { info: info });
1088
+ Object.assign(err, { info: info });
1019
1089
  await element.click();
1020
1090
  await new Promise((resolve) => setTimeout(resolve, 500));
1021
- const dateTimeValue = await getDateTimeValue({ value, element });
1022
- await element.evaluateHandle((el, dateTimeValue) => {
1023
- el.value = ""; // clear input
1024
- el.value = dateTimeValue;
1025
- }, dateTimeValue);
1091
+ if (format) {
1092
+ value = dayjs(value).format(format);
1093
+ await element.fill(value);
1094
+ }
1095
+ else {
1096
+ const dateTimeValue = await getDateTimeValue({ value, element });
1097
+ await element.evaluateHandle((el, dateTimeValue) => {
1098
+ el.value = ""; // clear input
1099
+ el.value = dateTimeValue;
1100
+ }, dateTimeValue);
1101
+ }
1102
+ if (enter) {
1103
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1104
+ await this.page.keyboard.press("Enter");
1105
+ await this.waitForPageLoad();
1106
+ }
1026
1107
  }
1027
1108
  }
1028
- catch (error) {
1109
+ catch (e) {
1029
1110
  error = e;
1030
1111
  throw e;
1031
1112
  }
@@ -1075,20 +1156,32 @@ class StableBrowser {
1075
1156
  await this.scrollIfNeeded(element, info);
1076
1157
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1077
1158
  await this._highlightElements(element);
1078
- try {
1079
- let currentValue = await element.inputValue();
1080
- if (currentValue) {
1081
- await element.fill("");
1159
+ if (options === null || options === undefined || !options.press) {
1160
+ try {
1161
+ let currentValue = await element.inputValue();
1162
+ if (currentValue) {
1163
+ await element.fill("");
1164
+ }
1165
+ }
1166
+ catch (e) {
1167
+ this.logger.info("unable to clear input value");
1082
1168
  }
1083
1169
  }
1084
- catch (e) {
1085
- this.logger.info("unable to clear input value");
1086
- }
1087
- try {
1088
- await element.click({ timeout: 5000 });
1170
+ if (options === null || options === undefined || options.press) {
1171
+ try {
1172
+ await element.click({ timeout: 5000 });
1173
+ }
1174
+ catch (e) {
1175
+ await element.dispatchEvent("click");
1176
+ }
1089
1177
  }
1090
- catch (e) {
1091
- await element.dispatchEvent("click");
1178
+ else {
1179
+ try {
1180
+ await element.focus();
1181
+ }
1182
+ catch (e) {
1183
+ await element.dispatchEvent("focus");
1184
+ }
1092
1185
  }
1093
1186
  await new Promise((resolve) => setTimeout(resolve, 500));
1094
1187
  const valueSegment = _value.split("&&");
@@ -1284,7 +1377,8 @@ class StableBrowser {
1284
1377
  let screenshotId = null;
1285
1378
  let screenshotPath = null;
1286
1379
  const info = {};
1287
- info.log = "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1380
+ info.log =
1381
+ "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1288
1382
  info.operation = "containsPattern";
1289
1383
  info.selectors = selectors;
1290
1384
  info.value = text;
@@ -1394,7 +1488,7 @@ class StableBrowser {
1394
1488
  return info;
1395
1489
  }
1396
1490
  catch (e) {
1397
- //await this.closeUnexpectedPopups();
1491
+ await this.closeUnexpectedPopups();
1398
1492
  this.logger.error("verify element contains text failed " + info.log);
1399
1493
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1400
1494
  info.screenshotPath = screenshotPath;
@@ -1454,15 +1548,62 @@ class StableBrowser {
1454
1548
  // save the data to the file
1455
1549
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1456
1550
  }
1551
+ _getDataFilePath(fileName) {
1552
+ let dataFile = path.join(this.project_path, "data", fileName);
1553
+ if (fs.existsSync(dataFile)) {
1554
+ return dataFile;
1555
+ }
1556
+ dataFile = path.join(this.project_path, fileName);
1557
+ if (fs.existsSync(dataFile)) {
1558
+ return dataFile;
1559
+ }
1560
+ throw new Error("data file not found " + fileName);
1561
+ }
1562
+ _parseCSVSync(filePath) {
1563
+ const data = fs.readFileSync(filePath, "utf8");
1564
+ const results = [];
1565
+ return new Promise((resolve, reject) => {
1566
+ const readableStream = new Readable();
1567
+ readableStream._read = () => { }; // _read is required but you can noop it
1568
+ readableStream.push(data);
1569
+ readableStream.push(null);
1570
+ readableStream
1571
+ .pipe(csv())
1572
+ .on("data", (data) => results.push(data))
1573
+ .on("end", () => resolve(results))
1574
+ .on("error", (error) => reject(error));
1575
+ });
1576
+ }
1457
1577
  loadTestData(type, dataSelector, world = null) {
1458
1578
  switch (type) {
1459
1579
  case "users":
1460
- // check if file users.json exists
1461
- if (!fs.existsSync(path.join(this.project_path, "users.json"))) {
1462
- throw new Error("users.json file not found");
1580
+ // get the users.json file path
1581
+ let dataFile = this._getDataFilePath("users.json");
1582
+ // read the file and return the data
1583
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1584
+ for (let i = 0; i < users.length; i++) {
1585
+ if (users[i].username === dataSelector) {
1586
+ const userObj = {
1587
+ username: users[i].username,
1588
+ password: "secret:" + users[i].password,
1589
+ totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
1590
+ };
1591
+ this.setTestData(userObj, world);
1592
+ return userObj;
1593
+ }
1463
1594
  }
1595
+ throw new Error("user not found " + dataSelector);
1596
+ default:
1597
+ throw new Error("unknown type " + type);
1598
+ }
1599
+ }
1600
+ async loadTestDataAsync(type, dataSelector, world = null) {
1601
+ switch (type) {
1602
+ case "users": {
1603
+ // get the users.json file path
1604
+ let dataFile = this._getDataFilePath("users.json");
1464
1605
  // read the file and return the data
1465
- const users = JSON.parse(fs.readFileSync(path.join(this.project_path, "users.json"), "utf8"));
1606
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1466
1607
  for (let i = 0; i < users.length; i++) {
1467
1608
  if (users[i].username === dataSelector) {
1468
1609
  const userObj = {
@@ -1475,6 +1616,29 @@ class StableBrowser {
1475
1616
  }
1476
1617
  }
1477
1618
  throw new Error("user not found " + dataSelector);
1619
+ }
1620
+ case "csv": {
1621
+ // 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
1622
+ const parts = dataSelector.split(":");
1623
+ let rowNumber = 0;
1624
+ if (parts.length > 1) {
1625
+ rowNumber = parseInt(parts[1]);
1626
+ }
1627
+ let dataFile = this._getDataFilePath(parts[0]);
1628
+ const results = await this._parseCSVSync(dataFile);
1629
+ // result stracture:
1630
+ // [
1631
+ // { NAME: 'Daffy Duck', AGE: '24' },
1632
+ // { NAME: 'Bugs Bunny', AGE: '22' }
1633
+ // ]
1634
+ // verify the row number is within the range
1635
+ if (rowNumber >= results.length) {
1636
+ throw new Error("row number is out of range " + rowNumber);
1637
+ }
1638
+ const data = results[rowNumber];
1639
+ this.setTestData(data, world);
1640
+ return data;
1641
+ }
1478
1642
  default:
1479
1643
  throw new Error("unknown type " + type);
1480
1644
  }
@@ -1579,13 +1743,13 @@ class StableBrowser {
1579
1743
  ])));
1580
1744
  const { data } = await client.send("Page.captureScreenshot", {
1581
1745
  format: "png",
1582
- clip: {
1583
- x: 0,
1584
- y: 0,
1585
- width: viewportWidth,
1586
- height: viewportHeight,
1587
- scale: 1,
1588
- },
1746
+ // clip: {
1747
+ // x: 0,
1748
+ // y: 0,
1749
+ // width: viewportWidth,
1750
+ // height: viewportHeight,
1751
+ // scale: 1,
1752
+ // },
1589
1753
  });
1590
1754
  if (!screenshotPath) {
1591
1755
  return data;
@@ -1691,7 +1855,8 @@ class StableBrowser {
1691
1855
  if (world) {
1692
1856
  world[variable] = info.value;
1693
1857
  }
1694
- this.logger.info("world." + variable + "=" + info.value);
1858
+ this.setTestData({ [variable]: info.value }, world);
1859
+ this.logger.info("set test data: " + variable + "=" + info.value);
1695
1860
  return info;
1696
1861
  }
1697
1862
  catch (e) {
@@ -1728,6 +1893,91 @@ class StableBrowser {
1728
1893
  });
1729
1894
  }
1730
1895
  }
1896
+ async extractEmailData(emailAddress, options, world) {
1897
+ if (!emailAddress) {
1898
+ throw new Error("email address is null");
1899
+ }
1900
+ // check if address contain @
1901
+ if (emailAddress.indexOf("@") === -1) {
1902
+ emailAddress = emailAddress + "@blinq-mail.io";
1903
+ }
1904
+ else {
1905
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1906
+ throw new Error("email address should end with @blinq-mail.io");
1907
+ }
1908
+ }
1909
+ const startTime = Date.now();
1910
+ let timeout = 60000;
1911
+ if (options && options.timeout) {
1912
+ timeout = options.timeout;
1913
+ }
1914
+ const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1915
+ const request = {
1916
+ method: "POST",
1917
+ url: serviceUrl,
1918
+ headers: {
1919
+ "Content-Type": "application/json",
1920
+ Authorization: `Bearer ${process.env.TOKEN}`,
1921
+ },
1922
+ data: JSON.stringify({
1923
+ email: emailAddress,
1924
+ }),
1925
+ };
1926
+ let errorCount = 0;
1927
+ while (true) {
1928
+ try {
1929
+ let result = await this.context.api.request(request);
1930
+ // the response body expected to be the following:
1931
+ // {
1932
+ // "status": true,
1933
+ // "content": {
1934
+ // "url": "",
1935
+ // "code": "112112",
1936
+ // "name": "generate_link_or_code"
1937
+ // }
1938
+ //}
1939
+ if ((result && result.data, result.data.status === true)) {
1940
+ let codeOrUrlFound = false;
1941
+ let emailCode = null;
1942
+ let emailUrl = null;
1943
+ // check if a code is returned
1944
+ if (result.data.content && result.data.content.code) {
1945
+ let code = result.data.content.code;
1946
+ this.setTestData({ emailCode: code }, world);
1947
+ this.logger.info("set test data: emailCode = " + code);
1948
+ emailCode = code;
1949
+ codeOrUrlFound = true;
1950
+ }
1951
+ // check if a url is returned
1952
+ if (result.data.content && result.data.content.url) {
1953
+ let url = result.data.content.url;
1954
+ this.setTestData({ emailUrl: url }, world);
1955
+ this.logger.info("set test data: emailUrl = " + url);
1956
+ emailUrl = url;
1957
+ codeOrUrlFound = true;
1958
+ }
1959
+ if (codeOrUrlFound) {
1960
+ return { emailUrl, emailCode };
1961
+ }
1962
+ else {
1963
+ this.logger.info("an email received but no code or url found");
1964
+ }
1965
+ }
1966
+ }
1967
+ catch (e) {
1968
+ errorCount++;
1969
+ if (errorCount > 3) {
1970
+ throw e;
1971
+ }
1972
+ // ignore
1973
+ }
1974
+ // check if the timeout is reached
1975
+ if (Date.now() - startTime > timeout) {
1976
+ throw new Error("timeout reached");
1977
+ }
1978
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1979
+ }
1980
+ }
1731
1981
  async _highlightElements(scope, css) {
1732
1982
  try {
1733
1983
  if (!scope) {
@@ -1909,8 +2159,10 @@ class StableBrowser {
1909
2159
  const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
1910
2160
  await this._highlightElements(frame, dataAttribute);
1911
2161
  const element = await frame.$(dataAttribute);
1912
- await this.scrollIfNeeded(element, info);
1913
- await element.dispatchEvent("bvt_verify_page_contains_text");
2162
+ if (element) {
2163
+ await this.scrollIfNeeded(element, info);
2164
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2165
+ }
1914
2166
  }
1915
2167
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1916
2168
  return info;
@@ -1948,6 +2200,16 @@ class StableBrowser {
1948
2200
  });
1949
2201
  }
1950
2202
  }
2203
+ _getServerUrl() {
2204
+ let serviceUrl = "https://api.blinq.io";
2205
+ if (process.env.NODE_ENV_BLINQ === "dev") {
2206
+ serviceUrl = "https://dev.api.blinq.io";
2207
+ }
2208
+ else if (process.env.NODE_ENV_BLINQ === "stage") {
2209
+ serviceUrl = "https://stage.api.blinq.io";
2210
+ }
2211
+ return serviceUrl;
2212
+ }
1951
2213
  async visualVerification(text, options = {}, world = null) {
1952
2214
  const startTime = Date.now();
1953
2215
  let error = null;
@@ -1962,13 +2224,7 @@ class StableBrowser {
1962
2224
  throw new Error("TOKEN is not set");
1963
2225
  }
1964
2226
  try {
1965
- let serviceUrl = "https://api.blinq.io";
1966
- if (process.env.NODE_ENV_BLINQ === "dev") {
1967
- serviceUrl = "https://dev.api.blinq.io";
1968
- }
1969
- else if (process.env.NODE_ENV_BLINQ === "stage") {
1970
- serviceUrl = "https://stage.api.blinq.io";
1971
- }
2227
+ let serviceUrl = this._getServerUrl();
1972
2228
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1973
2229
  info.screenshotPath = screenshotPath;
1974
2230
  const screenshot = await this.takeScreenshot();
@@ -2117,7 +2373,16 @@ class StableBrowser {
2117
2373
  let screenshotId = null;
2118
2374
  let screenshotPath = null;
2119
2375
  const info = {};
2120
- info.log = "***** analyze table " + selectors.element_name + " query " + query + " operator " + operator + " value " + value + " *****\n";
2376
+ info.log =
2377
+ "***** analyze table " +
2378
+ selectors.element_name +
2379
+ " query " +
2380
+ query +
2381
+ " operator " +
2382
+ operator +
2383
+ " value " +
2384
+ value +
2385
+ " *****\n";
2121
2386
  info.operation = "analyzeTable";
2122
2387
  info.selectors = selectors;
2123
2388
  info.query = query;
@@ -2351,13 +2616,6 @@ class StableBrowser {
2351
2616
  const info = {};
2352
2617
  try {
2353
2618
  await this.page.close();
2354
- if (this.context && this.context.pages && this.context.pages.length > 0) {
2355
- this.context.pages.pop();
2356
- this.page = this.context.pages[this.context.pages.length - 1];
2357
- this.context.page = this.page;
2358
- let title = await this.page.title();
2359
- console.log("Switched to page " + title);
2360
- }
2361
2619
  }
2362
2620
  catch (e) {
2363
2621
  console.log(".");