automation_model 1.0.370-dev.0 → 1.0.370-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,94 @@ 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]);
185
- }
186
- if (locator.css) {
187
- return scope.locator(this._fixUsingParams(locator.css, _params));
188
- }
189
- throw new Error("unknown locator type");
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]);
228
+ }
229
+ if (locator.css || locator.engine === "css") {
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
+ }
243
+ }
244
+ // locator object { engine: "internal:text", selector: '"Signup / Login"i', priority: 4 },
245
+ if (locator.engine === "internal:text") {
246
+ // extract the text and the i flag using regex
247
+ const match = locator.selector.match(/"(.*)"(.*)/);
248
+ if (match) {
249
+ const text = match[1];
250
+ const flags = match[2];
251
+ locatorReturn = scope.locator(`text=${text}`, { exact: flags === "i" });
252
+ }
253
+ }
254
+ if (locator.engine === "internal:attr") {
255
+ selector = `[${selector}]`;
256
+ locatorReturn = scope.locator(`${locator.engine}=${selector}`);
257
+ }
258
+ if (!locatorReturn) {
259
+ console.error(locator);
260
+ throw new Error("Locator " + JSON.stringify(locator) + " not found");
261
+ // } else {
262
+ // const count = locatorReturn.count();
263
+ // if (count === 0) {
264
+ // throw new Error("Elements not found");
265
+ // } else if (count > 1) {
266
+ // throw new Error("Multiple elements found");
267
+ // }
268
+ }
269
+ return locatorReturn;
190
270
  }
191
271
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
192
272
  let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
@@ -309,10 +389,13 @@ class StableBrowser {
309
389
  }
310
390
  async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
311
391
  let locatorSearch = selectorHierarchy[index];
312
- info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
392
+ //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
313
393
  let locator = null;
314
394
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
315
395
  let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
396
+ if (!locatorString) {
397
+ return;
398
+ }
316
399
  locator = this._getLocator({ css: locatorString }, scope, _params);
317
400
  }
