automation_model 1.0.698-dev → 1.0.698-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.
Files changed (55) hide show
  1. package/README.md +4 -1
  2. package/lib/analyze_helper.js.map +1 -1
  3. package/lib/api.d.ts +0 -1
  4. package/lib/api.js.map +1 -1
  5. package/lib/auto_page.d.ts +4 -2
  6. package/lib/auto_page.js +180 -89
  7. package/lib/auto_page.js.map +1 -1
  8. package/lib/browser_manager.d.ts +1 -0
  9. package/lib/browser_manager.js +54 -9
  10. package/lib/browser_manager.js.map +1 -1
  11. package/lib/bruno.js.map +1 -1
  12. package/lib/command_common.d.ts +1 -1
  13. package/lib/command_common.js +18 -1
  14. package/lib/command_common.js.map +1 -1
  15. package/lib/date_time.js.map +1 -1
  16. package/lib/drawRect.js.map +1 -1
  17. package/lib/environment.d.ts +1 -0
  18. package/lib/environment.js +1 -0
  19. package/lib/environment.js.map +1 -1
  20. package/lib/error-messages.js.map +1 -1
  21. package/lib/file_checker.js +129 -25
  22. package/lib/file_checker.js.map +1 -1
  23. package/lib/find_function.js.map +1 -1
  24. package/lib/init_browser.js +4 -4
  25. package/lib/init_browser.js.map +1 -1
  26. package/lib/locate_element.js.map +1 -1
  27. package/lib/locator.d.ts +1 -0
  28. package/lib/locator.js +10 -3
  29. package/lib/locator.js.map +1 -1
  30. package/lib/locator_log.js.map +1 -1
  31. package/lib/network.d.ts +2 -0
  32. package/lib/network.js +232 -4
  33. package/lib/network.js.map +1 -1
  34. package/lib/route.d.ts +21 -0
  35. package/lib/route.js +443 -0
  36. package/lib/route.js.map +1 -0
  37. package/lib/scripts/axe.mini.js +3 -3
  38. package/lib/snapshot_validation.d.ts +4 -2
  39. package/lib/snapshot_validation.js +160 -42
  40. package/lib/snapshot_validation.js.map +1 -1
  41. package/lib/stable_browser.d.ts +59 -26
  42. package/lib/stable_browser.js +904 -202
  43. package/lib/stable_browser.js.map +1 -1
  44. package/lib/table.d.ts +9 -7
  45. package/lib/table.js +82 -12
  46. package/lib/table.js.map +1 -1
  47. package/lib/table_analyze.js.map +1 -1
  48. package/lib/table_helper.js.map +1 -1
  49. package/lib/test_context.d.ts +1 -0
  50. package/lib/test_context.js +1 -0
  51. package/lib/test_context.js.map +1 -1
  52. package/lib/utils.d.ts +2 -1
  53. package/lib/utils.js +74 -63
  54. package/lib/utils.js.map +1 -1
  55. package/package.json +12 -7
@@ -10,7 +10,7 @@ import { getDateTimeValue } from "./date_time.js";
10
10
  import drawRectangle from "./drawRect.js";
11
11
  //import { closeUnexpectedPopups } from "./popups.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
14
14
  import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
@@ -19,35 +19,41 @@ import { getTestData } from "./auto_page.js";
19
19
  import { locate_element } from "./locate_element.js";
20
20
  import { randomUUID } from "crypto";
21
21
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
22
- import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
+ import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
23
23
  import { LocatorLog } from "./locator_log.js";
24
24
  import axios from "axios";
25
25
  import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
- import { snapshotValidation } from "./snapshot_validation.js";
26
+ import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
27
27
  import { loadBrunoParams } from "./bruno.js";
