automation_model 1.0.367-dev.0 → 1.0.367-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,23 +179,81 @@ 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
+ if (locator.type === "pw_selector") {
219
+ return scope.locator(locator.selector);
220
+ }
176
221
  if (locator.role) {
177
222
  if (locator.role[1].nameReg) {
178
223
  locator.role[1].name = reg_parser(locator.role[1].nameReg);
179
224
  delete locator.role[1].nameReg;
180
225
  }
181
- if (locator.role[1].name) {
182
- locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
183
- }
226
+ // if (locator.role[1].name) {
227
+ // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
228
+ // }
184
229
  return scope.getByRole(locator.role[0], locator.role[1]);
185
230
  }
186
231
  if (locator.css) {
187
- return scope.locator(this._fixUsingParams(locator.css, _params));
232
+ return scope.locator(locator.css);
233
+ }
234
+ // handle role/name locators
235
+ // locator.selector will be something like: textbox[name="Username"i]
236
+ if (locator.engine === "internal:role") {
237
+ // extract the role, name and the i/s flags using regex
238
+ const match = locator.selector.match(/(.*)\[(.*)="(.*)"(.*)\]/);
239
+ if (match) {
240
+ const role = match[1];
241
+ const name = match[3];
242
+ const flags = match[4];
243
+ return scope.getByRole(role, { name }, { exact: flags === "i" });
244
+ }
245
+ }
246
+ // handle css locators
247
+ if (locator.engine === "css") {
248
+ return scope.locator(locator.selector);
249
+ }
250
+ if ((locator === null || locator === void 0 ? void 0 : locator.engine) && (locator === null || locator === void 0 ? void 0 : locator.score) <= 520) {
251
+ let selector = locator.selector.replace(/"/g, '\\"');
252
+ if (locator.engine === "internal:att") {
253
+ selector = `[${selector}]`;
254
+ }
255
+ const newLocator = scope.locator(`${locator.engine}="${selector}"`);
256
+ return newLocator;
188
257
  }
189
258
  throw new Error("unknown locator type");
190
259
  }
@@ -309,10 +378,13 @@ class StableBrowser {
309
378
  }
310
379
  async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
311
380
  let locatorSearch = selectorHierarchy[index];
312
- info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
381
+ //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
313
382
  let locator = null;
314
383
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
315
384
  let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
385
+ if (!locatorString) {
386
+ return;
387
+ }
316
388
  locator = this._getLocator({ css: locatorString }, scope, _params);
317
389
  }