318
401
  else if (locatorSearch.text) {
@@ -334,7 +417,7 @@ class StableBrowser {
334
417
  // cssHref = true;
335
418
  // }
336
419
  let count = await locator.count();
337
- info.log += "total elements found " + count + "\n";
420
+ //info.log += "total elements found " + count + "\n";
338
421
  //let visibleCount = 0;
339
422
  let visibleLocator = null;
340
423
  if (locatorSearch.index && locatorSearch.index < count) {
@@ -344,16 +427,20 @@ class StableBrowser {
344
427
  for (let j = 0; j < count; j++) {
345
428
  let visible = await locator.nth(j).isVisible();
346
429
  const enabled = await locator.nth(j).isEnabled();
347
- info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
348
430
  if (!visibleOnly) {
349
431
  visible = true;
350
432
  }
351
433
  if (visible && enabled) {
352
434
  foundLocators.push(locator.nth(j));
353
- // if (cssHref) {
354
- // info.log += "css href locator found, will ignore all others" + "\n";
355
- // break;
356
- // }
435
+ }
436
+ else {
437
+ if (!info.printMessages) {
438
+ info.printMessages = {};
439
+ }
440
+ if (!info.printMessages[j.toString()]) {
441
+ info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
442
+ info.printMessages[j.toString()] = true;
443
+ }
357
444
  }
358
445
  }
359
446
  }
@@ -399,6 +486,10 @@ class StableBrowser {
399
486
  async _locate(selectors, info, _params, timeout = 30000) {
400
487
  for (let i = 0; i < 3; i++) {
401
488
  info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
489
+ for (let j = 0; j < selectors.locators.length; j++) {
490
+ let selector = selectors.locators[j];
491
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
492
+ }
402
493
  let element = await this._locate_internal(selectors, info, _params, timeout);
403
494
  if (!element.rerun) {
404
495
  return element;
@@ -493,6 +584,7 @@ class StableBrowser {
493
584
  let foundElements = result.foundElements;
494
585
  if (foundElements.length === 1 && foundElements[0].unique) {
495
586
  info.box = foundElements[0].box;
587
+ info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
496
588
  return foundElements[0].locator;
497
589
  }
498
590
  //info.log += "total elements found " + foundElements.length);
@@ -522,6 +614,7 @@ class StableBrowser {
522
614
  }
523
615
  }
524
616
  if (maxCountElement) {
617
+ info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
525
618
  info.box = await maxCountElement.locator.boundingBox();
526
619
  return maxCountElement.locator;
527
620
  }
@@ -530,9 +623,11 @@ class StableBrowser {
530
623
  break;
531
624
  }
532
625
  if (performance.now() - startTime > highPriorityTimeout) {
626
+ info.log += "high priority timeout, will try all elements" + "\n";
533
627
  highPriorityOnly = false;
534
628
  }
535
629
  if (performance.now() - startTime > visibleOnlyTimeout) {
630
+ info.log += "visible only timeout, will try all elements" + "\n";
536
631
  visibleOnly = false;
537
632
  }
538
633
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -576,6 +671,9 @@ class StableBrowser {
576
671
  async click(selectors, _params, options = {}, world = null) {
577
672
  this._validateSelectors(selectors);
578
673
  const startTime = Date.now();
674
+ if (options && options.context) {
675
+ selectors.locators[0].text = options.context;
676
+ }
579
677
  const info = {};
580
678
  info.log = "***** click on " + selectors.element_name + " *****\n";
581
679
  info.operation = "click";
@@ -884,71 +982,45 @@ class StableBrowser {
884
982
  });
885
983
  }
886
984
  }
887
- async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
985
+ async setInputValue(selectors, value, _params = null, options = {}, world = null) {
986
+ // set input value for non fillable inputs like date, time, range, color, etc.
888
987
  this._validateSelectors(selectors);
889
988
  const startTime = Date.now();
890
- let error = null;
891
- let screenshotId = null;
892
- let screenshotPath = null;
893
989
  const info = {};
894
- info.log = "";
895
- info.operation = Types.SET_DATE_TIME;
990
+ info.log = "***** set input value " + selectors.element_name + " *****\n";
991
+ info.operation = "setInputValue";
896
992
  info.selectors = selectors;
993
+ value = this._fixUsingParams(value, _params);
897
994
  info.value = value;
995
+ let error = null;
996
+ let screenshotId = null;
997
+ let screenshotPath = null;
898
998
  try {
899
999
  value = await this._replaceWithLocalData(value, this);
900
1000
  let element = await this._locate(selectors, info, _params);
901
- //insert red border around the element
902
1001
  await this.scrollIfNeeded(element, info);
903
1002
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
904
1003
  await this._highlightElements(element);
905
1004
  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
- }
1005
+ await element.evaluateHandle((el, value) => {
1006
+ el.value = value;
1007
+ }, value);
924
1008
  }
925
1009
  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)));
1010
+ this.logger.error("setInputValue failed, will try again");
1011
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
929
1012
  info.screenshotPath = screenshotPath;
930
1013
  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
- }
1014
+ await element.evaluateHandle((el, value) => {
1015
+ el.value = value;
1016
+ });
949
1017
  }
950
1018
  }