28
+ import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
28
29
  export const Types = {
29
30
  CLICK: "click_element",
30
31
  WAIT_ELEMENT: "wait_element",
31
32
  NAVIGATE: "navigate",
33
+ GO_BACK: "go_back",
34
+ GO_FORWARD: "go_forward",
32
35
  FILL: "fill_element",
33
- EXECUTE: "execute_page_method",
34
- OPEN: "open_environment",
36
+ EXECUTE: "execute_page_method", //
37
+ OPEN: "open_environment", //
35
38
  COMPLETE: "step_complete",
36
39
  ASK: "information_needed",
37
- GET_PAGE_STATUS: "get_page_status",
38
- CLICK_ROW_ACTION: "click_row_action",
40
+ GET_PAGE_STATUS: "get_page_status", ///
41
+ CLICK_ROW_ACTION: "click_row_action", //
39
42
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
40
43
  VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
41
44
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
42
45
  ANALYZE_TABLE: "analyze_table",
43
- SELECT: "select_combobox",
46
+ SELECT: "select_combobox", //
47
+ VERIFY_PROPERTY: "verify_element_property",
44
48
  VERIFY_PAGE_PATH: "verify_page_path",
49
+ VERIFY_PAGE_TITLE: "verify_page_title",
45
50
  TYPE_PRESS: "type_press",
46
51
  PRESS: "press_key",
47
52
  HOVER: "hover_element",
48
53
  CHECK: "check_element",
49
54
  UNCHECK: "uncheck_element",
50
55
  EXTRACT: "extract_attribute",
56
+ EXTRACT_PROPERTY: "extract_property",
51
57
  CLOSE_PAGE: "close_page",
52
58
  TABLE_OPERATION: "table_operation",
53
59
  SET_DATE_TIME: "set_date_time",
@@ -59,9 +65,13 @@ export const Types = {
59
65
  VERIFY_ATTRIBUTE: "verify_element_attribute",
60
66
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
61
67
  BRUNO: "bruno",
62
- SNAPSHOT_VALIDATION: "snapshot_validation",
63
68
  VERIFY_FILE_EXISTS: "verify_file_exists",
64
69
  SET_INPUT_FILES: "set_input_files",
70
+ SNAPSHOT_VALIDATION: "snapshot_validation",
71
+ REPORT_COMMAND: "report_command",
72
+ STEP_COMPLETE: "step_complete",
73
+ SLEEP: "sleep",
74
+ CONDITIONAL_WAIT: "conditional_wait",
65
75
  };
66
76
  export const apps = {};
67
77
  const formatElementName = (elementName) => {
@@ -73,6 +83,7 @@ class StableBrowser {
73
83
  logger;
74
84
  context;
75
85
  world;
86
+ fastMode;
76
87
  project_path = null;
77
88
  webLogFile = null;
78
89
  networkLogger = null;
@@ -81,12 +92,14 @@ class StableBrowser {
81
92
  tags = null;
82
93
  isRecording = false;
83
94
  initSnapshotTaken = false;
84
- constructor(browser, page, logger = null, context = null, world = null) {
95
+ abortedExecution = false;
96
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
85
97
  this.browser = browser;
86
98
  this.page = page;
87
99
  this.logger = logger;
88
100
  this.context = context;
89
101
  this.world = world;
102
+ this.fastMode = fastMode;
90
103
  if (!this.logger) {
91
104
  this.logger = console;
92
105
  }
@@ -115,6 +128,19 @@ class StableBrowser {
115
128
  context.pages = [this.page];
116
129
  const logFolder = path.join(this.project_path, "logs", "web");
117
130
  this.world = world;
131
+ if (this.configuration && this.configuration.fastMode === true) {
132
+ this.fastMode = true;
133
+ }
134
+ if (process.env.FAST_MODE === "true") {
135
+ // console.log("Fast mode enabled from environment variable");
136
+ this.fastMode = true;
137
+ }
138
+ if (process.env.FAST_MODE === "false") {
139
+ this.fastMode = false;
140
+ }
141
+ if (this.context) {
142
+ this.context.fastMode = this.fastMode;
143
+ }
118
144
  this.registerEventListeners(this.context);
119
145
  registerNetworkEvents(this.world, this, this.context, this.page);
120
146
  registerDownloadEvent(this.page, this.world, this.context);
@@ -125,6 +151,9 @@ class StableBrowser {
125
151
  if (!context.pageLoading) {
126
152
  context.pageLoading = { status: false };
127
153
  }
154
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
155
+ this.page.on("dialog", (dialog) => dialog.accept());
156
+ }
128
157
  context.playContext.on("page", async function (page) {
129
158
  if (this.configuration && this.configuration.closePopups === true) {
130
159
  console.log("close unexpected popups");
@@ -133,6 +162,14 @@ class StableBrowser {
133
162
  }
134
163
  context.pageLoading.status = true;
135
164
  this.page = page;
165
+ try {
166
+ if (this.configuration && this.configuration.acceptDialog) {
167
+ await page.on("dialog", (dialog) => dialog.accept());
168
+ }
169
+ }
170
+ catch (error) {
171
+ console.error("Error on dialog accept registration", error);
172
+ }
136
173
  context.page = page;
137
174
  context.pages.push(page);
138
175
  registerNetworkEvents(this.world, this, context, this.page);
@@ -184,7 +221,9 @@ class StableBrowser {
184
221
  if (newContextCreated) {
185
222
  this.registerEventListeners(this.context);
186
223
  await this.goto(this.context.environment.baseUrl);
187
- await this.waitForPageLoad();
224
+ if (!this.fastMode) {
225
+ await this.waitForPageLoad();
226
+ }
188
227
  }
189
228
  }
190
229
  async switchTab(tabTitleOrIndex) {
@@ -278,6 +317,7 @@ class StableBrowser {
278
317
  if (!url) {
279
318
  throw new Error("url is null, verify that the environment file is correct");
280
319
  }
320
+ url = await this._replaceWithLocalData(url, this.world);
281
321
  if (!url.startsWith("http")) {
282
322
  url = "https://" + url;
283
323
  }
@@ -309,6 +349,64 @@ class StableBrowser {
309
349
  await _commandFinally(state, this);
310
350
  }
311
351
  }
352
+ async goBack(options, world = null) {
353
+ const state = {
354
+ value: "",
355
+ world: world,
356
+ type: Types.GO_BACK,
357
+ text: `Browser navigate back`,
358
+ operation: "goBack",
359
+ log: "***** navigate back *****\n",
360
+ info: {},
361
+ locate: false,
362
+ scroll: false,
363
+ screenshot: false,
364
+ highlight: false,
365
+ };
366
+ try {
367
+ await _preCommand(state, this);
368
+ await this.page.goBack({
369
+ waitUntil: "load",
370
+ });
371
+ await _screenshot(state, this);
372
+ }
373
+ catch (error) {
374
+ console.error("Error on goBack", error);
375
+ _commandError(state, error, this);
376
+ }
377
+ finally {
378
+ await _commandFinally(state, this);
379
+ }
380
+ }
381
+ async goForward(options, world = null) {
382
+ const state = {
383
+ value: "",
384
+ world: world,
385
+ type: Types.GO_FORWARD,
386
+ text: `Browser navigate forward`,
387
+ operation: "goForward",
388
+ log: "***** navigate forward *****\n",
389
+ info: {},
390
+ locate: false,
391
+ scroll: false,
392
+ screenshot: false,
393
+ highlight: false,
394
+ };
395
+ try {
396
+ await _preCommand(state, this);
397
+ await this.page.goForward({
398
+ waitUntil: "load",
399
+ });
400
+ await _screenshot(state, this);
401
+ }
402
+ catch (error) {
403
+ console.error("Error on goForward", error);
404
+ _commandError(state, error, this);
405
+ }
406
+ finally {
407
+ await _commandFinally(state, this);
408
+ }
409
+ }
312
410
  async _getLocator(locator, scope, _params) {
313
411
  locator = _fixLocatorUsingParams(locator, _params);
314
412
  // locator = await this._replaceWithLocalData(locator);
@@ -422,7 +520,7 @@ class StableBrowser {
422
520
  }
423
521
  return { elementCount: tagCount, randomToken };
424
522
  }
425
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
523
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
426
524
  if (!info) {
427
525
  info = {};
428
526
  }
@@ -434,14 +532,13 @@ class StableBrowser {
434
532
  info.locatorLog = new LocatorLog(selectorHierarchy);
435
533
  }
436
534
  let locatorSearch = selectorHierarchy[index];
437
- let originalLocatorSearch = "";
438
535
  try {
439
- originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
440
- locatorSearch = JSON.parse(originalLocatorSearch);
536
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
441
537
  }
442
538
  catch (e) {
443
539
  console.error(e);
444
540
  }
541
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
445
542
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
446
543
  let locator = null;
447
544
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
@@ -489,7 +586,7 @@ class StableBrowser {
489
586
  }
490
587
  return;
491
588
  }
492
- if (info.locatorLog && count === 0) {
589
+ if (info.locatorLog && count === 0 && logErrors) {
493
590
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
494
591
  }
495
592
  for (let j = 0; j < count; j++) {
@@ -504,7 +601,7 @@ class StableBrowser {
504
601
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
505
602
  }
506
603
  }
507
- else {
604
+ else if (logErrors) {
508
605
  info.failCause.visible = visible;
509
606
  info.failCause.enabled = enabled;
510
607
  if (!info.printMessages) {
@@ -596,7 +693,7 @@ class StableBrowser {
596
693
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
597
694
  if (!element.rerun) {
598
695
  const randomToken = Math.random().toString(36).substring(7);
599
- element.evaluate((el, randomToken) => {
696
+ await element.evaluate((el, randomToken) => {
600
697
  el.setAttribute("data-blinq-id-" + randomToken, "");
601
698
  }, randomToken);
602
699
  // if (element._frame) {
@@ -649,7 +746,7 @@ class StableBrowser {
649
746
  break;
650
747
  }
651
748
  catch (error) {
652
- console.error("frame not found " + frameLocator.css);
749
+ // console.error("frame not found " + frameLocator.css);
653
750
  }
654
751
  }
655
752
  }
@@ -727,7 +824,6 @@ class StableBrowser {
727
824
  let locatorsCount = 0;
728
825
  let lazy_scroll = false;
729
826
  //let arrayMode = Array.isArray(selectors);
730
- let scope = await this._findFrameScope(selectors, timeout, info);
731
827
  let selectorsLocators = null;
732
828
  selectorsLocators = selectors.locators;
733
829
  // group selectors by priority
@@ -755,6 +851,7 @@ class StableBrowser {
755
851
  let highPriorityOnly = true;
756
852
  let visibleOnly = true;
757
853
  while (true) {
854
+ let scope = await this._findFrameScope(selectors, timeout, info);
758
855
  locatorsCount = 0;
759
856
  let result = [];
760
857
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -845,7 +942,7 @@ class StableBrowser {
845
942
  }
846
943
  throw new Error("failed to locate first element no elements found, " + info.log);
847
944
  }
848
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
945
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
849
946
  let foundElements = [];
850
947
  const result = {
851
948
  foundElements: foundElements,
@@ -864,7 +961,9 @@ class StableBrowser {
864
961
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
865
962
  }
866
963
  catch (e) {
867
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
964
+ if (logErrors) {
965
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
966
+ }
868
967
  }
869
968
  }
870
969
  if (foundLocators.length === 1) {
@@ -905,7 +1004,7 @@ class StableBrowser {
905
1004
  });
906
1005
  result.locatorIndex = i;
907
1006
  }
908
- else {
1007
+ else if (logErrors) {
909
1008
  info.failCause.foundMultiple = true;
910
1009
  if (info.locatorLog) {
911
1010
  info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
@@ -1028,7 +1127,9 @@ class StableBrowser {
1028
1127
  try {
1029
1128
  await _preCommand(state, this);
1030
1129
  await performAction("click", state.element, options, this, state, _params);
1031
- await this.waitForPageLoad();
1130
+ if (!this.fastMode) {
1131
+ await this.waitForPageLoad();
1132
+ }
1032
1133
  return state.info;
1033
1134
  }
1034
1135
  catch (e) {
@@ -1091,7 +1192,7 @@ class StableBrowser {
1091
1192
  // if (world && world.screenshot && !world.screenshotPath) {
1092
1193
  // console.log(`Highlighting while running from recorder`);
1093
1194
  await this._highlightElements(state.element);
1094
- await state.element.setChecked(checked);
1195
+ await state.element.setChecked(checked, { timeout: 2000 });
1095
1196
  await new Promise((resolve) => setTimeout(resolve, 1000));
1096
1197
  // await this._unHighlightElements(element);
1097
1198
  // }
@@ -1103,11 +1204,28 @@ class StableBrowser {
1103
1204
  this.logger.info("element did not change its state, ignoring...");
1104
1205
  }
1105
1206
  else {
1207
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1106
1208
  //await this.closeUnexpectedPopups();
1107
1209
  state.info.log += "setCheck failed, will try again" + "\n";
1108
- state.element = await this._locate(selectors, state.info, _params);
1109
- await state.element.setChecked(checked, { timeout: 5000, force: true });
1110
- await new Promise((resolve) => setTimeout(resolve, 1000));
1210
+ state.element_found = false;
1211
+ try {
1212
+ state.element = await this._locate(selectors, state.info, _params, 100);
1213
+ state.element_found = true;
1214
+ // check the check state
1215
+ }
1216
+ catch (error) {
1217
+ // element dismissed
1218
+ }
1219
+ if (state.element_found) {
1220
+ const isChecked = await state.element.isChecked();
1221
+ if (isChecked !== checked) {
1222
+ // perform click
1223
+ await state.element.click({ timeout: 2000, force: true });
1224
+ }
1225
+ else {
1226
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1227
+ }
1228
+ }
1111
1229
  }
1112
1230
  }
1113
1231
  await this.waitForPageLoad();
@@ -1403,7 +1521,9 @@ class StableBrowser {
1403
1521
  await new Promise((resolve) => setTimeout(resolve, 500));
1404
1522
  }
1405
1523
  }
1524
+ //if (!this.fastMode) {
1406
1525
  await _screenshot(state, this);
1526
+ //}
1407
1527
  if (enter === true) {
1408
1528
  await new Promise((resolve) => setTimeout(resolve, 2000));
1409
1529
  await this.page.keyboard.press("Enter");
@@ -1712,14 +1832,17 @@ class StableBrowser {
1712
1832
  throw new Error("referanceSnapshot is null");
1713
1833
  }
1714
1834
  let text = null;
1715
- if (fs.existsSync(path.join(this.project_path, "snapshots", referanceSnapshot + ".yml"))) {
1716
- text = fs.readFileSync(path.join(this.project_path, "snapshots", referanceSnapshot + ".yml"), "utf8");
1835
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1836
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1837
+ }
1838
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1839
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1717
1840
  }
1718
1841
  else if (referanceSnapshot.startsWith("yaml:")) {
1719
1842
  text = referanceSnapshot.substring(5);
1720
1843
  }
1721
1844
  else {
1722
- throw new Error("referanceSnapshot file not found: " + referanceSnapshot);
1845
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1723
1846
  }
1724
1847
  state.text = text;
1725
1848
  const newValue = await this._replaceWithLocalData(text, world);
@@ -1737,18 +1860,23 @@ class StableBrowser {
1737
1860
  scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1738
1861
  }
1739
1862
  const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1740
- matchResult = snapshotValidation(snapshot, newValue);
1863
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1741
1864
  if (matchResult.errorLine !== -1) {
1742
1865
  throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1743
1866
  }
1744
1867
  // highlight and screenshot
1868
+ try {
1869
+ await await highlightSnapshot(newValue, scope);
1870
+ await _screenshot(state, this);
1871
+ }
1872
+ catch (e) { }
1745
1873
  return state.info;
1746
1874
  }
1747
1875
  catch (e) {
1748
1876
  // Log error but continue retrying until timeout is reached
1749
- this.logger.warn("Retrying containsText due to: " + e.message);
1877
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1750
1878
  }
1751
- await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1879
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1752
1880
  }
1753
1881
  throw new Error("No snapshot match " + matchResult?.errorLineText);
1754
1882
  }
@@ -1900,12 +2028,7 @@ class StableBrowser {
1900
2028
  }
1901
2029
  }
1902
2030
  getTestData(world = null) {
1903
- const dataFile = _getDataFile(world, this.context, this);
1904
- let data = {};
1905
- if (fs.existsSync(dataFile)) {
1906
- data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1907
- }
1908
- return data;
2031
+ return _getTestData(world, this.context, this);
1909
2032
  }
1910
2033
  async _screenShot(options = {}, world = null, info = null) {
1911
2034
  // collect url/path/title
@@ -2125,6 +2248,77 @@ class StableBrowser {
2125
2248
  await _commandFinally(state, this);
2126
2249
  }
2127
2250
  }
2251
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2252
+ const state = {
2253
+ selectors,
2254
+ _params,
2255
+ property,
2256
+ variable,
2257
+ options,
2258
+ world,
2259
+ type: Types.EXTRACT_PROPERTY,
2260
+ text: `Extract property from element`,
2261
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2262
+ operation: "extractProperty",
2263
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2264
+ allowDisabled: true,
2265
+ };
2266
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2267
+ try {
2268
+ await _preCommand(state, this);
2269
+ switch (property) {
2270
+ case "inner_text":
2271
+ state.value = await state.element.innerText();
2272
+ break;
2273
+ case "href":
2274
+ state.value = await state.element.getAttribute("href");
2275
+ break;
2276
+ case "value":
2277
+ state.value = await state.element.inputValue();
2278
+ break;
2279
+ case "text":
2280
+ state.value = await state.element.textContent();
2281
+ break;
2282
+ default:
2283
+ if (property.startsWith("dataset.")) {
2284
+ const dataAttribute = property.substring(8);
2285
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2286
+ }
2287
+ else {
2288
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2289
+ }
2290
+ }
2291
+ if (options !== null) {
2292
+ if (options.regex && options.regex !== "") {
2293
+ // Construct a regex pattern from the provided string
2294
+ const regex = options.regex.slice(1, -1);
2295
+ const regexPattern = new RegExp(regex, "g");
2296
+ const matches = state.value.match(regexPattern);
2297
+ if (matches) {
2298
+ let newValue = "";
2299
+ for (const match of matches) {
2300
+ newValue += match;
2301
+ }
2302
+ state.value = newValue;
2303
+ }
2304
+ }
2305
+ if (options.trimSpaces && options.trimSpaces === true) {
2306
+ state.value = state.value.trim();
2307
+ }
2308
+ }
2309
+ state.info.value = state.value;
2310
+ this.setTestData({ [variable]: state.value }, world);
2311
+ this.logger.info("set test data: " + variable + "=" + state.value);
2312
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2313
+ return state.info;
2314
+ }
2315
+ catch (e) {
2316
+ await _commandError(state, e, this);
2317
+ }
2318
+ finally {
2319
+ await _commandFinally(state, this);
2320
+ }
2321
+ }
2128
2322
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2129
2323
  const state = {
2130
2324
  selectors,
@@ -2177,14 +2371,167 @@ class StableBrowser {
2177
2371
  let regex;
2178
2372
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2179
2373
  const patternBody = expectedValue.slice(1, -1);
2180
- regex = new RegExp(patternBody, "g");
2374
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2375
+ regex = new RegExp(processedPattern, "gs");
2376
+ state.info.regex = true;
2181
2377
  }
2182
2378
  else {
2183
2379
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2184
2380
  regex = new RegExp(escapedPattern, "g");
2185
2381
  }
2186
- if (!val.match(regex)) {
2187
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2382
+ if (attribute === "innerText") {
2383
+ if (state.info.regex) {
2384
+ if (!regex.test(val)) {
2385
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2386
+ state.info.failCause.assertionFailed = true;
2387
+ state.info.failCause.lastError = errorMessage;
2388
+ throw new Error(errorMessage);
2389
+ }
2390
+ }
2391
+ else {
2392
+ const valLines = val.split("\n");
2393
+ const expectedLines = expectedValue.split("\n");
2394
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2395
+ if (!isPart) {
2396
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2397
+ state.info.failCause.assertionFailed = true;
2398
+ state.info.failCause.lastError = errorMessage;
2399
+ throw new Error(errorMessage);
2400
+ }
2401
+ }
2402
+ }
2403
+ else {
2404
+ if (!val.match(regex)) {
2405
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2406
+ state.info.failCause.assertionFailed = true;
2407
+ state.info.failCause.lastError = errorMessage;
2408
+ throw new Error(errorMessage);
2409
+ }
2410
+ }
2411
+ return state.info;
2412
+ }
2413
+ catch (e) {
2414
+ await _commandError(state, e, this);
2415
+ }
2416
+ finally {
2417
+ await _commandFinally(state, this);
2418
+ }
2419
+ }
2420
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2421
+ const state = {
2422
+ selectors,
2423
+ _params,
2424
+ property,
2425
+ value,
2426
+ options,
2427
+ world,
2428
+ type: Types.VERIFY_PROPERTY,
2429
+ highlight: true,
2430
+ screenshot: true,
2431
+ text: `Verify element property`,
2432
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2433
+ operation: "verifyProperty",
2434
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2435
+ allowDisabled: true,
2436
+ };
2437
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2438
+ let val;
2439
+ let expectedValue;
2440
+ try {
2441
+ await _preCommand(state, this);
2442
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2443
+ state.info.expectedValue = expectedValue;
2444
+ switch (property) {
2445
+ case "innerText":
2446
+ val = String(await state.element.innerText());
2447
+ break;
2448
+ case "text":
2449
+ val = String(await state.element.textContent());
2450
+ break;
2451
+ case "value":
2452
+ val = String(await state.element.inputValue());
2453
+ break;
2454
+ case "checked":
2455
+ val = String(await state.element.isChecked());
2456
+ break;
2457
+ case "disabled":
2458
+ val = String(await state.element.isDisabled());
2459
+ break;
2460
+ case "readOnly":
2461
+ const isEditable = await state.element.isEditable();
2462
+ val = String(!isEditable);
2463
+ break;
2464
+ case "innerHTML":
2465
+ val = String(await state.element.innerHTML());
2466
+ break;
2467
+ case "outerHTML":
2468
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2469
+ break;
2470
+ default:
2471
+ if (property.startsWith("dataset.")) {
2472
+ const dataAttribute = property.substring(8);
2473
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2474
+ }
2475
+ else {
2476
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2477
+ }
2478
+ }
2479
+ // Helper function to remove all style="" attributes
2480
+ const removeStyleAttributes = (htmlString) => {
2481
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2482
+ };
2483
+ // Remove style attributes for innerHTML and outerHTML properties
2484
+ if (property === "innerHTML" || property === "outerHTML") {
2485
+ val = removeStyleAttributes(val);
2486
+ expectedValue = removeStyleAttributes(expectedValue);
2487
+ }
2488
+ state.info.value = val;
2489
+ let regex;
2490
+ state.info.value = val;
2491
+ const isRegex = expectedValue.startsWith("regex:");
2492
+ const isContains = expectedValue.startsWith("contains:");
2493
+ const isExact = expectedValue.startsWith("exact:");
2494
+ let matchPassed = false;
2495
+ if (isRegex) {
2496
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2497
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2498
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2499
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2500
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2501
+ const regex = new RegExp(patternBody, flags);
2502
+ state.info.regex = true;
2503
+ matchPassed = regex.test(val);
2504
+ }
2505
+ else {
2506
+ // Fallback: treat as literal
2507
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2508
+ const regex = new RegExp(escapedPattern, "g");
2509
+ matchPassed = regex.test(val);
2510
+ }
2511
+ }
2512
+ else if (isContains) {
2513
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2514
+ matchPassed = val.includes(containsValue);
2515
+ }
2516
+ else if (isExact) {
2517
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2518
+ matchPassed = val === exactValue;
2519
+ }
2520
+ else if (property === "innerText") {
2521
+ // Default innerText logic
2522
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2523
+ const valLines = val.split("\n");
2524
+ const expectedLines = normalizedExpectedValue.split("\n");
2525
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2526
+ }
2527
+ else {
2528
+ // Fallback exact or loose match
2529
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2530
+ const regex = new RegExp(escapedPattern, "g");
2531
+ matchPassed = regex.test(val);
2532
+ }
2533
+ if (!matchPassed) {
2534
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2188
2535
  state.info.failCause.assertionFailed = true;
2189
2536
  state.info.failCause.lastError = errorMessage;
2190
2537
  throw new Error(errorMessage);
@@ -2198,6 +2545,132 @@ class StableBrowser {
2198
2545
  await _commandFinally(state, this);
2199
2546
  }
2200
2547
  }
2548
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2549
+ // Convert timeout from seconds to milliseconds
2550
+ const timeoutMs = timeout * 1000;
2551
+ const state = {
2552
+ selectors,
2553
+ _params,
2554
+ condition,
2555
+ timeout: timeoutMs, // Store as milliseconds for internal use
2556
+ options,
2557
+ world,
2558
+ type: Types.CONDITIONAL_WAIT,
2559
+ highlight: true,
2560
+ screenshot: true,
2561
+ text: `Conditional wait for element`,
2562
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2563
+ operation: "conditionalWait",
2564
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2565
+ allowDisabled: true,
2566
+ info: {},
2567
+ };
2568
+ // Initialize startTime outside try block to ensure it's always accessible
2569
+ const startTime = Date.now();
2570
+ let conditionMet = false;
2571
+ let currentValue = null;
2572
+ let lastError = null;
2573
+ // Main retry loop - continues until timeout or condition is met
2574
+ while (Date.now() - startTime < timeoutMs) {
2575
+ const elapsedTime = Date.now() - startTime;
2576
+ const remainingTime = timeoutMs - elapsedTime;
2577
+ try {
2578
+ // Try to execute _preCommand (element location)
2579
+ await _preCommand(state, this);
2580
+ // If _preCommand succeeds, start condition checking
2581
+ const checkCondition = async () => {
2582
+ try {
2583
+ switch (condition.toLowerCase()) {
2584
+ case "checked":
2585
+ currentValue = await state.element.isChecked();
2586
+ return currentValue === true;
2587
+ case "unchecked":
2588
+ currentValue = await state.element.isChecked();
2589
+ return currentValue === false;
2590
+ case "visible":
2591
+ currentValue = await state.element.isVisible();
2592
+ return currentValue === true;
2593
+ case "hidden":
2594
+ currentValue = await state.element.isVisible();
2595
+ return currentValue === false;
2596
+ case "enabled":
2597
+ currentValue = await state.element.isDisabled();
2598
+ return currentValue === false;
2599
+ case "disabled":
2600
+ currentValue = await state.element.isDisabled();
2601
+ return currentValue === true;
2602
+ case "editable":
2603
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2604
+ currentValue = await state.element.isContentEditable();
2605
+ return currentValue === true;
2606
+ default:
2607
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2608
+ state.info.success = false;
2609
+ return false;
2610
+ }
2611
+ }
2612
+ catch (error) {
2613
+ // Don't throw here, just return false to continue retrying
2614
+ return false;
2615
+ }
2616
+ };
2617
+ // Inner loop for condition checking (once element is located)
2618
+ while (Date.now() - startTime < timeoutMs) {
2619
+ const currentElapsedTime = Date.now() - startTime;
2620
+ conditionMet = await checkCondition();
2621
+ if (conditionMet) {
2622
+ break;
2623
+ }
2624
+ // Check if we still have time for another attempt
2625
+ if (Date.now() - startTime + 50 < timeoutMs) {
2626
+ await new Promise((res) => setTimeout(res, 50));
2627
+ }
2628
+ else {
2629
+ break;
2630
+ }
2631
+ }
2632
+ // If we got here and condition is met, break out of main loop
2633
+ if (conditionMet) {
2634
+ break;
2635
+ }
2636
+ // If condition not met but no exception, we've timed out
2637
+ break;
2638
+ }
2639
+ catch (e) {
2640
+ lastError = e;
2641
+ const currentElapsedTime = Date.now() - startTime;
2642
+ const timeLeft = timeoutMs - currentElapsedTime;
2643
+ // Check if we have enough time left to retry
2644
+ if (timeLeft > 100) {
2645
+ await new Promise((resolve) => setTimeout(resolve, 50));
2646
+ }
2647
+ else {
2648
+ break;
2649
+ }
2650
+ }
2651
+ }
2652
+ const actualWaitTime = Date.now() - startTime;
2653
+ state.info = {
2654
+ success: conditionMet,
2655
+ conditionMet,
2656
+ actualWaitTime,
2657
+ currentValue,
2658
+ lastError: lastError?.message || null,
2659
+ message: conditionMet
2660
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2661
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2662
+ };
2663
+ if (lastError) {
2664
+ state.log += `Last error: ${lastError.message}\n`;
2665
+ }
2666
+ try {
2667
+ await _commandFinally(state, this);
2668
+ }
2669
+ catch (finallyError) {
2670
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2671
+ }
2672
+ return state.info;
2673
+ }
2201
2674
  async extractEmailData(emailAddress, options, world) {
2202
2675
  if (!emailAddress) {
2203
2676
  throw new Error("email address is null");
@@ -2355,56 +2828,49 @@ class StableBrowser {
2355
2828
  console.debug(error);
2356
2829
  }
2357
2830
  }
2358
- // async _unhighlightElements(scope, css) {
2359
- // try {
2360
- // if (!scope) {
2361
- // return;
2362
- // }
2363
- // if (!css) {
2364
- // scope
2365
- // .evaluate((node) => {
2366
- // if (node && node.style) {
2367
- // if (!node.__previousOutline) {
2368
- // node.style.outline = "";
2369
- // } else {
2370
- // node.style.outline = node.__previousOutline;
2371
- // }
2372
- // }
2373
- // })
2374
- // .then(() => {})
2375
- // .catch((e) => {
2376
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2377
- // });
2378
- // } else {
2379
- // scope
2380
- // .evaluate(([css]) => {
2381
- // if (!css) {
2382
- // return;
2383
- // }
2384
- // let elements = Array.from(document.querySelectorAll(css));
2385
- // for (i = 0; i < elements.length; i++) {
2386
- // let element = elements[i];
2387
- // if (!element.style) {
2388
- // return;
2389
- // }
2390
- // if (!element.__previousOutline) {
2391
- // element.style.outline = "";
2392
- // } else {
2393
- // element.style.outline = element.__previousOutline;
2394
- // }
2395
- // }
2396
- // })
2397
- // .then(() => {})
2398
- // .catch((e) => {
2399
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2400
- // });
2401
- // }
2402
- // } catch (error) {
2403
- // // console.debug(error);
2404
- // }
2405
- // }
2831
+ _matcher(text) {
2832
+ if (!text) {
2833
+ return { matcher: "contains", queryText: "" };
2834
+ }
2835
+ if (text.length < 2) {
2836
+ return { matcher: "contains", queryText: text };
2837
+ }
2838
+ const split = text.split(":");
2839
+ const matcher = split[0].toLowerCase();
2840
+ const queryText = split.slice(1).join(":").trim();
2841
+ return { matcher, queryText };
2842
+ }
2843
+ _getDomain(url) {
2844
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2845
+ return "";
2846
+ }
2847
+ let hostnameFragments = url.split("/")[2].split(".");
2848
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2849
+ return hostnameFragments.join("-").split(":").join("-");
2850
+ }
2851
+ let n = hostnameFragments.length;
2852
+ let fragments = [...hostnameFragments];
2853
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2854
+ hostnameFragments.pop();
2855
+ n = hostnameFragments.length;
2856
+ }
2857
+ if (n == 0) {
2858
+ if (fragments[0] === "www")
2859
+ fragments = fragments.slice(1);
2860
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2861
+ }
2862
+ if (hostnameFragments[0] === "www")
2863
+ hostnameFragments = hostnameFragments.slice(1);
2864
+ return hostnameFragments.join(".");
2865
+ }
2866
+ /**
2867
+ * Verify the page path matches the given path.
2868
+ * @param {string} pathPart - The path to verify.
2869
+ * @param {object} options - Options for verification.
2870
+ * @param {object} world - The world context.
2871
+ * @returns {Promise<object>} - The state info after verification.
2872
+ */
2406
2873
  async verifyPagePath(pathPart, options = {}, world = null) {
2407
- const startTime = Date.now();
2408
2874
  let error = null;
2409
2875
  let screenshotId = null;
2410
2876
  let screenshotPath = null;
@@ -2418,113 +2884,212 @@ class StableBrowser {
2418
2884
  pathPart = newValue;
2419
2885
  }
2420
2886
  info.pathPart = pathPart;
2887
+ const { matcher, queryText } = this._matcher(pathPart);
2888
+ const state = {
2889
+ text_search: queryText,
2890
+ options,
2891
+ world,
2892
+ locate: false,
2893
+ scroll: false,
2894
+ highlight: false,
2895
+ type: Types.VERIFY_PAGE_PATH,
2896
+ text: `Verify the page url is ${queryText}`,
2897
+ _text: `Verify the page url is ${queryText}`,
2898
+ operation: "verifyPagePath",
2899
+ log: "***** verify page url is " + queryText + " *****\n",
2900
+ };
2421
2901
  try {
2902
+ await _preCommand(state, this);
2903
+ state.info.text = queryText;
2422
2904
  for (let i = 0; i < 30; i++) {
2423
2905
  const url = await this.page.url();
2424
- if (!url.includes(pathPart)) {
2425
- if (i === 29) {
2426
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2427
- }
2428
- await new Promise((resolve) => setTimeout(resolve, 1000));
2429
- continue;
2906
+ switch (matcher) {
2907
+ case "exact":
2908
+ if (url !== queryText) {
2909
+ if (i === 29) {
2910
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2911
+ }
2912
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2913
+ continue;
2914
+ }
2915
+ break;
2916
+ case "contains":
2917
+ if (!url.includes(queryText)) {
2918
+ if (i === 29) {
2919
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2920
+ }
2921
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2922
+ continue;
2923
+ }
2924
+ break;
2925
+ case "starts-with":
2926
+ {
2927
+ const domain = this._getDomain(url);
2928
+ if (domain.length > 0 && domain !== queryText) {
2929
+ if (i === 29) {
2930
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2931
+ }
2932
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2933
+ continue;
2934
+ }
2935
+ }
2936
+ break;
2937
+ case "ends-with":
2938
+ {
2939
+ const urlObj = new URL(url);
2940
+ let route = "/";
2941
+ if (urlObj.pathname !== "/") {
2942
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2943
+ }
2944
+ else {
2945
+ route = "/";
2946
+ }
2947
+ if (route !== queryText) {
2948
+ if (i === 29) {
2949
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2950
+ }
2951
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2952
+ continue;
2953
+ }
2954
+ }
2955
+ break;
2956
+ case "regex":
2957
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2958
+ if (!regex.test(url)) {
2959
+ if (i === 29) {
2960
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2961
+ }
2962
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2963
+ continue;
2964
+ }
2965
+ break;
2966
+ default:
2967
+ console.log("Unknown matching type, defaulting to contains matching");
2968
+ if (!url.includes(pathPart)) {
2969
+ if (i === 29) {
2970
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2971
+ }
2972
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2973
+ continue;
2974
+ }
2430
2975
  }
2431
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2432
- return info;
2976
+ await _screenshot(state, this);
2977
+ return state.info;
2433
2978
  }
2434
2979
  }
2435
2980
  catch (e) {
2436
- //await this.closeUnexpectedPopups();
2437
- this.logger.error("verify page path failed " + info.log);
2438
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2439
- info.screenshotPath = screenshotPath;
2440
- Object.assign(e, { info: info });
2441
- error = e;
2442
- // throw e;
2443
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2981
+ state.info.failCause.lastError = e.message;
2982
+ state.info.failCause.assertionFailed = true;
2983
+ await _commandError(state, e, this);
2444
2984
  }
2445
2985
  finally {
2446
- const endTime = Date.now();
2447
- _reportToWorld(world, {
2448
- type: Types.VERIFY_PAGE_PATH,
2449
- text: "Verify page path",
2450
- _text: "Verify the page path contains " + pathPart,
2451
- screenshotId,
2452
- result: error
2453
- ? {
2454
- status: "FAILED",
2455
- startTime,
2456
- endTime,
2457
- message: error?.message,
2458
- }
2459
- : {
2460
- status: "PASSED",
2461
- startTime,
2462
- endTime,
2463
- },
2464
- info: info,
2465
- });
2986
+ await _commandFinally(state, this);
2466
2987
  }
2467
2988
  }
2989
+ /**
2990
+ * Verify the page title matches the given title.
2991
+ * @param {string} title - The title to verify.
2992
+ * @param {object} options - Options for verification.
2993
+ * @param {object} world - The world context.
2994
+ * @returns {Promise<object>} - The state info after verification.
2995
+ */
2468
2996
  async verifyPageTitle(title, options = {}, world = null) {
2469
- const startTime = Date.now();
2470
2997
  let error = null;
2471
2998
  let screenshotId = null;
2472
2999
  let screenshotPath = null;
2473
3000
  await new Promise((resolve) => setTimeout(resolve, 2000));
2474
- const info = {};
2475
- info.log = "***** verify page title " + title + " *****\n";
2476
- info.operation = "verifyPageTitle";
2477
3001
  const newValue = await this._replaceWithLocalData(title, world);
2478
3002
  if (newValue !== title) {
2479
3003
  this.logger.info(title + "=" + newValue);
2480
3004
  title = newValue;
2481
3005
  }
2482
- info.title = title;
3006
+ const { matcher, queryText } = this._matcher(title);
3007
+ const state = {
3008
+ text_search: queryText,
3009
+ options,
3010
+ world,
3011
+ locate: false,
3012
+ scroll: false,
3013
+ highlight: false,
3014
+ type: Types.VERIFY_PAGE_TITLE,
3015
+ text: `Verify the page title is ${queryText}`,
3016
+ _text: `Verify the page title is ${queryText}`,
3017
+ operation: "verifyPageTitle",
3018
+ log: "***** verify page title is " + queryText + " *****\n",
3019
+ };
2483
3020
  try {
3021
+ await _preCommand(state, this);
3022
+ state.info.text = queryText;
2484
3023
  for (let i = 0; i < 30; i++) {
2485
3024
  const foundTitle = await this.page.title();
2486
- if (!foundTitle.includes(title)) {
2487
- if (i === 29) {
2488
- throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2489
- }
2490
- await new Promise((resolve) => setTimeout(resolve, 1000));
2491
- continue;
3025
+ switch (matcher) {
3026
+ case "exact":
3027
+ if (foundTitle !== queryText) {
3028
+ if (i === 29) {
3029
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
3030
+ }
3031
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3032
+ continue;
3033
+ }
3034
+ break;
3035
+ case "contains":
3036
+ if (!foundTitle.includes(queryText)) {
3037
+ if (i === 29) {
3038
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
3039
+ }
3040
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3041
+ continue;
3042
+ }
3043
+ break;
3044
+ case "starts-with":
3045
+ if (!foundTitle.startsWith(queryText)) {
3046
+ if (i === 29) {
3047
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
3048
+ }
3049
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3050
+ continue;
3051
+ }
3052
+ break;
3053
+ case "ends-with":
3054
+ if (!foundTitle.endsWith(queryText)) {
3055
+ if (i === 29) {
3056
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
3057
+ }
3058
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3059
+ continue;
3060
+ }
3061
+ break;
3062
+ case "regex":
3063
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3064
+ if (!regex.test(foundTitle)) {
3065
+ if (i === 29) {
3066
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
3067
+ }
3068
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3069
+ continue;
3070
+ }
3071
+ break;
3072
+ default:
3073
+ console.log("Unknown matching type, defaulting to contains matching");
3074
+ if (!foundTitle.includes(title)) {
3075
+ if (i === 29) {
3076
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
3077
+ }
3078
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3079
+ continue;
3080
+ }
2492
3081
  }
2493
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2494
- return info;
3082
+ await _screenshot(state, this);
3083
+ return state.info;
2495
3084
  }
2496
3085
  }
2497
3086
  catch (e) {
2498
- //await this.closeUnexpectedPopups();
2499
- this.logger.error("verify page title failed " + info.log);
2500
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2501
- info.screenshotPath = screenshotPath;
2502
- Object.assign(e, { info: info });
2503
- error = e;
2504
- // throw e;
2505
- await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
3087
+ state.info.failCause.lastError = e.message;
3088
+ state.info.failCause.assertionFailed = true;
3089
+ await _commandError(state, e, this);
2506
3090
  }
2507
3091
  finally {
2508
- const endTime = Date.now();
2509
- _reportToWorld(world, {
2510
- type: Types.VERIFY_PAGE_PATH,
2511
- text: "Verify page title",
2512
- _text: "Verify the page title contains " + title,
2513
- screenshotId,
2514
- result: error
2515
- ? {
2516
- status: "FAILED",
2517
- startTime,
2518
- endTime,
2519
- message: error?.message,
2520
- }
2521
- : {
2522
- status: "PASSED",
2523
- startTime,
2524
- endTime,
2525
- },
2526
- info: info,
2527
- });
3092
+ await _commandFinally(state, this);
2528
3093
  }
2529
3094
  }
2530
3095
  async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
@@ -2608,27 +3173,10 @@ class StableBrowser {
2608
3173
  const frame = resultWithElementsFound[0].frame;
2609
3174
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2610
3175
  await this._highlightElements(frame, dataAttribute);
2611
- // if (world && world.screenshot && !world.screenshotPath) {
2612
- // console.log(`Highlighting for verify text is found while running from recorder`);
2613
- // this._highlightElements(frame, dataAttribute).then(async () => {
2614
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2615
- // this._unhighlightElements(frame, dataAttribute)
2616
- // .then(async () => {
2617
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2618
- // })
2619
- // .catch(
2620
- // (e) => {}
2621
- // console.error(e)
2622
- // );
2623
- // });
2624
- // }
2625
3176
  const element = await frame.locator(dataAttribute).first();
2626
- // await new Promise((resolve) => setTimeout(resolve, 100));
2627
- // await this._unhighlightElements(frame, dataAttribute);
2628
3177
  if (element) {
2629
3178
  await this.scrollIfNeeded(element, state.info);
2630
3179
  await element.dispatchEvent("bvt_verify_page_contains_text");
2631
- // await _screenshot(state, this, element);
2632
3180
  }
2633
3181
  }
2634
3182
  await _screenshot(state, this);
@@ -2638,7 +3186,6 @@ class StableBrowser {
2638
3186
  console.error(error);
2639
3187
  }
2640
3188
  }
2641
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2642
3189
  }
2643
3190
  catch (e) {
2644
3191
  await _commandError(state, e, this);
@@ -2720,6 +3267,8 @@ class StableBrowser {
2720
3267
  operation: "verify_text_with_relation",
2721
3268
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2722
3269
  };
3270
+ const cmdStartTime = Date.now();
3271
+ let cmdEndTime = null;
2723
3272
  const timeout = this._getFindElementTimeout(options);
2724
3273
  await new Promise((resolve) => setTimeout(resolve, 2000));
2725
3274
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -2755,6 +3304,17 @@ class StableBrowser {
2755
3304
  await new Promise((resolve) => setTimeout(resolve, 1000));
2756
3305
  continue;
2757
3306
  }
3307
+ else {
3308
+ cmdEndTime = Date.now();
3309
+ if (cmdEndTime - cmdStartTime > 55000) {
3310
+ if (foundAncore) {
3311
+ throw new Error(`Text ${textToVerify} not found in page`);
3312
+ }
3313
+ else {
3314
+ throw new Error(`Text ${textAnchor} not found in page`);
3315
+ }
3316
+ }
3317
+ }
2758
3318
  try {
2759
3319
  for (let i = 0; i < resultWithElementsFound.length; i++) {
2760
3320
  foundAncore = true;
@@ -3154,8 +3714,51 @@ class StableBrowser {
3154
3714
  });
3155
3715
  }
3156
3716
  }
3717
+ /**
3718
+ * Explicit wait/sleep function that pauses execution for a specified duration
3719
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3720
+ * @param options - Optional configuration object
3721
+ * @param world - Optional world context
3722
+ * @returns Promise that resolves after the specified duration
3723
+ */
3724
+ async sleep(duration = 1000, options = {}, world = null) {
3725
+ const state = {
3726
+ duration,
3727
+ options,
3728
+ world,
3729
+ locate: false,
3730
+ scroll: false,
3731
+ screenshot: false,
3732
+ highlight: false,
3733
+ type: Types.SLEEP,
3734
+ text: `Sleep for ${duration} ms`,
3735
+ _text: `Sleep for ${duration} ms`,
3736
+ operation: "sleep",
3737
+ log: `***** Sleep for ${duration} ms *****\n`,
3738
+ };
3739
+ try {
3740
+ await _preCommand(state, this);
3741
+ if (duration < 0) {
3742
+ throw new Error("Sleep duration cannot be negative");
3743
+ }
3744
+ await new Promise((resolve) => setTimeout(resolve, duration));
3745
+ return state.info;
3746
+ }
3747
+ catch (e) {
3748
+ await _commandError(state, e, this);
3749
+ }
3750
+ finally {
3751
+ await _commandFinally(state, this);
3752
+ }
3753
+ }
3157
3754
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
3158
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3755
+ try {
3756
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3757
+ }
3758
+ catch (error) {
3759
+ this.logger.debug(error);
3760
+ throw error;
3761
+ }
3159
3762
  }
3160
3763
  _getLoadTimeout(options) {
3161
3764
  let timeout = 15000;
@@ -3178,6 +3781,7 @@ class StableBrowser {
3178
3781
  }
3179
3782
  async saveStoreState(path = null, world = null) {
3180
3783
  const storageState = await this.page.context().storageState();
3784
+ path = await this._replaceWithLocalData(path, this.world);
3181
3785
  //const testDataFile = _getDataFile(world, this.context, this);
3182
3786
  if (path) {
3183
3787
  // save { storageState: storageState } into the path
@@ -3188,10 +3792,14 @@ class StableBrowser {
3188
3792
  }
3189
3793
  }
3190
3794
  async restoreSaveState(path = null, world = null) {
3795
+ path = await this._replaceWithLocalData(path, this.world);
3191
3796
  await refreshBrowser(this, path, world);
3192
3797
  this.registerEventListeners(this.context);
3193
3798
  registerNetworkEvents(this.world, this, this.context, this.page);
3194
3799
  registerDownloadEvent(this.page, this.world, this.context);
3800
+ if (this.onRestoreSaveState) {
3801
+ this.onRestoreSaveState(path);
3802
+ }
3195
3803
  }
3196
3804
  async waitForPageLoad(options = {}, world = null) {
3197
3805
  let timeout = this._getLoadTimeout(options);
@@ -3226,7 +3834,6 @@ class StableBrowser {
3226
3834
  else if (e.label === "domcontentloaded") {
3227
3835
  console.log("waited for the domcontent loaded timeout");
3228
3836
  }
3229
- console.log(".");
3230
3837
  }
3231
3838
  finally {
3232
3839
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -3270,7 +3877,6 @@ class StableBrowser {
3270
3877
  await this.page.close();
3271
3878
  }
3272
3879
  catch (e) {
3273
- console.log(".");
3274
3880
  await _commandError(state, e, this);
3275
3881
  }
3276
3882
  finally {
@@ -3385,7 +3991,6 @@ class StableBrowser {
3385
3991
  await this.page.setViewportSize({ width: width, height: hight });
3386
3992
  }
3387
3993
  catch (e) {
3388
- console.log(".");
3389
3994
  await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
3390
3995
  }
3391
3996
  finally {
@@ -3423,7 +4028,6 @@ class StableBrowser {
3423
4028
  await this.page.reload();
3424
4029
  }
3425
4030
  catch (e) {
3426
- console.log(".");
3427
4031
  await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
3428
4032
  }
3429
4033
  finally {
@@ -3467,6 +4071,10 @@ class StableBrowser {
3467
4071
  }
3468
4072
  }
3469
4073
  async beforeScenario(world, scenario) {
4074
+ if (world && world.attach) {
4075
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4076
+ }
4077
+ this.context.loadedRoutes = null;
3470
4078
  this.beforeScenarioCalled = true;
3471
4079
  if (scenario && scenario.pickle && scenario.pickle.name) {
3472
4080
  this.scenarioName = scenario.pickle.name;
@@ -3490,14 +4098,18 @@ class StableBrowser {
3490
4098
  envName = this.context.environment.name;
3491
4099
  }
3492
4100
  if (!process.env.TEMP_RUN) {
3493
- await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
4101
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3494
4102
  }
3495
4103
  await loadBrunoParams(this.context, this.context.environment.name);
3496
4104
  }
3497
4105
  async afterScenario(world, scenario) { }
3498
4106
  async beforeStep(world, step) {
4107
+ if (this.abortedExecution) {
4108
+ throw new Error("Aborted");
4109
+ }
3499
4110
  if (!this.beforeScenarioCalled) {
3500
4111
  this.beforeScenario(world, step);
4112
+ this.context.loadedRoutes = null;
3501
4113
  }
3502
4114
  if (this.stepIndex === undefined) {
3503
4115
  this.stepIndex = 0;
@@ -3522,13 +4134,16 @@ class StableBrowser {
3522
4134
  }
3523
4135
  if (this.initSnapshotTaken === false) {
3524
4136
  this.initSnapshotTaken = true;
3525
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4137
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
3526
4138
  const snapshot = await this.getAriaSnapshot();
3527
4139
  if (snapshot) {
3528
4140
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3529
4141
  }
3530
4142
  }
3531
4143
  }
4144
+ this.context.routeResults = null;
4145
+ await registerBeforeStepRoutes(this.context, this.stepName);
4146
+ networkBeforeStep(this.stepName);
3532
4147
  }
3533
4148
  async getAriaSnapshot() {
3534
4149
  try {
@@ -3544,19 +4159,74 @@ class StableBrowser {
3544
4159
  const content = [`- path: ${path}`, `- title: ${title}`];
3545
4160
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3546
4161
  for (let i = 0; i < frames.length; i++) {
3547
- content.push(`- frame: ${i}`);
3548
4162
  const frame = frames[i];
3549
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3550
- content.push(snapshot);
4163
+ try {
4164
+ // Ensure frame is attached and has body
4165
+ const body = frame.locator("body");
4166
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4167
+ const snapshot = await body.ariaSnapshot({ timeout });
4168
+ if (!snapshot) {
4169
+ continue;
4170
+ }
4171
+ content.push(`- frame: ${i}`);
4172
+ content.push(snapshot);
4173
+ }
4174
+ catch (innerErr) {
4175
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4176
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4177
+ }
3551
4178
  }
3552
4179
  return content.join("\n");
3553
4180
  }
3554
4181
  catch (e) {
3555
4182
  console.log("Error in getAriaSnapshot");
3556
- console.debug(e);
4183
+ //console.debug(e);
3557
4184
  }
3558
4185
  return null;
3559
4186
  }
4187
+ /**
4188
+ * Sends command with custom payload to report.
4189
+ * @param commandText - Title of the command to be shown in the report.
4190
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
4191
+ * @param content - Content of the command to be shown in the report.
4192
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
4193
+ * @param world - Optional world context.
4194
+ * @public
4195
+ */
4196
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
4197
+ const state = {
4198
+ options,
4199
+ world,
4200
+ locate: false,
4201
+ scroll: false,
4202
+ screenshot: options.screenshot ?? false,
4203
+ highlight: options.highlight ?? false,
4204
+ type: Types.REPORT_COMMAND,
4205
+ text: commandText,
4206
+ _text: commandText,
4207
+ operation: "report_command",
4208
+ log: "***** " + commandText + " *****\n",
4209
+ };
4210
+ try {
4211
+ await _preCommand(state, this);
4212
+ const payload = {
4213
+ type: options.type ?? "text",
4214
+ content: content,
4215
+ screenshotId: null,
4216
+ };
4217
+ state.payload = payload;
4218
+ if (commandStatus === "FAILED") {
4219
+ state.throwError = true;
4220
+ throw new Error("Command failed");
4221
+ }
4222
+ }
4223
+ catch (e) {
4224
+ await _commandError(state, e, this);
4225
+ }
4226
+ finally {
4227
+ await _commandFinally(state, this);
4228
+ }
4229
+ }
3560
4230
  async afterStep(world, step) {
3561
4231
  this.stepName = null;
3562
4232
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3564,10 +4234,12 @@ class StableBrowser {
3564
4234
  await this.context.browserObject.context.tracing.stopChunk({
3565
4235
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3566
4236
  });
3567
- await world.attach(JSON.stringify({
3568
- type: "trace",
3569
- traceFilePath: `trace-${this.stepIndex}.zip`,
3570
- }), "application/json+trace");
4237
+ if (world && world.attach) {
4238
+ await world.attach(JSON.stringify({
4239
+ type: "trace",
4240
+ traceFilePath: `trace-${this.stepIndex}.zip`,
4241
+ }), "application/json+trace");
4242
+ }
3571
4243
  // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3572
4244
  }
3573
4245
  }
@@ -3581,6 +4253,36 @@ class StableBrowser {
3581
4253
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3582
4254
  }
3583
4255
  }
4256
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4257
+ if (this.context.routeResults) {
4258
+ if (world && world.attach) {
4259
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4260
+ }
4261
+ }
4262
+ if (!process.env.TEMP_RUN) {
4263
+ const state = {
4264
+ world,
4265
+ locate: false,
4266
+ scroll: false,
4267
+ screenshot: true,
4268
+ highlight: true,
4269
+ type: Types.STEP_COMPLETE,
4270
+ text: "end of scenario",
4271
+ _text: "end of scenario",
4272
+ operation: "step_complete",
4273
+ log: "***** " + "end of scenario" + " *****\n",
4274
+ };
4275
+ try {
4276
+ await _preCommand(state, this);
4277
+ }
4278
+ catch (e) {
4279
+ await _commandError(state, e, this);
4280
+ }
4281
+ finally {
4282
+ await _commandFinally(state, this);
4283
+ }
4284
+ }
4285
+ networkAfterStep(this.stepName);
3584
4286
  }
3585
4287
  }
3586
4288
  function createTimedPromise(promise, label) {