318
390
  else if (locatorSearch.text) {
@@ -334,7 +406,7 @@ class StableBrowser {
334
406
  // cssHref = true;
335
407
  // }
336
408
  let count = await locator.count();
337
- info.log += "total elements found " + count + "\n";
409
+ //info.log += "total elements found " + count + "\n";
338
410
  //let visibleCount = 0;
339
411
  let visibleLocator = null;
340
412
  if (locatorSearch.index && locatorSearch.index < count) {
@@ -344,16 +416,20 @@ class StableBrowser {
344
416
  for (let j = 0; j < count; j++) {
345
417
  let visible = await locator.nth(j).isVisible();
346
418
  const enabled = await locator.nth(j).isEnabled();
347
- info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
348
419
  if (!visibleOnly) {
349
420
  visible = true;
350
421
  }
351
422
  if (visible && enabled) {
352
423
  foundLocators.push(locator.nth(j));
353
- // if (cssHref) {
354
- // info.log += "css href locator found, will ignore all others" + "\n";
355
- // break;
356
- // }
424
+ }
425
+ else {
426
+ if (!info.printMessages) {
427
+ info.printMessages = {};
428
+ }
429
+ if (!info.printMessages[j.toString()]) {
430
+ info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
431
+ info.printMessages[j.toString()] = true;
432
+ }
357
433
  }
358
434
  }
359
435
  }
@@ -399,6 +475,10 @@ class StableBrowser {
399
475
  async _locate(selectors, info, _params, timeout = 30000) {
400
476
  for (let i = 0; i < 3; i++) {
401
477
  info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
478
+ for (let j = 0; j < selectors.locators.length; j++) {
479
+ let selector = selectors.locators[j];
480
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
481
+ }
402
482
  let element = await this._locate_internal(selectors, info, _params, timeout);
403
483
  if (!element.rerun) {
404
484
  return element;
@@ -482,14 +562,18 @@ class StableBrowser {
482
562
  // info.log += "scanning locators in priority 2" + "\n";
483
563
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
484
564
  }
485
- let shouldNotTestPriority3 = highPriorityOnly && !onlyPriority3;
486
- if (result.foundElements.length === 0 && shouldTestPriority3) {
487
- // info.log += "scanning locators in priority 3" + "\n";
565
+ if (result.foundElements.length === 0 && onlyPriority3) {
488
566
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
489
567
  }
568
+ else {
569
+ if (result.foundElements.length === 0 && !highPriorityOnly) {
570
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
571
+ }
572
+ }
490
573
  let foundElements = result.foundElements;
491
574
  if (foundElements.length === 1 && foundElements[0].unique) {
492
575
  info.box = foundElements[0].box;
576
+ info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
493
577
  return foundElements[0].locator;
494
578
  }
495
579
  //info.log += "total elements found " + foundElements.length);
@@ -519,6 +603,7 @@ class StableBrowser {
519
603
  }
520
604
  }
521
605
  if (maxCountElement) {
606
+ info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
522
607
  info.box = await maxCountElement.locator.boundingBox();
523
608
  return maxCountElement.locator;
524
609
  }
@@ -527,9 +612,11 @@ class StableBrowser {
527
612
  break;
528
613
  }
529
614
  if (performance.now() - startTime > highPriorityTimeout) {
615
+ info.log += "high priority timeout, will try all elements" + "\n";
530
616
  highPriorityOnly = false;
531
617
  }
532
618
  if (performance.now() - startTime > visibleOnlyTimeout) {
619
+ info.log += "visible only timeout, will try all elements" + "\n";
533
620
  visibleOnly = false;
534
621
  }
535
622
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -881,71 +968,45 @@ class StableBrowser {
881
968
  });
882
969
  }
883
970
  }
884
- async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
971
+ async setInputValue(selectors, value, _params = null, options = {}, world = null) {
972
+ // set input value for non fillable inputs like date, time, range, color, etc.
885
973
  this._validateSelectors(selectors);
886
974
  const startTime = Date.now();
887
- let error = null;
888
- let screenshotId = null;
889
- let screenshotPath = null;
890
975
  const info = {};
891
- info.log = "";
892
- info.operation = Types.SET_DATE_TIME;
976
+ info.log = "***** set input value " + selectors.element_name + " *****\n";
977
+ info.operation = "setInputValue";
893
978
  info.selectors = selectors;
979
+ value = this._fixUsingParams(value, _params);
894
980
  info.value = value;
981
+ let error = null;
982
+ let screenshotId = null;
983
+ let screenshotPath = null;
895
984
  try {
896
985
  value = await this._replaceWithLocalData(value, this);
897
986
  let element = await this._locate(selectors, info, _params);
898
- //insert red border around the element
899
987
  await this.scrollIfNeeded(element, info);
900
988
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
901
989
  await this._highlightElements(element);
902
990
  try {
903
- await element.click();
904
- await new Promise((resolve) => setTimeout(resolve, 500));
905
- if (format) {
906
- value = dayjs(value).format(format);
907
- await element.fill(value);
908
- }
909
- else {
910
- const dateTimeValue = await getDateTimeValue({ value, element });
911
- await element.evaluateHandle((el, dateTimeValue) => {
912
- el.value = ""; // clear input
913
- el.value = dateTimeValue;
914
- }, dateTimeValue);
915
- }
916
- if (enter) {
917
- await new Promise((resolve) => setTimeout(resolve, 2000));
918
- await this.page.keyboard.press("Enter");
919
- await this.waitForPageLoad();
920
- }
991
+ await element.evaluateHandle((el, value) => {
992
+ el.value = value;
993
+ }, value);
921
994
  }
922
995
  catch (error) {
923
- //await this.closeUnexpectedPopups();
924
- this.logger.error("setting date time input failed " + JSON.stringify(info));
925
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
996
+ this.logger.error("setInputValue failed, will try again");
997
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
926
998
  info.screenshotPath = screenshotPath;
927
999
  Object.assign(error, { info: info });
928
- await element.click();
929
- await new Promise((resolve) => setTimeout(resolve, 500));
930
- if (format) {
931
- value = dayjs(value).format(format);
932
- await element.fill(value);
933
- }
934
- else {
935
- const dateTimeValue = await getDateTimeValue({ value, element });
936
- await element.evaluateHandle((el, dateTimeValue) => {
937
- el.value = ""; // clear input
938
- el.value = dateTimeValue;
939
- }, dateTimeValue);
940
- }
941
- if (enter) {
942
- await new Promise((resolve) => setTimeout(resolve, 2000));
943
- await this.page.keyboard.press("Enter");
944
- await this.waitForPageLoad();
945
- }
1000
+ await element.evaluateHandle((el, value) => {
1001
+ el.value = value;
1002
+ });
946
1003
  }
947
1004
  }
948
- catch (error) {
1005
+ catch (e) {
1006
+ this.logger.error("setInputValue failed " + info.log);
1007
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1008
+ info.screenshotPath = screenshotPath;
1009
+ Object.assign(e, { info: info });
949
1010
  error = e;
950
1011
  throw e;
951
1012
  }
@@ -953,10 +1014,10 @@ class StableBrowser {
953
1014
  const endTime = Date.now();
954
1015
  this._reportToWorld(world, {
955
1016
  element_name: selectors.element_name,
956
- type: Types.SET_DATE_TIME,
957
- screenshotId,
1017
+ type: Types.SET_INPUT,
1018
+ text: `Set input value`,
958
1019
  value: value,
959
- text: `setDateTime input with value: ${value}`,
1020
+ screenshotId,
960
1021
  result: error
961
1022
  ? {
962
1023
  status: "FAILED",
@@ -973,7 +1034,7 @@ class StableBrowser {
973
1034
  });
974
1035
  }
975
1036
  }
976
- async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
1037
+ async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
977
1038
  this._validateSelectors(selectors);
978
1039
  const startTime = Date.now();
979
1040
  let error = null;
@@ -985,6 +1046,7 @@ class StableBrowser {
985
1046
  info.selectors = selectors;
986
1047
  info.value = value;
987
1048
  try {
1049
+ value = await this._replaceWithLocalData(value, this);
988
1050
  let element = await this._locate(selectors, info, _params);
989
1051
  //insert red border around the element
990
1052
  await this.scrollIfNeeded(element, info);
@@ -993,28 +1055,51 @@ class StableBrowser {
993
1055
  try {
994
1056
  await element.click();
995
1057
  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);
1058
+ if (format) {
1059
+ value = dayjs(value).format(format);
1060
+ await element.fill(value);
1061
+ }
1062
+ else {
1063
+ const dateTimeValue = await getDateTimeValue({ value, element });
1064
+ await element.evaluateHandle((el, dateTimeValue) => {
1065
+ el.value = ""; // clear input
1066
+ el.value = dateTimeValue;
1067
+ }, dateTimeValue);
1068
+ }
1069
+ if (enter) {
1070
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1071
+ await this.page.keyboard.press("Enter");
1072
+ await this.waitForPageLoad();
1073
+ }
1001
1074
  }
1002
- catch (error) {
1075
+ catch (err) {
1003
1076
  //await this.closeUnexpectedPopups();
1004
1077
  this.logger.error("setting date time input failed " + JSON.stringify(info));
1005
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
1078
+ this.logger.info("Trying again");
1079
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1006
1080
  info.screenshotPath = screenshotPath;
1007
- Object.assign(error, { info: info });
1081
+ Object.assign(err, { info: info });
1008
1082
  await element.click();
1009
1083
  await new Promise((resolve) => setTimeout(resolve, 500));
1010
- const dateTimeValue = await getDateTimeValue({ value, element });
1011
- await element.evaluateHandle((el, dateTimeValue) => {
1012
- el.value = ""; // clear input
1013
- el.value = dateTimeValue;
1014
- }, dateTimeValue);
1084
+ if (format) {
1085
+ value = dayjs(value).format(format);
1086
+ await element.fill(value);
1087
+ }
1088
+ else {
1089
+ const dateTimeValue = await getDateTimeValue({ value, element });
1090
+ await element.evaluateHandle((el, dateTimeValue) => {
1091
+ el.value = ""; // clear input
1092
+ el.value = dateTimeValue;
1093
+ }, dateTimeValue);
1094
+ }
1095
+ if (enter) {
1096
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1097
+ await this.page.keyboard.press("Enter");
1098
+ await this.waitForPageLoad();
1099
+ }
1015
1100
  }
1016
1101
  }
1017
- catch (error) {
1102
+ catch (e) {
1018
1103
  error = e;
1019
1104
  throw e;
1020
1105
  }
@@ -1064,20 +1149,32 @@ class StableBrowser {
1064
1149
  await this.scrollIfNeeded(element, info);
1065
1150
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1066
1151
  await this._highlightElements(element);
1067
- try {
1068
- let currentValue = await element.inputValue();
1069
- if (currentValue) {
1070
- await element.fill("");
1152
+ if (options === null || options === undefined || !options.press) {
1153
+ try {
1154
+ let currentValue = await element.inputValue();
1155
+ if (currentValue) {
1156
+ await element.fill("");
1157
+ }
1158
+ }
1159
+ catch (e) {
1160
+ this.logger.info("unable to clear input value");
1071
1161
  }
1072
1162
  }
1073
- catch (e) {
1074
- this.logger.info("unable to clear input value");
1075
- }
1076
- try {
1077
- await element.click({ timeout: 5000 });
1163
+ if (options === null || options === undefined || options.press) {
1164
+ try {
1165
+ await element.click({ timeout: 5000 });
1166
+ }
1167
+ catch (e) {
1168
+ await element.dispatchEvent("click");
1169
+ }
1078
1170
  }
1079
- catch (e) {
1080
- await element.dispatchEvent("click");
1171
+ else {
1172
+ try {
1173
+ await element.focus();
1174
+ }
1175
+ catch (e) {
1176
+ await element.dispatchEvent("focus");
1177
+ }
1081
1178
  }
1082
1179
  await new Promise((resolve) => setTimeout(resolve, 500));
1083
1180
  const valueSegment = _value.split("&&");
@@ -1273,7 +1370,8 @@ class StableBrowser {
1273
1370
  let screenshotId = null;
1274
1371
  let screenshotPath = null;
1275
1372
  const info = {};
1276
- info.log = "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1373
+ info.log =
1374
+ "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1277
1375
  info.operation = "containsPattern";
1278
1376
  info.selectors = selectors;
1279
1377
  info.value = text;
@@ -1443,15 +1541,62 @@ class StableBrowser {
1443
1541
  // save the data to the file
1444
1542
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1445
1543
  }
1544
+ _getDataFilePath(fileName) {
1545
+ let dataFile = path.join(this.project_path, "data", fileName);
1546
+ if (fs.existsSync(dataFile)) {
1547
+ return dataFile;
1548
+ }
1549
+ dataFile = path.join(this.project_path, fileName);
1550
+ if (fs.existsSync(dataFile)) {
1551
+ return dataFile;
1552
+ }
1553
+ throw new Error("data file not found " + fileName);
1554
+ }
1555
+ _parseCSVSync(filePath) {
1556
+ const data = fs.readFileSync(filePath, "utf8");
1557
+ const results = [];
1558
+ return new Promise((resolve, reject) => {
1559
+ const readableStream = new Readable();
1560
+ readableStream._read = () => { }; // _read is required but you can noop it
1561
+ readableStream.push(data);
1562
+ readableStream.push(null);
1563
+ readableStream
1564
+ .pipe(csv())
1565
+ .on("data", (data) => results.push(data))
1566
+ .on("end", () => resolve(results))
1567
+ .on("error", (error) => reject(error));
1568
+ });
1569
+ }
1446
1570
  loadTestData(type, dataSelector, world = null) {
1447
1571
  switch (type) {
1448
1572
  case "users":
1449
- // check if file users.json exists
1450
- if (!fs.existsSync(path.join(this.project_path, "users.json"))) {
1451
- throw new Error("users.json file not found");
1573
+ // get the users.json file path
1574
+ let dataFile = this._getDataFilePath("users.json");
1575
+ // read the file and return the data
1576
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1577
+ for (let i = 0; i < users.length; i++) {
1578
+ if (users[i].username === dataSelector) {
1579
+ const userObj = {
1580
+ username: users[i].username,
1581
+ password: "secret:" + users[i].password,
1582
+ totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
1583
+ };
1584
+ this.setTestData(userObj, world);
1585
+ return userObj;
1586
+ }
1452
1587
  }
1588
+ throw new Error("user not found " + dataSelector);
1589
+ default:
1590
+ throw new Error("unknown type " + type);
1591
+ }
1592
+ }
1593
+ async loadTestDataAsync(type, dataSelector, world = null) {
1594
+ switch (type) {
1595
+ case "users": {
1596
+ // get the users.json file path
1597
+ let dataFile = this._getDataFilePath("users.json");
1453
1598
  // read the file and return the data
1454
- const users = JSON.parse(fs.readFileSync(path.join(this.project_path, "users.json"), "utf8"));
1599
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1455
1600
  for (let i = 0; i < users.length; i++) {
1456
1601
  if (users[i].username === dataSelector) {
1457
1602
  const userObj = {
@@ -1464,6 +1609,29 @@ class StableBrowser {
1464
1609
  }
1465
1610
  }
1466
1611
  throw new Error("user not found " + dataSelector);
1612
+ }
1613
+ case "csv": {
1614
+ // 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
1615
+ const parts = dataSelector.split(":");
1616
+ let rowNumber = 0;
1617
+ if (parts.length > 1) {
1618
+ rowNumber = parseInt(parts[1]);
1619
+ }
1620
+ let dataFile = this._getDataFilePath(parts[0]);
1621
+ const results = await this._parseCSVSync(dataFile);
1622
+ // result stracture:
1623
+ // [
1624
+ // { NAME: 'Daffy Duck', AGE: '24' },
1625
+ // { NAME: 'Bugs Bunny', AGE: '22' }
1626
+ // ]
1627
+ // verify the row number is within the range
1628
+ if (rowNumber >= results.length) {
1629
+ throw new Error("row number is out of range " + rowNumber);
1630
+ }
1631
+ const data = results[rowNumber];
1632
+ this.setTestData(data, world);
1633
+ return data;
1634
+ }
1467
1635
  default:
1468
1636
  throw new Error("unknown type " + type);
1469
1637
  }
@@ -1680,7 +1848,8 @@ class StableBrowser {
1680
1848
  if (world) {
1681
1849
  world[variable] = info.value;
1682
1850
  }
1683
- this.logger.info("world." + variable + "=" + info.value);
1851
+ this.setTestData({ [variable]: info.value }, world);
1852
+ this.logger.info("set test data: " + variable + "=" + info.value);
1684
1853
  return info;
1685
1854
  }
1686
1855
  catch (e) {
@@ -1717,6 +1886,91 @@ class StableBrowser {
1717
1886
  });
1718
1887
  }
1719
1888
  }
1889
+ async extractEmailData(emailAddress, options, world) {
1890
+ if (!emailAddress) {
1891
+ throw new Error("email address is null");
1892
+ }
1893
+ // check if address contain @
1894
+ if (emailAddress.indexOf("@") === -1) {
1895
+ emailAddress = emailAddress + "@blinq-mail.io";
1896
+ }
1897
+ else {
1898
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1899
+ throw new Error("email address should end with @blinq-mail.io");
1900
+ }
1901
+ }
1902
+ const startTime = Date.now();
1903
+ let timeout = 60000;
1904
+ if (options && options.timeout) {
1905
+ timeout = options.timeout;
1906
+ }
1907
+ const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1908
+ const request = {
1909
+ method: "POST",
1910
+ url: serviceUrl,
1911
+ headers: {
1912
+ "Content-Type": "application/json",
1913
+ Authorization: `Bearer ${process.env.TOKEN}`,
1914
+ },
1915
+ data: JSON.stringify({
1916
+ email: emailAddress,
1917
+ }),
1918
+ };
1919
+ let errorCount = 0;
1920
+ while (true) {
1921
+ try {
1922
+ let result = await this.context.api.request(request);
1923
+ // the response body expected to be the following:
1924
+ // {
1925
+ // "status": true,
1926
+ // "content": {
1927
+ // "url": "",
1928
+ // "code": "112112",
1929
+ // "name": "generate_link_or_code"
1930
+ // }
1931
+ //}
1932
+ if ((result && result.data, result.data.status === true)) {
1933
+ let codeOrUrlFound = false;
1934
+ let emailCode = null;
1935
+ let emailUrl = null;
1936
+ // check if a code is returned
1937
+ if (result.data.content && result.data.content.code) {
1938
+ let code = result.data.content.code;
1939
+ this.setTestData({ emailCode: code }, world);
1940
+ this.logger.info("set test data: emailCode = " + code);
1941
+ emailCode = code;
1942
+ codeOrUrlFound = true;
1943
+ }
1944
+ // check if a url is returned
1945
+ if (result.data.content && result.data.content.url) {
1946
+ let url = result.data.content.url;
1947
+ this.setTestData({ emailUrl: url }, world);
1948
+ this.logger.info("set test data: emailUrl = " + url);
1949
+ emailUrl = url;
1950
+ codeOrUrlFound = true;
1951
+ }
1952
+ if (codeOrUrlFound) {
1953
+ return { emailUrl, emailCode };
1954
+ }
1955
+ else {
1956
+ this.logger.info("an email received but no code or url found");
1957
+ }
1958
+ }
1959
+ }
1960
+ catch (e) {
1961
+ errorCount++;
1962
+ if (errorCount > 3) {
1963
+ throw e;
1964
+ }
1965
+ // ignore
1966
+ }
1967
+ // check if the timeout is reached
1968
+ if (Date.now() - startTime > timeout) {
1969
+ throw new Error("timeout reached");
1970
+ }
1971
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1972
+ }
1973
+ }
1720
1974
  async _highlightElements(scope, css) {
1721
1975
  try {
1722
1976
  if (!scope) {
@@ -1898,8 +2152,10 @@ class StableBrowser {
1898
2152
  const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
1899
2153
  await this._highlightElements(frame, dataAttribute);
1900
2154
  const element = await frame.$(dataAttribute);
1901
- await this.scrollIfNeeded(element, info);
1902
- await element.dispatchEvent("bvt_verify_page_contains_text");
2155
+ if (element) {
2156
+ await this.scrollIfNeeded(element, info);
2157
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2158
+ }
1903
2159
  }
1904
2160
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1905
2161
  return info;
@@ -1937,6 +2193,16 @@ class StableBrowser {
1937
2193
  });
1938
2194
  }
1939
2195
  }
2196
+ _getServerUrl() {
2197
+ let serviceUrl = "https://api.blinq.io";
2198
+ if (process.env.NODE_ENV_BLINQ === "dev") {
2199
+ serviceUrl = "https://dev.api.blinq.io";
2200
+ }
2201
+ else if (process.env.NODE_ENV_BLINQ === "stage") {
2202
+ serviceUrl = "https://stage.api.blinq.io";
2203
+ }
2204
+ return serviceUrl;
2205
+ }
1940
2206
  async visualVerification(text, options = {}, world = null) {
1941
2207
  const startTime = Date.now();
1942
2208
  let error = null;
@@ -1951,13 +2217,7 @@ class StableBrowser {
1951
2217
  throw new Error("TOKEN is not set");
1952
2218
  }
1953
2219
  try {
1954
- let serviceUrl = "https://api.blinq.io";
1955
- if (process.env.NODE_ENV_BLINQ === "dev") {
1956
- serviceUrl = "https://dev.api.blinq.io";
1957
- }
1958
- else if (process.env.NODE_ENV_BLINQ === "stage") {
1959
- serviceUrl = "https://stage.api.blinq.io";
1960
- }
2220
+ let serviceUrl = this._getServerUrl();
1961
2221
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1962
2222
  info.screenshotPath = screenshotPath;
1963
2223
  const screenshot = await this.takeScreenshot();
@@ -2106,7 +2366,16 @@ class StableBrowser {
2106
2366
  let screenshotId = null;
2107
2367
  let screenshotPath = null;
2108
2368
  const info = {};
2109
- info.log = "***** analyze table " + selectors.element_name + " query " + query + " operator " + operator + " value " + value + " *****\n";
2369
+ info.log =
2370
+ "***** analyze table " +
2371
+ selectors.element_name +
2372
+ " query " +
2373
+ query +
2374
+ " operator " +
2375
+ operator +
2376
+ " value " +
2377
+ value +
2378
+ " *****\n";
2110
2379
  info.operation = "analyzeTable";
2111
2380
  info.selectors = selectors;
2112
2381
  info.query = query;
@@ -2340,13 +2609,6 @@ class StableBrowser {
2340
2609
  const info = {};
2341
2610
  try {
2342
2611
  await this.page.close();
2343
- if (this.context && this.context.pages && this.context.pages.length > 0) {
2344
- this.context.pages.pop();
2345
- this.page = this.context.pages[this.context.pages.length - 1];
2346
- this.context.page = this.page;
2347
- let title = await this.page.title();
2348
- console.log("Switched to page " + title);
2349
- }
2350
2612
  }
2351
2613
  catch (e) {
2352
2614
  console.log(".");