951
- catch (error) {
1019
+ catch (e) {
1020
+ this.logger.error("setInputValue failed " + info.log);
1021
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1022
+ info.screenshotPath = screenshotPath;
1023
+ Object.assign(e, { info: info });
952
1024
  error = e;
953
1025
  throw e;
954
1026
  }
@@ -956,10 +1028,10 @@ class StableBrowser {
956
1028
  const endTime = Date.now();
957
1029
  this._reportToWorld(world, {
958
1030
  element_name: selectors.element_name,
959
- type: Types.SET_DATE_TIME,
960
- screenshotId,
1031
+ type: Types.SET_INPUT,
1032
+ text: `Set input value`,
961
1033
  value: value,
962
- text: `setDateTime input with value: ${value}`,
1034
+ screenshotId,
963
1035
  result: error
964
1036
  ? {
965
1037
  status: "FAILED",
@@ -976,7 +1048,7 @@ class StableBrowser {
976
1048
  });
977
1049
  }
978
1050
  }
979
- async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
1051
+ async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
980
1052
  this._validateSelectors(selectors);
981
1053
  const startTime = Date.now();
982
1054
  let error = null;
@@ -988,6 +1060,7 @@ class StableBrowser {
988
1060
  info.selectors = selectors;
989
1061
  info.value = value;
990
1062
  try {
1063
+ value = await this._replaceWithLocalData(value, this);
991
1064
  let element = await this._locate(selectors, info, _params);
992
1065
  //insert red border around the element
993
1066
  await this.scrollIfNeeded(element, info);
@@ -996,28 +1069,51 @@ class StableBrowser {
996
1069
  try {
997
1070
  await element.click();
998
1071
  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);
1072
+ if (format) {
1073
+ value = dayjs(value).format(format);
1074
+ await element.fill(value);
1075
+ }
1076
+ else {
1077
+ const dateTimeValue = await getDateTimeValue({ value, element });
1078
+ await element.evaluateHandle((el, dateTimeValue) => {
1079
+ el.value = ""; // clear input
1080
+ el.value = dateTimeValue;
1081
+ }, dateTimeValue);
1082
+ }
1083
+ if (enter) {
1084
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1085
+ await this.page.keyboard.press("Enter");
1086
+ await this.waitForPageLoad();
1087
+ }
1004
1088
  }
1005
- catch (error) {
1089
+ catch (err) {
1006
1090
  //await this.closeUnexpectedPopups();
1007
1091
  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)));
1092
+ this.logger.info("Trying again");
1093
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1009
1094
  info.screenshotPath = screenshotPath;
1010
- Object.assign(error, { info: info });
1095
+ Object.assign(err, { info: info });
1011
1096
  await element.click();
1012
1097
  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);
1098
+ if (format) {
1099
+ value = dayjs(value).format(format);
1100
+ await element.fill(value);
1101
+ }
1102
+ else {
1103
+ const dateTimeValue = await getDateTimeValue({ value, element });
1104
+ await element.evaluateHandle((el, dateTimeValue) => {
1105
+ el.value = ""; // clear input
1106
+ el.value = dateTimeValue;
1107
+ }, dateTimeValue);
1108
+ }
1109
+ if (enter) {
1110
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1111
+ await this.page.keyboard.press("Enter");
1112
+ await this.waitForPageLoad();
1113
+ }
1018
1114
  }
1019
1115
  }
