automation_model 1.0.372-dev.0 → 1.0.372-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,88 @@ 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.engine === "internal:text") {
245
+ // extract the text and the i flag using regex
246
+ const match = locator.selector.match(/"(.*)"(.*)/);
247
+ if (match) {
248
+ const text = match[1];
249
+ const flags = match[2];
250
+ locatorReturn = scope.locator(`text=${text}`, { exact: flags === "i" });
251
+ }
252
+ }
253
+ if (locator.engine === "internal:attr") {
254
+ if (!selector.startsWith("[")) {
255
+ selector = `[${selector}]`;
256
+ }
257
+ locatorReturn = scope.locator(`${locator.engine}=${selector}`);
258
+ }
259
+ if (!locatorReturn) {
260
+ console.error(locator);
261
+ throw new Error("Locator " + JSON.stringify(locator) + " not found");
262
+ }
263
+ return locatorReturn;
190
264
  }
191
265
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
192
266
  let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
@@ -309,10 +383,13 @@ class StableBrowser {
309
383
  }
310
384
  async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
311
385
  let locatorSearch = selectorHierarchy[index];
312
- info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
386
+ //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
313
387
  let locator = null;
314
388
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
315
389
  let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
390
+ if (!locatorString) {
391
+ return;
392
+ }
316
393
  locator = this._getLocator({ css: locatorString }, scope, _params);
317
394
  }
318
395
  else if (locatorSearch.text) {
@@ -334,7 +411,7 @@ class StableBrowser {
334
411
  // cssHref = true;
335
412
  // }
336
413
  let count = await locator.count();
337
- info.log += "total elements found " + count + "\n";
414
+ //info.log += "total elements found " + count + "\n";
338
415
  //let visibleCount = 0;
339
416
  let visibleLocator = null;
340
417
  if (locatorSearch.index && locatorSearch.index < count) {
@@ -344,16 +421,20 @@ class StableBrowser {
344
421
  for (let j = 0; j < count; j++) {
345
422
  let visible = await locator.nth(j).isVisible();
346
423
  const enabled = await locator.nth(j).isEnabled();
347
- info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
348
424
  if (!visibleOnly) {
349
425
  visible = true;
350
426
  }
351
427
  if (visible && enabled) {
352
428
  foundLocators.push(locator.nth(j));
353
- // if (cssHref) {
354
- // info.log += "css href locator found, will ignore all others" + "\n";
355
- // break;
356
- // }
429
+ }
430
+ else {
431
+ if (!info.printMessages) {
432
+ info.printMessages = {};
433
+ }
434
+ if (!info.printMessages[j.toString()]) {
435
+ info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
436
+ info.printMessages[j.toString()] = true;
437
+ }
357
438
  }
358
439
  }
359
440
  }
@@ -399,6 +480,10 @@ class StableBrowser {
399
480
  async _locate(selectors, info, _params, timeout = 30000) {
400
481
  for (let i = 0; i < 3; i++) {
401
482
  info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
483
+ for (let j = 0; j < selectors.locators.length; j++) {
484
+ let selector = selectors.locators[j];
485
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
486
+ }
402
487
  let element = await this._locate_internal(selectors, info, _params, timeout);
403
488
  if (!element.rerun) {
404
489
  return element;
@@ -493,6 +578,7 @@ class StableBrowser {
493
578
  let foundElements = result.foundElements;
494
579
  if (foundElements.length === 1 && foundElements[0].unique) {
495
580
  info.box = foundElements[0].box;
581
+ info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
496
582
  return foundElements[0].locator;
497
583
  }
498
584
  //info.log += "total elements found " + foundElements.length);
@@ -522,6 +608,7 @@ class StableBrowser {
522
608
  }
523
609
  }
524
610
  if (maxCountElement) {
611
+ info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
525
612
  info.box = await maxCountElement.locator.boundingBox();
526
613
  return maxCountElement.locator;
527
614
  }
@@ -530,9 +617,11 @@ class StableBrowser {
530
617
  break;
531
618
  }
532
619
  if (performance.now() - startTime > highPriorityTimeout) {
620
+ info.log += "high priority timeout, will try all elements" + "\n";
533
621
  highPriorityOnly = false;
534
622
  }
535
623
  if (performance.now() - startTime > visibleOnlyTimeout) {
624
+ info.log += "visible only timeout, will try all elements" + "\n";
536
625
  visibleOnly = false;
537
626
  }
538
627
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -576,6 +665,9 @@ class StableBrowser {
576
665
  async click(selectors, _params, options = {}, world = null) {
577
666
  this._validateSelectors(selectors);
578
667
  const startTime = Date.now();
668
+ if (options && options.context) {
669
+ selectors.locators[0].text = options.context;
670
+ }
579
671
  const info = {};
580
672
  info.log = "***** click on " + selectors.element_name + " *****\n";
581
673
  info.operation = "click";
@@ -884,71 +976,45 @@ class StableBrowser {
884
976
  });
885
977
  }
886
978
  }
887
- async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
979
+ async setInputValue(selectors, value, _params = null, options = {}, world = null) {
980
+ // set input value for non fillable inputs like date, time, range, color, etc.
888
981
  this._validateSelectors(selectors);
889
982
  const startTime = Date.now();
890
- let error = null;
891
- let screenshotId = null;
892
- let screenshotPath = null;
893
983
  const info = {};
894
- info.log = "";
895
- info.operation = Types.SET_DATE_TIME;
984
+ info.log = "***** set input value " + selectors.element_name + " *****\n";
985
+ info.operation = "setInputValue";
896
986
  info.selectors = selectors;
987
+ value = this._fixUsingParams(value, _params);
897
988
  info.value = value;
989
+ let error = null;
990
+ let screenshotId = null;
991
+ let screenshotPath = null;
898
992
  try {
899
993
  value = await this._replaceWithLocalData(value, this);
900
994
  let element = await this._locate(selectors, info, _params);
901
- //insert red border around the element
902
995
  await this.scrollIfNeeded(element, info);
903
996
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
904
997
  await this._highlightElements(element);
905
998
  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
- }
999
+ await element.evaluateHandle((el, value) => {
1000
+ el.value = value;
1001
+ }, value);
924
1002
  }
925
1003
  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)));
1004
+ this.logger.error("setInputValue failed, will try again");
1005
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
929
1006
  info.screenshotPath = screenshotPath;
930
1007
  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
- }
1008
+ await element.evaluateHandle((el, value) => {
1009
+ el.value = value;
1010
+ });
949
1011
  }
950
1012
  }
