automation_model 1.0.365 → 1.0.366-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,65 @@ 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
+ if ((locator === null || locator === void 0 ? void 0 : locator.engine) && (locator === null || locator === void 0 ? void 0 : locator.score) <= 520) {
235
+ let selector = locator.selector.replace(/"/g, '\\"');
236
+ if (locator.engine === "internal:att") {
237
+ selector = `[${selector}]`;
238
+ }
239
+ const locator = scope.locator(`${locator.engine}="${selector}"`);
240
+ return locator;
188
241
  }
189
242
  throw new Error("unknown locator type");
190
243
  }
@@ -309,10 +362,13 @@ class StableBrowser {
309
362
  }
310
363
  async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
311
364
  let locatorSearch = selectorHierarchy[index];
312
- info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
365
+ //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
313
366
  let locator = null;
314
367
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
315
368
  let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
369
+ if (!locatorString) {
370
+ return;
371
+ }
316
372
  locator = this._getLocator({ css: locatorString }, scope, _params);
317
373
  }
318
374
  else if (locatorSearch.text) {
@@ -334,7 +390,7 @@ class StableBrowser {
334
390
  // cssHref = true;
335
391
  // }
336
392
  let count = await locator.count();
337
- info.log += "total elements found " + count + "\n";
393
+ //info.log += "total elements found " + count + "\n";
338
394
  //let visibleCount = 0;
339
395
  let visibleLocator = null;
340
396
  if (locatorSearch.index && locatorSearch.index < count) {
@@ -344,16 +400,20 @@ class StableBrowser {
344
400
  for (let j = 0; j < count; j++) {
345
401
  let visible = await locator.nth(j).isVisible();
346
402
  const enabled = await locator.nth(j).isEnabled();
347
- info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
348
403
  if (!visibleOnly) {
349
404
  visible = true;
350
405
  }
351
406
  if (visible && enabled) {
352
407
  foundLocators.push(locator.nth(j));
353
- // if (cssHref) {
354
- // info.log += "css href locator found, will ignore all others" + "\n";
355
- // break;
356
- // }
408
+ }
409
+ else {
410
+ if (!info.printMessages) {
411
+ info.printMessages = {};
412
+ }
413
+ if (!info.printMessages[j.toString()]) {
414
+ info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
415
+ info.printMessages[j.toString()] = true;
416
+ }
357
417
  }
358
418
  }
359
419
  }
@@ -379,15 +439,30 @@ class StableBrowser {
379
439
  }
380
440
  if (result.foundElements.length > 0) {
381
441
  // need to handle popup
382
- let dialogCloseLocator = this._getLocator(this.configuration.popupHandlers[result.locatorIndex].close_dialog_locator, scope, _params);
383
- await dialogCloseLocator.click();
384
- return { rerun: true };
442
+ const closeHandlerGroup = [];
443
+ closeHandlerGroup.push(this.configuration.popupHandlers[result.locatorIndex].close_dialog_locator);
444
+ for (let i = 0; i < scopes.length; i++) {
445
+ result = await this._scanLocatorsGroup(closeHandlerGroup, scopes[i], _params, info, true);
446
+ if (result.foundElements.length > 0) {
447
+ break;
448
+ }
449
+ }
450
+ if (result.foundElements.length > 0) {
451
+ let dialogCloseLocator = result.foundElements[0].locator;
452
+ await dialogCloseLocator.click();
453
+ return { rerun: true };
454
+ }
385
455
  }
386
456
  }
387
457
  return { rerun: false };
388
458
  }
389
459
  async _locate(selectors, info, _params, timeout = 30000) {
390
460
  for (let i = 0; i < 3; i++) {
461
+ info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
462
+ for (let j = 0; j < selectors.locators.length; j++) {
463
+ let selector = selectors.locators[j];
464
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
465
+ }
391
466
  let element = await this._locate_internal(selectors, info, _params, timeout);
392
467
  if (!element.rerun) {
393
468
  return element;
@@ -403,6 +478,7 @@ class StableBrowser {
403
478
  //let arrayMode = Array.isArray(selectors);
404
479
  let scope = this.page;
405
480
  if (selectors.iframe_src || selectors.frameLocators) {
481
+ info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
406
482
  while (true) {
407
483
  let frameFound = false;
408
484
  if (selectors.frameLocators) {
@@ -463,19 +539,25 @@ class StableBrowser {
463
539
  if (popupResult.rerun) {
464
540
  return popupResult;
465
541
  }
466
- info.log += "scanning locators in priority 1" + "\n";
542
+ // info.log += "scanning locators in priority 1" + "\n";
543
+ let onlyPriority3 = selectorsLocators[0].priority === 3;
467
544
  result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
468
545
  if (result.foundElements.length === 0) {
469
- info.log += "scanning locators in priority 2" + "\n";
546
+ // info.log += "scanning locators in priority 2" + "\n";
470
547
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
471
548
  }
472
- if (result.foundElements.length === 0 && !highPriorityOnly) {
473
- info.log += "scanning locators in priority 3" + "\n";
549
+ if (result.foundElements.length === 0 && onlyPriority3) {
474
550
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
475
551
  }
552
+ else {
553
+ if (result.foundElements.length === 0 && !highPriorityOnly) {
554
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
555
+ }
556
+ }
476
557
  let foundElements = result.foundElements;
477
558
  if (foundElements.length === 1 && foundElements[0].unique) {
478
559
  info.box = foundElements[0].box;
560
+ info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
479
561
  return foundElements[0].locator;
480
562
  }
481
563
  //info.log += "total elements found " + foundElements.length);
@@ -505,6 +587,7 @@ class StableBrowser {
505
587
  }
506
588
  }
507
589
  if (maxCountElement) {
590
+ info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
508
591
  info.box = await maxCountElement.locator.boundingBox();
509
592
  return maxCountElement.locator;
510
593
  }
@@ -513,9 +596,11 @@ class StableBrowser {
513
596
  break;
514
597
  }
515
598
  if (performance.now() - startTime > highPriorityTimeout) {
599
+ info.log += "high priority timeout, will try all elements" + "\n";
516
600
  highPriorityOnly = false;
517
601
  }
518
602
  if (performance.now() - startTime > visibleOnlyTimeout) {
603
+ info.log += "visible only timeout, will try all elements" + "\n";
519
604
  visibleOnly = false;
520
605
  }
521
606
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -560,7 +645,7 @@ class StableBrowser {
560
645
  this._validateSelectors(selectors);
561
646
  const startTime = Date.now();
562
647
  const info = {};
563
- info.log = "";
648
+ info.log = "***** click on " + selectors.element_name + " *****\n";
564
649
  info.operation = "click";
565
650
  info.selectors = selectors;
566
651
  let error = null;
@@ -867,71 +952,45 @@ class StableBrowser {
867
952
  });
868
953
  }
869
954
  }
870
- async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
955
+ async setInputValue(selectors, value, _params = null, options = {}, world = null) {
956
+ // set input value for non fillable inputs like date, time, range, color, etc.
871
957
  this._validateSelectors(selectors);
872
958
  const startTime = Date.now();
873
- let error = null;
874
- let screenshotId = null;
875
- let screenshotPath = null;
876
959
  const info = {};
877
- info.log = "";
878
- info.operation = Types.SET_DATE_TIME;
960
+ info.log = "***** set input value " + selectors.element_name + " *****\n";
961
+ info.operation = "setInputValue";
879
962
  info.selectors = selectors;
963
+ value = this._fixUsingParams(value, _params);
880
964
  info.value = value;
965
+ let error = null;
966
+ let screenshotId = null;
967
+ let screenshotPath = null;
881
968
  try {
882
969
  value = await this._replaceWithLocalData(value, this);
883
970
  let element = await this._locate(selectors, info, _params);
884
- //insert red border around the element
885
971
  await this.scrollIfNeeded(element, info);
886
972
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
887
973
  await this._highlightElements(element);
888
974
  try {
889
- await element.click();
890
- await new Promise((resolve) => setTimeout(resolve, 500));
891
- if (format) {
892
- value = dayjs(value).format(format);
893
- await element.fill(value);
894
- }
895
- else {
896
- const dateTimeValue = await getDateTimeValue({ value, element });
897
- await element.evaluateHandle((el, dateTimeValue) => {
898
- el.value = ""; // clear input
899
- el.value = dateTimeValue;
900
- }, dateTimeValue);
901
- }
902
- if (enter) {
903
- await new Promise((resolve) => setTimeout(resolve, 2000));
904
- await this.page.keyboard.press("Enter");
905
- await this.waitForPageLoad();
906
- }
975
+ await element.evaluateHandle((el, value) => {
976
+ el.value = value;
977
+ }, value);
907
978
  }
908
979
  catch (error) {
909
- //await this.closeUnexpectedPopups();
910
- this.logger.error("setting date time input failed " + JSON.stringify(info));
911
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
980
+ this.logger.error("setInputValue failed, will try again");
981
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
912
982
  info.screenshotPath = screenshotPath;
913
983
  Object.assign(error, { info: info });
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
- }
984
+ await element.evaluateHandle((el, value) => {
985
+ el.value = value;
986
+ });
932
987
  }
933
988
  }
934
- catch (error) {
989
+ catch (e) {
990
+ this.logger.error("setInputValue failed " + info.log);
991
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
992
+ info.screenshotPath = screenshotPath;
993
+ Object.assign(e, { info: info });
935
994
  error = e;
936
995
  throw e;
937
996
  }
@@ -939,10 +998,10 @@ class StableBrowser {
939
998
  const endTime = Date.now();
940
999
  this._reportToWorld(world, {
941
1000
  element_name: selectors.element_name,
942
- type: Types.SET_DATE_TIME,
943
- screenshotId,
1001
+ type: Types.SET_INPUT,
1002
+ text: `Set input value`,
944
1003
  value: value,
945
- text: `setDateTime input with value: ${value}`,
1004
+ screenshotId,
946
1005
  result: error
947
1006
  ? {
948
1007
  status: "FAILED",
@@ -959,7 +1018,7 @@ class StableBrowser {
959
1018
  });
960
1019
  }
961
1020
  }
962
- async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
1021
+ async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
963
1022
  this._validateSelectors(selectors);
964
1023
  const startTime = Date.now();
965
1024
  let error = null;
@@ -971,6 +1030,7 @@ class StableBrowser {
971
1030
  info.selectors = selectors;
972
1031
  info.value = value;
973
1032
  try {
1033
+ value = await this._replaceWithLocalData(value, this);
974
1034
  let element = await this._locate(selectors, info, _params);
975
1035
  //insert red border around the element
976
1036
  await this.scrollIfNeeded(element, info);
@@ -979,28 +1039,51 @@ class StableBrowser {
979
1039
  try {
980
1040
  await element.click();
981
1041
  await new Promise((resolve) => setTimeout(resolve, 500));
982
- const dateTimeValue = await getDateTimeValue({ value, element });
983
- await element.evaluateHandle((el, dateTimeValue) => {
984
- el.value = ""; // clear input
985
- el.value = dateTimeValue;
986
- }, dateTimeValue);
1042
+ if (format) {
1043
+ value = dayjs(value).format(format);
1044
+ await element.fill(value);
1045
+ }
1046
+ else {
1047
+ const dateTimeValue = await getDateTimeValue({ value, element });
1048
+ await element.evaluateHandle((el, dateTimeValue) => {
1049
+ el.value = ""; // clear input
1050
+ el.value = dateTimeValue;
1051
+ }, dateTimeValue);
1052
+ }
1053
+ if (enter) {
1054
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1055
+ await this.page.keyboard.press("Enter");
1056
+ await this.waitForPageLoad();
1057
+ }
987
1058
  }
988
- catch (error) {
1059
+ catch (err) {
989
1060
  //await this.closeUnexpectedPopups();
990
1061
  this.logger.error("setting date time input failed " + JSON.stringify(info));
991
- this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
1062
+ this.logger.info("Trying again");
1063
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
992
1064
  info.screenshotPath = screenshotPath;
993
- Object.assign(error, { info: info });
1065
+ Object.assign(err, { info: info });
994
1066
  await element.click();
995
1067
  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);
1068
+ if (format) {
1069
+ value = dayjs(value).format(format);
1070
+ await element.fill(value);
1071
+ }
1072
+ else {
1073
+ const dateTimeValue = await getDateTimeValue({ value, element });
1074
+ await element.evaluateHandle((el, dateTimeValue) => {
1075
+ el.value = ""; // clear input
1076
+ el.value = dateTimeValue;
1077
+ }, dateTimeValue);
1078
+ }
1079
+ if (enter) {
1080
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1081
+ await this.page.keyboard.press("Enter");
1082
+ await this.waitForPageLoad();
1083
+ }
1001
1084
  }
1002
1085
  }
1003
- catch (error) {
1086
+ catch (e) {
1004
1087
  error = e;
1005
1088
  throw e;
1006
1089
  }
@@ -1035,7 +1118,7 @@ class StableBrowser {
1035
1118
  let screenshotId = null;
1036
1119
  let screenshotPath = null;
1037
1120
  const info = {};
1038
- info.log = "";
1121
+ info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1039
1122
  info.operation = "clickType";
1040
1123
  info.selectors = selectors;
1041
1124
  const newValue = await this._replaceWithLocalData(_value, world);
@@ -1050,20 +1133,32 @@ class StableBrowser {
1050
1133
  await this.scrollIfNeeded(element, info);
1051
1134
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1052
1135
  await this._highlightElements(element);
1053
- try {
1054
- let currentValue = await element.inputValue();
1055
- if (currentValue) {
1056
- await element.fill("");
1136
+ if (options === null || options === undefined || !options.press) {
1137
+ try {
1138
+ let currentValue = await element.inputValue();
1139
+ if (currentValue) {
1140
+ await element.fill("");
1141
+ }
1142
+ }
1143
+ catch (e) {
1144
+ this.logger.info("unable to clear input value");
1057
1145
  }
1058
1146
  }
1059
- catch (e) {
1060
- this.logger.info("unable to clear input value");
1061
- }
1062
- try {
1063
- await element.click({ timeout: 5000 });
1147
+ if (options === null || options === undefined || options.press) {
1148
+ try {
1149
+ await element.click({ timeout: 5000 });
1150
+ }
1151
+ catch (e) {
1152
+ await element.dispatchEvent("click");
1153
+ }
1064
1154
  }
1065
- catch (e) {
1066
- await element.dispatchEvent("click");
1155
+ else {
1156
+ try {
1157
+ await element.focus();
1158
+ }
1159
+ catch (e) {
1160
+ await element.dispatchEvent("focus");
1161
+ }
1067
1162
  }
1068
1163
  await new Promise((resolve) => setTimeout(resolve, 500));
1069
1164
  const valueSegment = _value.split("&&");
@@ -1143,7 +1238,7 @@ class StableBrowser {
1143
1238
  let screenshotId = null;
1144
1239
  let screenshotPath = null;
1145
1240
  const info = {};
1146
- info.log = "";
1241
+ info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1147
1242
  info.operation = "fill";
1148
1243
  info.selectors = selectors;
1149
1244
  info.value = value;
@@ -1259,7 +1354,8 @@ class StableBrowser {
1259
1354
  let screenshotId = null;
1260
1355
  let screenshotPath = null;
1261
1356
  const info = {};
1262
- info.log = "";
1357
+ info.log =
1358
+ "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1263
1359
  info.operation = "containsPattern";
1264
1360
  info.selectors = selectors;
1265
1361
  info.value = text;
@@ -1325,7 +1421,7 @@ class StableBrowser {
1325
1421
  let screenshotId = null;
1326
1422
  let screenshotPath = null;
1327
1423
  const info = {};
1328
- info.log = "";
1424
+ info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1329
1425
  info.operation = "containsText";
1330
1426
  info.selectors = selectors;
1331
1427
  const newValue = await this._replaceWithLocalData(text, world);
@@ -1429,15 +1525,62 @@ class StableBrowser {
1429
1525
  // save the data to the file
1430
1526
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1431
1527
  }
1528
+ _getDataFilePath(fileName) {
1529
+ let dataFile = path.join(this.project_path, "data", fileName);
1530
+ if (fs.existsSync(dataFile)) {
1531
+ return dataFile;
1532
+ }
1533
+ dataFile = path.join(this.project_path, fileName);
1534
+ if (fs.existsSync(dataFile)) {
1535
+ return dataFile;
1536
+ }
1537
+ throw new Error("data file not found " + fileName);
1538
+ }
1539
+ _parseCSVSync(filePath) {
1540
+ const data = fs.readFileSync(filePath, "utf8");
1541
+ const results = [];
1542
+ return new Promise((resolve, reject) => {
1543
+ const readableStream = new Readable();
1544
+ readableStream._read = () => { }; // _read is required but you can noop it
1545
+ readableStream.push(data);
1546
+ readableStream.push(null);
1547
+ readableStream
1548
+ .pipe(csv())
1549
+ .on("data", (data) => results.push(data))
1550
+ .on("end", () => resolve(results))
1551
+ .on("error", (error) => reject(error));
1552
+ });
1553
+ }
1432
1554
  loadTestData(type, dataSelector, world = null) {
1433
1555
  switch (type) {
1434
1556
  case "users":
1435
- // check if file users.json exists
1436
- if (!fs.existsSync(path.join(this.project_path, "users.json"))) {
1437
- throw new Error("users.json file not found");
1557
+ // get the users.json file path
1558
+ let dataFile = this._getDataFilePath("users.json");
1559
+ // read the file and return the data
1560
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1561
+ for (let i = 0; i < users.length; i++) {
1562
+ if (users[i].username === dataSelector) {
1563
+ const userObj = {
1564
+ username: users[i].username,
1565
+ password: "secret:" + users[i].password,
1566
+ totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
1567
+ };
1568
+ this.setTestData(userObj, world);
1569
+ return userObj;
1570
+ }
1438
1571
  }
1572
+ throw new Error("user not found " + dataSelector);
1573
+ default:
1574
+ throw new Error("unknown type " + type);
1575
+ }
1576
+ }
1577
+ async loadTestDataAsync(type, dataSelector, world = null) {
1578
+ switch (type) {
1579
+ case "users": {
1580
+ // get the users.json file path
1581
+ let dataFile = this._getDataFilePath("users.json");
1439
1582
  // read the file and return the data
1440
- const users = JSON.parse(fs.readFileSync(path.join(this.project_path, "users.json"), "utf8"));
1583
+ const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1441
1584
  for (let i = 0; i < users.length; i++) {
1442
1585
  if (users[i].username === dataSelector) {
1443
1586
  const userObj = {
@@ -1450,6 +1593,29 @@ class StableBrowser {
1450
1593
  }
1451
1594
  }
1452
1595
  throw new Error("user not found " + dataSelector);
1596
+ }
1597
+ case "csv": {
1598
+ // 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
1599
+ const parts = dataSelector.split(":");
1600
+ let rowNumber = 0;
1601
+ if (parts.length > 1) {
1602
+ rowNumber = parseInt(parts[1]);
1603
+ }
1604
+ let dataFile = this._getDataFilePath(parts[0]);
1605
+ const results = await this._parseCSVSync(dataFile);
1606
+ // result stracture:
1607
+ // [
1608
+ // { NAME: 'Daffy Duck', AGE: '24' },
1609
+ // { NAME: 'Bugs Bunny', AGE: '22' }
1610
+ // ]
1611
+ // verify the row number is within the range
1612
+ if (rowNumber >= results.length) {
1613
+ throw new Error("row number is out of range " + rowNumber);
1614
+ }
1615
+ const data = results[rowNumber];
1616
+ this.setTestData(data, world);
1617
+ return data;
1618
+ }
1453
1619
  default:
1454
1620
  throw new Error("unknown type " + type);
1455
1621
  }
@@ -1588,7 +1754,7 @@ class StableBrowser {
1588
1754
  let screenshotPath = null;
1589
1755
  await new Promise((resolve) => setTimeout(resolve, 2000));
1590
1756
  const info = {};
1591
- info.log = "";
1757
+ info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1592
1758
  info.operation = "verify";
1593
1759
  info.selectors = selectors;
1594
1760
  try {
@@ -1641,7 +1807,7 @@ class StableBrowser {
1641
1807
  let screenshotPath = null;
1642
1808
  await new Promise((resolve) => setTimeout(resolve, 2000));
1643
1809
  const info = {};
1644
- info.log = "";
1810
+ info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1645
1811
  info.operation = "extract";
1646
1812
  info.selectors = selectors;
1647
1813
  try {
@@ -1666,7 +1832,8 @@ class StableBrowser {
1666
1832
  if (world) {
1667
1833
  world[variable] = info.value;
1668
1834
  }
1669
- this.logger.info("world." + variable + "=" + info.value);
1835
+ this.setTestData({ [variable]: info.value }, world);
1836
+ this.logger.info("set test data: " + variable + "=" + info.value);
1670
1837
  return info;
1671
1838
  }
1672
1839
  catch (e) {
@@ -1703,6 +1870,91 @@ class StableBrowser {
1703
1870
  });
1704
1871
  }
1705
1872
  }
1873
+ async extractEmailData(emailAddress, options, world) {
1874
+ if (!emailAddress) {
1875
+ throw new Error("email address is null");
1876
+ }
1877
+ // check if address contain @
1878
+ if (emailAddress.indexOf("@") === -1) {
1879
+ emailAddress = emailAddress + "@blinq-mail.io";
1880
+ }
1881
+ else {
1882
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1883
+ throw new Error("email address should end with @blinq-mail.io");
1884
+ }
1885
+ }
1886
+ const startTime = Date.now();
1887
+ let timeout = 60000;
1888
+ if (options && options.timeout) {
1889
+ timeout = options.timeout;
1890
+ }
1891
+ const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1892
+ const request = {
1893
+ method: "POST",
1894
+ url: serviceUrl,
1895
+ headers: {
1896
+ "Content-Type": "application/json",
1897
+ Authorization: `Bearer ${process.env.TOKEN}`,
1898
+ },
1899
+ data: JSON.stringify({
1900
+ email: emailAddress,
1901
+ }),
1902
+ };
1903
+ let errorCount = 0;
1904
+ while (true) {
1905
+ try {
1906
+ let result = await this.context.api.request(request);
1907
+ // the response body expected to be the following:
1908
+ // {
1909
+ // "status": true,
1910
+ // "content": {
1911
+ // "url": "",
1912
+ // "code": "112112",
1913
+ // "name": "generate_link_or_code"
1914
+ // }
1915
+ //}
1916
+ if ((result && result.data, result.data.status === true)) {
1917
+ let codeOrUrlFound = false;
1918
+ let emailCode = null;
1919
+ let emailUrl = null;
1920
+ // check if a code is returned
1921
+ if (result.data.content && result.data.content.code) {
1922
+ let code = result.data.content.code;
1923
+ this.setTestData({ emailCode: code }, world);
1924
+ this.logger.info("set test data: emailCode = " + code);
1925
+ emailCode = code;
1926
+ codeOrUrlFound = true;
1927
+ }
1928
+ // check if a url is returned
1929
+ if (result.data.content && result.data.content.url) {
1930
+ let url = result.data.content.url;
1931
+ this.setTestData({ emailUrl: url }, world);
1932
+ this.logger.info("set test data: emailUrl = " + url);
1933
+ emailUrl = url;
1934
+ codeOrUrlFound = true;
1935
+ }
1936
+ if (codeOrUrlFound) {
1937
+ return { emailUrl, emailCode };
1938
+ }
1939
+ else {
1940
+ this.logger.info("an email received but no code or url found");
1941
+ }
1942
+ }
1943
+ }
1944
+ catch (e) {
1945
+ errorCount++;
1946
+ if (errorCount > 3) {
1947
+ throw e;
1948
+ }
1949
+ // ignore
1950
+ }
1951
+ // check if the timeout is reached
1952
+ if (Date.now() - startTime > timeout) {
1953
+ throw new Error("timeout reached");
1954
+ }
1955
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1956
+ }
1957
+ }
1706
1958
  async _highlightElements(scope, css) {
1707
1959
  try {
1708
1960
  if (!scope) {
@@ -1774,7 +2026,7 @@ class StableBrowser {
1774
2026
  let screenshotPath = null;
1775
2027
  await new Promise((resolve) => setTimeout(resolve, 2000));
1776
2028
  const info = {};
1777
- info.log = "";
2029
+ info.log = "***** verify page path " + pathPart + " *****\n";
1778
2030
  info.operation = "verifyPagePath";
1779
2031
  const newValue = await this._replaceWithLocalData(pathPart, world);
1780
2032
  if (newValue !== pathPart) {
@@ -1835,7 +2087,7 @@ class StableBrowser {
1835
2087
  let screenshotPath = null;
1836
2088
  await new Promise((resolve) => setTimeout(resolve, 2000));
1837
2089
  const info = {};
1838
- info.log = "";
2090
+ info.log = "***** verify text " + text + " exists in page *****\n";
1839
2091
  info.operation = "verifyTextExistInPage";
1840
2092
  const newValue = await this._replaceWithLocalData(text, world);
1841
2093
  if (newValue !== text) {
@@ -1884,8 +2136,10 @@ class StableBrowser {
1884
2136
  const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
1885
2137
  await this._highlightElements(frame, dataAttribute);
1886
2138
  const element = await frame.$(dataAttribute);
1887
- await this.scrollIfNeeded(element, info);
1888
- await element.dispatchEvent("bvt_verify_page_contains_text");
2139
+ if (element) {
2140
+ await this.scrollIfNeeded(element, info);
2141
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2142
+ }
1889
2143
  }
1890
2144
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1891
2145
  return info;
@@ -1923,6 +2177,16 @@ class StableBrowser {
1923
2177
  });
1924
2178
  }
1925
2179
  }
2180
+ _getServerUrl() {
2181
+ let serviceUrl = "https://api.blinq.io";
2182
+ if (process.env.NODE_ENV_BLINQ === "dev") {
2183
+ serviceUrl = "https://dev.api.blinq.io";
2184
+ }
2185
+ else if (process.env.NODE_ENV_BLINQ === "stage") {
2186
+ serviceUrl = "https://stage.api.blinq.io";
2187
+ }
2188
+ return serviceUrl;
2189
+ }
1926
2190
  async visualVerification(text, options = {}, world = null) {
1927
2191
  const startTime = Date.now();
1928
2192
  let error = null;
@@ -1937,13 +2201,7 @@ class StableBrowser {
1937
2201
  throw new Error("TOKEN is not set");
1938
2202
  }
1939
2203
  try {
1940
- let serviceUrl = "https://api.blinq.io";
1941
- if (process.env.NODE_ENV_BLINQ === "dev") {
1942
- serviceUrl = "https://dev.api.blinq.io";
1943
- }
1944
- else if (process.env.NODE_ENV_BLINQ === "stage") {
1945
- serviceUrl = "https://stage.api.blinq.io";
1946
- }
2204
+ let serviceUrl = this._getServerUrl();
1947
2205
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1948
2206
  info.screenshotPath = screenshotPath;
1949
2207
  const screenshot = await this.takeScreenshot();
@@ -2092,7 +2350,16 @@ class StableBrowser {
2092
2350
  let screenshotId = null;
2093
2351
  let screenshotPath = null;
2094
2352
  const info = {};
2095
- info.log = "";
2353
+ info.log =
2354
+ "***** analyze table " +
2355
+ selectors.element_name +
2356
+ " query " +
2357
+ query +
2358
+ " operator " +
2359
+ operator +
2360
+ " value " +
2361
+ value +
2362
+ " *****\n";
2096
2363
  info.operation = "analyzeTable";
2097
2364
  info.selectors = selectors;
2098
2365
  info.query = query;
@@ -2326,13 +2593,6 @@ class StableBrowser {
2326
2593
  const info = {};
2327
2594
  try {
2328
2595
  await this.page.close();
2329
- if (this.context && this.context.pages && this.context.pages.length > 0) {
2330
- this.context.pages.pop();
2331
- this.page = this.context.pages[this.context.pages.length - 1];
2332
- this.context.page = this.page;
2333
- let title = await this.page.title();
2334
- console.log("Switched to page " + title);
2335
- }
2336
2596
  }
2337
2597
  catch (e) {
2338
2598
  console.log(".");