1020
- catch (error) {
1116
+ catch (e) {
1021
1117
  error = e;
1022
1118
  throw e;
1023
1119
  }
@@ -1067,20 +1163,32 @@ class StableBrowser {
1067
1163
  await this.scrollIfNeeded(element, info);
1068
1164
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1069
1165
  await this._highlightElements(element);
1070
- try {
1071
- let currentValue = await element.inputValue();
1072
- if (currentValue) {
1073
- await element.fill("");
1166
+ if (options === null || options === undefined || !options.press) {
1167
+ try {
1168
+ let currentValue = await element.inputValue();
1169
+ if (currentValue) {
1170
+ await element.fill("");
1171
+ }
1172
+ }
1173
+ catch (e) {
1174
+ this.logger.info("unable to clear input value");
1074
1175
  }
1075
1176
  }
1076
- catch (e) {
1077
- this.logger.info("unable to clear input value");
1078
- }
1079
- try {
1080
- await element.click({ timeout: 5000 });
1177
+ if (options === null || options === undefined || options.press) {
1178
+ try {
1179
+ await element.click({ timeout: 5000 });
1180
+ }
1181
+ catch (e) {
1182
+ await element.dispatchEvent("click");
1183
+ }
1081
1184
  }
1082
- catch (e) {
1083
- await element.dispatchEvent("click");
1185
+ else {
1186
+ try {
1187
+ await element.focus();
1188
+ }
1189
+ catch (e) {
1190
+ await element.dispatchEvent("focus");
1191
+ }
1084
1192
  }
1085
1193
  await new Promise((resolve) => setTimeout(resolve, 500));
1086
1194
  const valueSegment = _value.split("&&");
@@ -1276,7 +1384,8 @@ class StableBrowser {
1276
1384
  let screenshotId = null;
1277
1385
  let screenshotPath = null;
1278
1386
  const info = {};
1279
- info.log = "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1387
+ info.log =
1388
+ "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1280
1389
  info.operation = "containsPattern";
1281
1390
  info.selectors = selectors;
1282
1391
  info.value = text;
@@ -1446,15 +1555,62 @@ class StableBrowser {
1446
1555
  // save the data to the file
1447
1556
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1448
1557
  }
1558
+ _getDataFilePath(fileName) {
1559
+ let dataFile = path.join(this.project_path, "data", fileName);
1560
+ if (fs.existsSync(dataFile)) {
1561
+ return dataFile;
1562
+ }
1563
+ dataFile = path.join(this.project_path, fileName);
1564
+ if (fs.existsSync(dataFile)) {
1565
+ return dataFile;
1566
+ }
1567
+ throw new Error("data file not found " + fileName);
1568
+ }
1569
+ _parseCSVSync(filePath) {
1570
+ const data = fs.readFileSync(filePath, "utf8");
1571
+ const results = [];
1572
+ return new Promise((resolve, reject) => {
1573
+ const readableStream = new Readable();
1574
+ readableStream._read = () => { }; // _read is required but you can noop it
1575
+ readableStream.push(data);
1576
+ readableStream.push(null);
1577
+ readableStream
1578
+ .pipe(csv())
1579
+ .on("data", (data) => results.push(data))
1580
+ .on("end", () => resolve(results))
1581
+ .on("error", (error) => reject(error));
1582
+ });
1583
+ }
1449
1584
  loadTestData(type, dataSelector, world = null) {
1450
1585
  switch (type) {
1451
1586
  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");
1587
+ // get the users.json file path
1588
+ let dataFile = this._getDataFilePath("users.json");
1589
+ // read the file and return the data
1590
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1591
+ for (let i = 0; i < users.length; i++) {
1592
+ if (users[i].username === dataSelector) {
1593
+ const userObj = {
1594
+ username: users[i].username,
1595
+ password: "secret:" + users[i].password,
1596
+ totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
1597
+ };
1598
+ this.setTestData(userObj, world);
1599
+ return userObj;
1600
+ }
1455
1601
  }
1602
+ throw new Error("user not found " + dataSelector);
1603
+ default:
1604
+ throw new Error("unknown type " + type);
1605
+ }
1606
+ }
1607
+ async loadTestDataAsync(type, dataSelector, world = null) {
1608
+ switch (type) {
1609
+ case "users": {
1610
+ // get the users.json file path
1611
+ let dataFile = this._getDataFilePath("users.json");
1456
1612
  // read the file and return the data
1457
- const users = JSON.parse(fs.readFileSync(path.join(this.project_path, "users.json"), "utf8"));
1613
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1458
1614
  for (let i = 0; i < users.length; i++) {
1459
1615
  if (users[i].username === dataSelector) {
1460
1616
  const userObj = {
@@ -1467,6 +1623,29 @@ class StableBrowser {
1467
1623
  }
1468
1624
  }
1469
1625
  throw new Error("user not found " + dataSelector);
1626
+ }
1627
+ case "csv": {
1628
+ // 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
1629
+ const parts = dataSelector.split(":");
1630
+ let rowNumber = 0;
1631
+ if (parts.length > 1) {
1632
+ rowNumber = parseInt(parts[1]);
1633
+ }
1634
+ let dataFile = this._getDataFilePath(parts[0]);
1635
+ const results = await this._parseCSVSync(dataFile);
1636
+ // result stracture:
1637
+ // [
1638
+ // { NAME: 'Daffy Duck', AGE: '24' },
1639
+ // { NAME: 'Bugs Bunny', AGE: '22' }
1640
+ // ]
1641
+ // verify the row number is within the range
1642
+ if (rowNumber >= results.length) {
1643
+ throw new Error("row number is out of range " + rowNumber);
1644
+ }
1645
+ const data = results[rowNumber];
1646
+ this.setTestData(data, world);
1647
+ return data;
1648
+ }
1470
1649
  default:
1471
1650
  throw new Error("unknown type " + type);
1472
1651
  }
@@ -1571,13 +1750,13 @@ class StableBrowser {
1571
1750
  ])));
1572
1751
  const { data } = await client.send("Page.captureScreenshot", {
1573
1752
  format: "png",
1574
- clip: {
1575
- x: 0,
1576
- y: 0,
1577
- width: viewportWidth,
1578
- height: viewportHeight,
1579
- scale: 1,
1580
- },
1753
+ // clip: {
1754
+ // x: 0,
1755
+ // y: 0,
1756
+ // width: viewportWidth,
1757
+ // height: viewportHeight,
1758
+ // scale: 1,
1759
+ // },
1581
1760
  });
1582
1761
  if (!screenshotPath) {
1583
1762
  return data;
@@ -1683,7 +1862,8 @@ class StableBrowser {
1683
1862
  if (world) {
1684
1863
  world[variable] = info.value;
1685
1864
  }
1686
- this.logger.info("world." + variable + "=" + info.value);
1865
+ this.setTestData({ [variable]: info.value }, world);
1866
+ this.logger.info("set test data: " + variable + "=" + info.value);
1687
1867
  return info;
1688
1868
  }
1689
1869
  catch (e) {
@@ -1720,6 +1900,91 @@ class StableBrowser {
1720
1900
  });
1721
1901
  }
1722
1902
  }
1903
+ async extractEmailData(emailAddress, options, world) {
1904
+ if (!emailAddress) {
1905
+ throw new Error("email address is null");
1906
+ }
1907
+ // check if address contain @
1908
+ if (emailAddress.indexOf("@") === -1) {
1909
+ emailAddress = emailAddress + "@blinq-mail.io";
1910
+ }
1911
+ else {
1912
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1913
+ throw new Error("email address should end with @blinq-mail.io");
1914
+ }
1915
+ }
1916
+ const startTime = Date.now();
1917
+ let timeout = 60000;
1918
+ if (options && options.timeout) {
1919
+ timeout = options.timeout;
1920
+ }
1921
+ const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1922
+ const request = {
1923
+ method: "POST",
1924
+ url: serviceUrl,
1925
+ headers: {
1926
+ "Content-Type": "application/json",
1927
+ Authorization: `Bearer ${process.env.TOKEN}`,
1928
+ },
1929
+ data: JSON.stringify({
1930
+ email: emailAddress,
1931
+ }),
1932
+ };
1933
+ let errorCount = 0;
1934
+ while (true) {
1935
+ try {
1936
+ let result = await this.context.api.request(request);
1937
+ // the response body expected to be the following:
1938
+ // {
1939
+ // "status": true,
1940
+ // "content": {
1941
+ // "url": "",
1942
+ // "code": "112112",
1943
+ // "name": "generate_link_or_code"
1944
+ // }
1945
+ //}
1946
+ if ((result && result.data, result.data.status === true)) {
1947
+ let codeOrUrlFound = false;
1948
+ let emailCode = null;
1949
+ let emailUrl = null;
1950
+ // check if a code is returned
1951
+ if (result.data.content && result.data.content.code) {
1952
+ let code = result.data.content.code;
1953
+ this.setTestData({ emailCode: code }, world);
1954
+ this.logger.info("set test data: emailCode = " + code);
1955
+ emailCode = code;
1956
+ codeOrUrlFound = true;
1957
+ }
1958
+ // check if a url is returned
1959
+ if (result.data.content && result.data.content.url) {
1960
+ let url = result.data.content.url;
1961
+ this.setTestData({ emailUrl: url }, world);
1962
+ this.logger.info("set test data: emailUrl = " + url);
1963
+ emailUrl = url;
1964
+ codeOrUrlFound = true;
1965
+ }
1966
+ if (codeOrUrlFound) {
1967
+ return { emailUrl, emailCode };
1968
+ }
1969
+ else {
1970
+ this.logger.info("an email received but no code or url found");
1971
+ }
1972
+ }
1973
+ }
1974
+ catch (e) {
1975
+ errorCount++;
1976
+ if (errorCount > 3) {
1977
+ throw e;
1978
+ }
1979
+ // ignore
1980
+ }
1981
+ // check if the timeout is reached
1982
+ if (Date.now() - startTime > timeout) {
1983
+ throw new Error("timeout reached");
1984
+ }
1985
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1986
+ }
1987
+ }
1723
1988
  async _highlightElements(scope, css) {
1724
1989
  try {
1725
1990
  if (!scope) {
@@ -1901,8 +2166,10 @@ class StableBrowser {
1901
2166
  const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
1902
2167
  await this._highlightElements(frame, dataAttribute);
1903
2168
  const element = await frame.$(dataAttribute);
1904
- await this.scrollIfNeeded(element, info);
1905
- await element.dispatchEvent("bvt_verify_page_contains_text");
2169
+ if (element) {
2170
+ await this.scrollIfNeeded(element, info);
2171
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2172
+ }
1906
2173
  }
1907
2174
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1908
2175
  return info;
@@ -1940,6 +2207,16 @@ class StableBrowser {
1940
2207
  });
1941
2208
  }
1942
2209
  }
2210
+ _getServerUrl() {
2211
+ let serviceUrl = "https://api.blinq.io";
2212
+ if (process.env.NODE_ENV_BLINQ === "dev") {
2213
+ serviceUrl = "https://dev.api.blinq.io";
2214
+ }
2215
+ else if (process.env.NODE_ENV_BLINQ === "stage") {
2216
+ serviceUrl = "https://stage.api.blinq.io";
2217
+ }
2218
+ return serviceUrl;
2219
+ }
1943
2220
  async visualVerification(text, options = {}, world = null) {
1944
2221
  const startTime = Date.now();
1945
2222
  let error = null;
@@ -1954,13 +2231,7 @@ class StableBrowser {
1954
2231
  throw new Error("TOKEN is not set");
1955
2232
  }
1956
2233
  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
- }
2234
+ let serviceUrl = this._getServerUrl();
1964
2235
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1965
2236
  info.screenshotPath = screenshotPath;
1966
2237
  const screenshot = await this.takeScreenshot();
@@ -2109,7 +2380,16 @@ class StableBrowser {
2109
2380
  let screenshotId = null;
2110
2381
  let screenshotPath = null;
2111
2382
  const info = {};
2112
- info.log = "***** analyze table " + selectors.element_name + " query " + query + " operator " + operator + " value " + value + " *****\n";
2383
+ info.log =
2384
+ "***** analyze table " +
2385
+ selectors.element_name +
2386
+ " query " +
2387
+ query +
2388
+ " operator " +
2389
+ operator +
2390
+ " value " +
2391
+ value +
2392
+ " *****\n";
2113
2393
  info.operation = "analyzeTable";
2114
2394
  info.selectors = selectors;
2115
2395
  info.query = query;
@@ -2343,13 +2623,6 @@ class StableBrowser {
2343
2623
  const info = {};
2344
2624
  try {
2345
2625
  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
2626
  }
2354
2627
  catch (e) {
2355
2628
  console.log(".");