951
- catch (error) {
1013
+ catch (e) {
1014
+ this.logger.error("setInputValue failed " + info.log);
1015
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1016
+ info.screenshotPath = screenshotPath;
1017
+ Object.assign(e, { info: info });
952
1018
  error = e;
953
1019
  throw e;
954
1020
  }
@@ -956,10 +1022,10 @@ class StableBrowser {
956
1022
  const endTime = Date.now();
957
1023
  this._reportToWorld(world, {
958
1024
  element_name: selectors.element_name,
959
- type: Types.SET_DATE_TIME,
960
- screenshotId,
1025
+ type: Types.SET_INPUT,
1026
+ text: `Set input value`,
961
1027
  value: value,
962
- text: `setDateTime input with value: ${value}`,
1028
+ screenshotId,
963
1029
  result: error
964
1030
  ? {
965
1031
  status: "FAILED",
@@ -976,7 +1042,7 @@ class StableBrowser {
976
1042
  });
977
1043
  }
978
1044
  }
979
- async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
1045
+ async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
980
1046
  this._validateSelectors(selectors);
981
1047
  const startTime = Date.now();
982
1048
  let error = null;
@@ -988,6 +1054,7 @@ class StableBrowser {
988
1054
  info.selectors = selectors;
989
1055
  info.value = value;
990
1056
  try {
1057
+ value = await this._replaceWithLocalData(value, this);
991
1058
  let element = await this._locate(selectors, info, _params);
992
1059
  //insert red border around the element
993
1060
  await this.scrollIfNeeded(element, info);
@@ -996,28 +1063,51 @@ class StableBrowser {
996
1063
  try {
997
1064
  await element.click();
998
1065
  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);
1066
+ if (format) {
1067
+ value = dayjs(value).format(format);
1068
+ await element.fill(value);
1069
+ }
1070
+ else {
1071
+ const dateTimeValue = await getDateTimeValue({ value, element });
1072
+ await element.evaluateHandle((el, dateTimeValue) => {
1073
+ el.value = ""; // clear input
1074
+ el.value = dateTimeValue;
1075
+ }, dateTimeValue);
1076
+ }
1077
+ if (enter) {
1078
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1079
+ await this.page.keyboard.press("Enter");
1080
+ await this.waitForPageLoad();
1081
+ }
1004
1082
  }
1005
- catch (error) {
1083
+ catch (err) {
1006
1084
  //await this.closeUnexpectedPopups();
1007
1085
  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)));
1086
+ this.logger.info("Trying again");
1087
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1009
1088
  info.screenshotPath = screenshotPath;
1010
- Object.assign(error, { info: info });
1089
+ Object.assign(err, { info: info });
1011
1090
  await element.click();
1012
1091
  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);
1092
+ if (format) {
1093
+ value = dayjs(value).format(format);
1094
+ await element.fill(value);
1095
+ }
1096
+ else {
1097
+ const dateTimeValue = await getDateTimeValue({ value, element });
1098
+ await element.evaluateHandle((el, dateTimeValue) => {
1099
+ el.value = ""; // clear input
1100
+ el.value = dateTimeValue;
1101
+ }, dateTimeValue);
1102
+ }
1103
+ if (enter) {
1104
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1105
+ await this.page.keyboard.press("Enter");
1106
+ await this.waitForPageLoad();
1107
+ }
1018
1108
  }
1019
1109
  }
1020
- catch (error) {
1110
+ catch (e) {
1021
1111
  error = e;
1022
1112
  throw e;
1023
1113
  }
@@ -1067,20 +1157,32 @@ class StableBrowser {
1067
1157
  await this.scrollIfNeeded(element, info);
1068
1158
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1069
1159
  await this._highlightElements(element);
1070
- try {
1071
- let currentValue = await element.inputValue();
1072
- if (currentValue) {
1073
- await element.fill("");
1160
+ if (options === null || options === undefined || !options.press) {
1161
+ try {
1162
+ let currentValue = await element.inputValue();
1163
+ if (currentValue) {
1164
+ await element.fill("");
1165
+ }
1166
+ }
1167
+ catch (e) {
1168
+ this.logger.info("unable to clear input value");
1074
1169
  }
1075
1170
  }
1076
- catch (e) {
1077
- this.logger.info("unable to clear input value");
1078
- }
1079
- try {
1080
- await element.click({ timeout: 5000 });
1171
+ if (options === null || options === undefined || options.press) {
1172
+ try {
1173
+ await element.click({ timeout: 5000 });
1174
+ }
1175
+ catch (e) {
1176
+ await element.dispatchEvent("click");
1177
+ }
1081
1178
  }
1082
- catch (e) {
1083
- await element.dispatchEvent("click");
1179
+ else {
1180
+ try {
1181
+ await element.focus();
1182
+ }
1183
+ catch (e) {
1184
+ await element.dispatchEvent("focus");
1185
+ }
1084
1186
  }
1085
1187
  await new Promise((resolve) => setTimeout(resolve, 500));
1086
1188
  const valueSegment = _value.split("&&");
@@ -1276,7 +1378,8 @@ class StableBrowser {
1276
1378
  let screenshotId = null;
1277
1379
  let screenshotPath = null;
1278
1380
  const info = {};
1279
- info.log = "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1381
+ info.log =
1382
+ "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1280
1383
  info.operation = "containsPattern";
1281
1384
  info.selectors = selectors;
1282
1385
  info.value = text;
@@ -1446,15 +1549,62 @@ class StableBrowser {
1446
1549
  // save the data to the file
1447
1550
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1448
1551
  }
1552
+ _getDataFilePath(fileName) {
1553
+ let dataFile = path.join(this.project_path, "data", fileName);
1554
+ if (fs.existsSync(dataFile)) {
1555
+ return dataFile;
1556
+ }
1557
+ dataFile = path.join(this.project_path, fileName);
1558
+ if (fs.existsSync(dataFile)) {
1559
+ return dataFile;
1560
+ }
1561
+ throw new Error("data file not found " + fileName);
1562
+ }
1563
+ _parseCSVSync(filePath) {
1564
+ const data = fs.readFileSync(filePath, "utf8");
1565
+ const results = [];
1566
+ return new Promise((resolve, reject) => {
1567
+ const readableStream = new Readable();
1568
+ readableStream._read = () => { }; // _read is required but you can noop it
1569
+ readableStream.push(data);
1570
+ readableStream.push(null);
1571
+ readableStream
1572
+ .pipe(csv())
1573
+ .on("data", (data) => results.push(data))
1574
+ .on("end", () => resolve(results))
1575
+ .on("error", (error) => reject(error));
1576
+ });
1577
+ }
1449
1578
  loadTestData(type, dataSelector, world = null) {
1450
1579
  switch (type) {
1451
1580
  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");
1581
+ // get the users.json file path
1582
+ let dataFile = this._getDataFilePath("users.json");
1583
+ // read the file and return the data
1584
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1585
+ for (let i = 0; i < users.length; i++) {
1586
+ if (users[i].username === dataSelector) {
1587
+ const userObj = {
1588
+ username: users[i].username,
1589
+ password: "secret:" + users[i].password,
1590
+ totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
1591
+ };
1592
+ this.setTestData(userObj, world);
1593
+ return userObj;
1594
+ }
1455
1595
  }
1596
+ throw new Error("user not found " + dataSelector);
1597
+ default:
1598
+ throw new Error("unknown type " + type);
1599
+ }
1600
+ }
1601
+ async loadTestDataAsync(type, dataSelector, world = null) {
1602
+ switch (type) {
1603
+ case "users": {
1604
+ // get the users.json file path
1605
+ let dataFile = this._getDataFilePath("users.json");
1456
1606
  // read the file and return the data
1457
- const users = JSON.parse(fs.readFileSync(path.join(this.project_path, "users.json"), "utf8"));
1607
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1458
1608
  for (let i = 0; i < users.length; i++) {
1459
1609
  if (users[i].username === dataSelector) {
1460
1610
  const userObj = {
@@ -1467,6 +1617,29 @@ class StableBrowser {
1467
1617
  }
1468
1618
  }
1469
1619
  throw new Error("user not found " + dataSelector);
1620
+ }
1621
+ case "csv": {
1622
+ // 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
1623
+ const parts = dataSelector.split(":");
1624
+ let rowNumber = 0;
1625
+ if (parts.length > 1) {
1626
+ rowNumber = parseInt(parts[1]);
1627
+ }
1628
+ let dataFile = this._getDataFilePath(parts[0]);
1629
+ const results = await this._parseCSVSync(dataFile);
1630
+ // result stracture:
1631
+ // [
1632
+ // { NAME: 'Daffy Duck', AGE: '24' },
1633
+ // { NAME: 'Bugs Bunny', AGE: '22' }
1634
+ // ]
1635
+ // verify the row number is within the range
1636
+ if (rowNumber >= results.length) {
1637
+ throw new Error("row number is out of range " + rowNumber);
1638
+ }
1639
+ const data = results[rowNumber];
1640
+ this.setTestData(data, world);
1641
+ return data;
1642
+ }
1470
1643
  default:
1471
1644
  throw new Error("unknown type " + type);
1472
1645
  }
@@ -1571,13 +1744,13 @@ class StableBrowser {
1571
1744
  ])));
1572
1745
  const { data } = await client.send("Page.captureScreenshot", {
1573
1746
  format: "png",
1574
- clip: {
1575
- x: 0,
1576
- y: 0,
1577
- width: viewportWidth,
1578
- height: viewportHeight,
1579
- scale: 1,
1580
- },
1747
+ // clip: {
1748
+ // x: 0,
1749
+ // y: 0,
1750
+ // width: viewportWidth,
1751
+ // height: viewportHeight,
1752
+ // scale: 1,
1753
+ // },
1581
1754
  });
1582
1755
  if (!screenshotPath) {
1583
1756
  return data;
@@ -1683,7 +1856,8 @@ class StableBrowser {
1683
1856
  if (world) {
1684
1857
  world[variable] = info.value;
1685
1858
  }
1686
- this.logger.info("world." + variable + "=" + info.value);
1859
+ this.setTestData({ [variable]: info.value }, world);
1860
+ this.logger.info("set test data: " + variable + "=" + info.value);
1687
1861
  return info;
1688
1862
  }
1689
1863
  catch (e) {
@@ -1720,6 +1894,91 @@ class StableBrowser {
1720
1894
  });
1721
1895
  }
1722
1896
  }
1897
+ async extractEmailData(emailAddress, options, world) {
1898
+ if (!emailAddress) {
1899
+ throw new Error("email address is null");
1900
+ }
1901
+ // check if address contain @
1902
+ if (emailAddress.indexOf("@") === -1) {
1903
+ emailAddress = emailAddress + "@blinq-mail.io";
1904
+ }
1905
+ else {
1906
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1907
+ throw new Error("email address should end with @blinq-mail.io");
1908
+ }
1909
+ }
1910
+ const startTime = Date.now();
1911
+ let timeout = 60000;
1912
+ if (options && options.timeout) {
1913
+ timeout = options.timeout;
1914
+ }
1915
+ const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1916
+ const request = {
1917
+ method: "POST",
1918
+ url: serviceUrl,
1919
+ headers: {
1920
+ "Content-Type": "application/json",
1921
+ Authorization: `Bearer ${process.env.TOKEN}`,
1922
+ },
1923
+ data: JSON.stringify({
1924
+ email: emailAddress,
1925
+ }),
1926
+ };
1927
+ let errorCount = 0;
1928
+ while (true) {
1929
+ try {
1930
+ let result = await this.context.api.request(request);
1931
+ // the response body expected to be the following:
1932
+ // {
1933
+ // "status": true,
1934
+ // "content": {
1935
+ // "url": "",
1936
+ // "code": "112112",
1937
+ // "name": "generate_link_or_code"
1938
+ // }
1939
+ //}
1940
+ if ((result && result.data, result.data.status === true)) {
1941
+ let codeOrUrlFound = false;
1942
+ let emailCode = null;
1943
+ let emailUrl = null;
1944
+ // check if a code is returned
1945
+ if (result.data.content && result.data.content.code) {
1946
+ let code = result.data.content.code;
1947
+ this.setTestData({ emailCode: code }, world);
1948
+ this.logger.info("set test data: emailCode = " + code);
1949
+ emailCode = code;
1950
+ codeOrUrlFound = true;
1951
+ }
1952
+ // check if a url is returned
1953
+ if (result.data.content && result.data.content.url) {
1954
+ let url = result.data.content.url;
1955
+ this.setTestData({ emailUrl: url }, world);
1956
+ this.logger.info("set test data: emailUrl = " + url);
1957
+ emailUrl = url;
1958
+ codeOrUrlFound = true;
1959
+ }
1960
+ if (codeOrUrlFound) {
1961
+ return { emailUrl, emailCode };
1962
+ }
1963
+ else {
1964
+ this.logger.info("an email received but no code or url found");
1965
+ }
1966
+ }
1967
+ }
1968
+ catch (e) {
1969
+ errorCount++;
1970
+ if (errorCount > 3) {
1971
+ throw e;
1972
+ }
1973
+ // ignore
1974
+ }
1975
+ // check if the timeout is reached
1976
+ if (Date.now() - startTime > timeout) {
1977
+ throw new Error("timeout reached");
1978
+ }
1979
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1980
+ }
1981
+ }
1723
1982
  async _highlightElements(scope, css) {
1724
1983
  try {
1725
1984
  if (!scope) {
@@ -1901,8 +2160,10 @@ class StableBrowser {
1901
2160
  const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
1902
2161
  await this._highlightElements(frame, dataAttribute);
1903
2162
  const element = await frame.$(dataAttribute);
1904
- await this.scrollIfNeeded(element, info);
1905
- await element.dispatchEvent("bvt_verify_page_contains_text");
2163
+ if (element) {
2164
+ await this.scrollIfNeeded(element, info);
2165
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2166
+ }
1906
2167
  }
1907
2168
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1908
2169
  return info;
@@ -1940,6 +2201,16 @@ class StableBrowser {
1940
2201
  });
1941
2202
  }
1942
2203
  }
2204
+ _getServerUrl() {
2205
+ let serviceUrl = "https://api.blinq.io";
2206
+ if (process.env.NODE_ENV_BLINQ === "dev") {
2207
+ serviceUrl = "https://dev.api.blinq.io";
2208
+ }
2209
+ else if (process.env.NODE_ENV_BLINQ === "stage") {
2210
+ serviceUrl = "https://stage.api.blinq.io";
2211
+ }
2212
+ return serviceUrl;
2213
+ }
1943
2214
  async visualVerification(text, options = {}, world = null) {
1944
2215
  const startTime = Date.now();
1945
2216
  let error = null;
@@ -1954,13 +2225,7 @@ class StableBrowser {
1954
2225
  throw new Error("TOKEN is not set");
1955
2226
  }
1956
2227
  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
- }
2228
+ let serviceUrl = this._getServerUrl();
1964
2229
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1965
2230
  info.screenshotPath = screenshotPath;
1966
2231
  const screenshot = await this.takeScreenshot();
@@ -2109,7 +2374,16 @@ class StableBrowser {
2109
2374
  let screenshotId = null;
2110
2375
  let screenshotPath = null;
2111
2376
  const info = {};
2112
- info.log = "***** analyze table " + selectors.element_name + " query " + query + " operator " + operator + " value " + value + " *****\n";
2377
+ info.log =
2378
+ "***** analyze table " +
2379
+ selectors.element_name +
2380
+ " query " +
2381
+ query +
2382
+ " operator " +
2383
+ operator +
2384
+ " value " +
2385
+ value +
2386
+ " *****\n";
2113
2387
  info.operation = "analyzeTable";
2114
2388
  info.selectors = selectors;
2115
2389
  info.query = query;
@@ -2343,13 +2617,6 @@ class StableBrowser {
2343
2617
  const info = {};
2344
2618
  try {
2345
2619
  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
2620
  }
2354
2621
  catch (e) {
2355
2622
  console.log(".");