automation_model 1.0.745-dev → 1.0.745-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 (44) hide show
  1. package/README.md +4 -1
  2. package/lib/api.js +4 -3
  3. package/lib/api.js.map +1 -1
  4. package/lib/auto_page.d.ts +3 -1
  5. package/lib/auto_page.js +66 -16
  6. package/lib/auto_page.js.map +1 -1
  7. package/lib/browser_manager.js +19 -25
  8. package/lib/browser_manager.js.map +1 -1
  9. package/lib/bruno.js.map +1 -1
  10. package/lib/check_performance.d.ts +1 -0
  11. package/lib/check_performance.js +57 -0
  12. package/lib/check_performance.js.map +1 -0
  13. package/lib/command_common.js +17 -1
  14. package/lib/command_common.js.map +1 -1
  15. package/lib/file_checker.js +129 -25
  16. package/lib/file_checker.js.map +1 -1
  17. package/lib/index.js +1 -0
  18. package/lib/index.js.map +1 -1
  19. package/lib/init_browser.d.ts +1 -2
  20. package/lib/init_browser.js +121 -125
  21. package/lib/init_browser.js.map +1 -1
  22. package/lib/locator_log.js.map +1 -1
  23. package/lib/network.d.ts +2 -0
  24. package/lib/network.js +398 -87
  25. package/lib/network.js.map +1 -1
  26. package/lib/route.d.ts +83 -0
  27. package/lib/route.js +682 -0
  28. package/lib/route.js.map +1 -0
  29. package/lib/scripts/axe.mini.js +23994 -1
  30. package/lib/snapshot_validation.js.map +1 -1
  31. package/lib/stable_browser.d.ts +15 -4
  32. package/lib/stable_browser.js +559 -86
  33. package/lib/stable_browser.js.map +1 -1
  34. package/lib/table.js +4 -4
  35. package/lib/table.js.map +1 -1
  36. package/lib/table_helper.js +14 -0
  37. package/lib/table_helper.js.map +1 -1
  38. package/lib/test_context.d.ts +1 -0
  39. package/lib/test_context.js +1 -0
  40. package/lib/test_context.js.map +1 -1
  41. package/lib/utils.d.ts +5 -2
  42. package/lib/utils.js +33 -8
  43. package/lib/utils.js.map +1 -1
  44. package/package.json +16 -11
@@ -1,4 +1,5 @@
1
1
  // @ts-nocheck
2
+ import { check_performance } from "./check_performance.js";
2
3
  import { expect } from "@playwright/test";
3
4
  import dayjs from "dayjs";
4
5
  import fs from "fs";
@@ -10,6 +11,7 @@ import { getDateTimeValue } from "./date_time.js";
10
11
  import drawRectangle from "./drawRect.js";
11
12
  //import { closeUnexpectedPopups } from "./popups.js";
12
13
  import { getTableCells, getTableData } from "./table_analyze.js";
14
+ import errorStackParser from "error-stack-parser";
13
15
  import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
14
16
  import csv from "csv-parser";
15
17
  import { Readable } from "node:stream";
@@ -19,16 +21,20 @@ import { getTestData } from "./auto_page.js";
19
21
  import { locate_element } from "./locate_element.js";
20
22
  import { randomUUID } from "crypto";
21
23
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
22
- import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
24
+ import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
23
25
  import { LocatorLog } from "./locator_log.js";
24
26
  import axios from "axios";
25
27
  import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
28
  import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
27
29
  import { loadBrunoParams } from "./bruno.js";
30
+ import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
31
+ import { existsSync } from "node:fs";
28
32
  export const Types = {
29
33
  CLICK: "click_element",
30
34
  WAIT_ELEMENT: "wait_element",
31
- NAVIGATE: "navigate", ///
35
+ NAVIGATE: "navigate",
36
+ GO_BACK: "go_back",
37
+ GO_FORWARD: "go_forward",
32
38
  FILL: "fill_element",
33
39
  EXECUTE: "execute_page_method", //
34
40
  OPEN: "open_environment", //
@@ -41,6 +47,7 @@ export const Types = {
41
47
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
42
48
  ANALYZE_TABLE: "analyze_table",
43
49
  SELECT: "select_combobox", //
50
+ VERIFY_PROPERTY: "verify_element_property",
44
51
  VERIFY_PAGE_PATH: "verify_page_path",
45
52
  VERIFY_PAGE_TITLE: "verify_page_title",
46
53
  TYPE_PRESS: "type_press",
@@ -59,15 +66,15 @@ export const Types = {
59
66
  SET_INPUT: "set_input",
60
67
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
61
68
  VERIFY_ATTRIBUTE: "verify_element_attribute",
62
- VERIFY_PROPERTY: "verify_element_property",
63
69
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
64
70
  BRUNO: "bruno",
65
- SNAPSHOT_VALIDATION: "snapshot_validation",
66
71
  VERIFY_FILE_EXISTS: "verify_file_exists",
67
72
  SET_INPUT_FILES: "set_input_files",
73
+ SNAPSHOT_VALIDATION: "snapshot_validation",
68
74
  REPORT_COMMAND: "report_command",
69
75
  STEP_COMPLETE: "step_complete",
70
76
  SLEEP: "sleep",
77
+ CONDITIONAL_WAIT: "conditional_wait",
71
78
  };
72
79
  export const apps = {};
73
80
  const formatElementName = (elementName) => {
@@ -80,6 +87,7 @@ class StableBrowser {
80
87
  context;
81
88
  world;
82
89
  fastMode;
90
+ stepTags;
83
91
  project_path = null;
84
92
  webLogFile = null;
85
93
  networkLogger = null;
@@ -88,13 +96,15 @@ class StableBrowser {
88
96
  tags = null;
89
97
  isRecording = false;
90
98
  initSnapshotTaken = false;
91
- constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
99
+ onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
100
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
92
101
  this.browser = browser;
93
102
  this.page = page;
94
103
  this.logger = logger;
95
104
  this.context = context;
96
105
  this.world = world;
97
106
  this.fastMode = fastMode;
107
+ this.stepTags = stepTags;
98
108
  if (!this.logger) {
99
109
  this.logger = console;
100
110
  }
@@ -123,9 +133,16 @@ class StableBrowser {
123
133
  context.pages = [this.page];
124
134
  const logFolder = path.join(this.project_path, "logs", "web");
125
135
  this.world = world;
136
+ if (this.configuration && this.configuration.fastMode === true) {
137
+ this.fastMode = true;
138
+ }
126
139
  if (process.env.FAST_MODE === "true") {
140
+ // console.log("Fast mode enabled from environment variable");
127
141
  this.fastMode = true;
128
142
  }
143
+ if (process.env.FAST_MODE === "false") {
144
+ this.fastMode = false;
145
+ }
129
146
  if (this.context) {
130
147
  this.context.fastMode = this.fastMode;
131
148
  }
@@ -163,6 +180,7 @@ class StableBrowser {
163
180
  registerNetworkEvents(this.world, this, context, this.page);
164
181
  registerDownloadEvent(this.page, this.world, context);
165
182
  page.on("close", async () => {
183
+ // return if browser context is already closed
166
184
  if (this.context && this.context.pages && this.context.pages.length > 1) {
167
185
  this.context.pages.pop();
168
186
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -172,7 +190,12 @@ class StableBrowser {
172
190
  console.log("Switched to page " + title);
173
191
  }
174
192
  catch (error) {
175
- console.error("Error on page close", error);
193
+ if (error?.message?.includes("Target page, context or browser has been closed")) {
194
+ // Ignore this error
195
+ }
196
+ else {
197
+ console.error("Error on page close", error);
198
+ }
176
199
  }
177
200
  }
178
201
  });
@@ -181,7 +204,12 @@ class StableBrowser {
181
204
  console.log("Switch page: " + (await page.title()));
182
205
  }
183
206
  catch (e) {
184
- this.logger.error("error on page load " + e);
207
+ if (e?.message?.includes("Target page, context or browser has been closed")) {
208
+ // Ignore this error
209
+ }
210
+ else {
211
+ this.logger.error("error on page load " + e);
212
+ }
185
213
  }
186
214
  context.pageLoading.status = false;
187
215
  }.bind(this));
@@ -209,7 +237,7 @@ class StableBrowser {
209
237
  if (newContextCreated) {
210
238
  this.registerEventListeners(this.context);
211
239
  await this.goto(this.context.environment.baseUrl);
212
- if (!this.fastMode) {
240
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
213
241
  await this.waitForPageLoad();
214
242
  }
215
243
  }
@@ -337,6 +365,64 @@ class StableBrowser {
337
365
  await _commandFinally(state, this);
338
366
  }
339
367
  }
368
+ async goBack(options, world = null) {
369
+ const state = {
370
+ value: "",
371
+ world: world,
372
+ type: Types.GO_BACK,
373
+ text: `Browser navigate back`,
374
+ operation: "goBack",
375
+ log: "***** navigate back *****\n",
376
+ info: {},
377
+ locate: false,
378
+ scroll: false,
379
+ screenshot: false,
380
+ highlight: false,
381
+ };
382
+ try {
383
+ await _preCommand(state, this);
384
+ await this.page.goBack({
385
+ waitUntil: "load",
386
+ });
387
+ await _screenshot(state, this);
388
+ }
389
+ catch (error) {
390
+ console.error("Error on goBack", error);
391
+ _commandError(state, error, this);
392
+ }
393
+ finally {
394
+ await _commandFinally(state, this);
395
+ }
396
+ }
397
+ async goForward(options, world = null) {
398
+ const state = {
399
+ value: "",
400
+ world: world,
401
+ type: Types.GO_FORWARD,
402
+ text: `Browser navigate forward`,
403
+ operation: "goForward",
404
+ log: "***** navigate forward *****\n",
405
+ info: {},
406
+ locate: false,
407
+ scroll: false,
408
+ screenshot: false,
409
+ highlight: false,
410
+ };
411
+ try {
412
+ await _preCommand(state, this);
413
+ await this.page.goForward({
414
+ waitUntil: "load",
415
+ });
416
+ await _screenshot(state, this);
417
+ }
418
+ catch (error) {
419
+ console.error("Error on goForward", error);
420
+ _commandError(state, error, this);
421
+ }
422
+ finally {
423
+ await _commandFinally(state, this);
424
+ }
425
+ }
340
426
  async _getLocator(locator, scope, _params) {
341
427
  locator = _fixLocatorUsingParams(locator, _params);
342
428
  // locator = await this._replaceWithLocalData(locator);
@@ -435,12 +521,6 @@ class StableBrowser {
435
521
  if (!el.setAttribute) {
436
522
  el = el.parentElement;
437
523
  }
438
- // remove any attributes start with data-blinq-id
439
- // for (let i = 0; i < el.attributes.length; i++) {
440
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
441
- // el.removeAttribute(el.attributes[i].name);
442
- // }
443
- // }
444
524
  el.setAttribute("data-blinq-id-" + randomToken, "");
445
525
  return true;
446
526
  }, [tag1, randomToken]))) {
@@ -462,14 +542,13 @@ class StableBrowser {
462
542
  info.locatorLog = new LocatorLog(selectorHierarchy);
463
543
  }
464
544
  let locatorSearch = selectorHierarchy[index];
465
- let originalLocatorSearch = "";
466
545
  try {
467
- originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
468
- locatorSearch = JSON.parse(originalLocatorSearch);
546
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
469
547
  }
470
548
  catch (e) {
471
549
  console.error(e);
472
550
  }
551
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
473
552
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
474
553
  let locator = null;
475
554
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
@@ -611,40 +690,197 @@ class StableBrowser {
611
690
  }
612
691
  return { rerun: false };
613
692
  }
693
+ getFilePath() {
694
+ const stackFrames = errorStackParser.parse(new Error());
695
+ const stackFrame = stackFrames.findLast((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
696
+ // return stackFrame?.fileName || null;
697
+ const filepath = stackFrame?.fileName;
698
+ if (filepath) {
699
+ let jsonFilePath = filepath.replace(".mjs", ".json");
700
+ if (existsSync(jsonFilePath)) {
701
+ return jsonFilePath;
702
+ }
703
+ const config = this.configuration ?? {};
704
+ if (!config?.locatorsMetadataDir) {
705
+ config.locatorsMetadataDir = "features/step_definitions/locators";
706
+ }
707
+ if (config && config.locatorsMetadataDir) {
708
+ jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
709
+ }
710
+ if (existsSync(jsonFilePath)) {
711
+ return jsonFilePath;
712
+ }
713
+ return null;
714
+ }
715
+ return null;
716
+ }
717
+ getFullElementLocators(selectors, filePath) {
718
+ if (!filePath || !existsSync(filePath)) {
719
+ return null;
720
+ }
721
+ const content = fs.readFileSync(filePath, "utf8");
722
+ try {
723
+ const allElements = JSON.parse(content);
724
+ const element_key = selectors?.element_key;
725
+ if (element_key && allElements[element_key]) {
726
+ return allElements[element_key];
727
+ }
728
+ for (const elementKey in allElements) {
729
+ const element = allElements[elementKey];
730
+ let foundStrategy = null;
731
+ for (const key in element) {
732
+ if (key === "strategy") {
733
+ continue;
734
+ }
735
+ const locators = element[key];
736
+ if (!locators || !locators.length) {
737
+ continue;
738
+ }
739
+ for (const locator of locators) {
740
+ delete locator.score;
741
+ }
742
+ if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
743
+ foundStrategy = key;
744
+ break;
745
+ }
746
+ }
747
+ if (foundStrategy) {
748
+ return element;
749
+ }
750
+ }
751
+ }
752
+ catch (error) {
753
+ console.error("Error parsing locators from file: " + filePath, error);
754
+ }
755
+ return null;
756
+ }
614
757
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
615
758
  if (!timeout) {
616
759
  timeout = 30000;
617
760
  }
761
+ let element = null;
762
+ let allStrategyLocators = null;
763
+ let selectedStrategy = null;
764
+ if (this.tryAllStrategies) {
765
+ allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
766
+ selectedStrategy = allStrategyLocators?.strategy;
767
+ }
618
768
  for (let i = 0; i < 3; i++) {
619
769
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
620
770
  for (let j = 0; j < selectors.locators.length; j++) {
621
771
  let selector = selectors.locators[j];
622
772
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
623
773
  }
624
- let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
774
+ if (this.tryAllStrategies && selectedStrategy) {
775
+ const strategyLocators = allStrategyLocators[selectedStrategy];
776
+ let err;
777
+ if (strategyLocators && strategyLocators.length) {
778
+ try {
779
+ selectors.locators = strategyLocators;
780
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
781
+ info.selectedStrategy = selectedStrategy;
782
+ info.log += "element found using strategy " + selectedStrategy + "\n";
783
+ }
784
+ catch (error) {
785
+ err = error;
786
+ }
787
+ }
788
+ if (!element) {
789
+ for (const key in allStrategyLocators) {
790
+ if (key === "strategy" || key === selectedStrategy) {
791
+ continue;
792
+ }
793
+ const strategyLocators = allStrategyLocators[key];
794
+ if (strategyLocators && strategyLocators.length) {
795
+ try {
796
+ info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
797
+ selectors.locators = strategyLocators;
798
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
799
+ err = null;
800
+ info.selectedStrategy = key;
801
+ info.log += "element found using strategy " + key + "\n";
802
+ break;
803
+ }
804
+ catch (error) {
805
+ err = error;
806
+ }
807
+ }
808
+ }
809
+ }
810
+ if (err) {
811
+ throw err;
812
+ }
813
+ }
814
+ else {
815
+ element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
816
+ }
625
817
  if (!element.rerun) {
626
- const randomToken = Math.random().toString(36).substring(7);
627
- await element.evaluate((el, randomToken) => {
628
- el.setAttribute("data-blinq-id-" + randomToken, "");
629
- }, randomToken);
630
- // if (element._frame) {
631
- // return element;
632
- // }
818
+ let newElementSelector = "";
819
+ if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
820
+ const cssSelector = await element.evaluate((el) => {
821
+ function getCssSelector(el) {
822
+ if (!el || el.nodeType !== 1 || el === document.body)
823
+ return el.tagName.toLowerCase();
824
+ const parent = el.parentElement;
825
+ const tag = el.tagName.toLowerCase();
826
+ // Find the index of the element among its siblings of the same tag
827
+ let index = 1;
828
+ for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
829
+ if (sibling.tagName === el.tagName) {
830
+ index++;
831
+ }
832
+ }
833
+ // Use nth-child if necessary (i.e., if there's more than one of the same tag)
834
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
835
+ const needsNthChild = siblings.length > 1;
836
+ const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
837
+ return getCssSelector(parent) + " > " + selector;
838
+ }
839
+ const cssSelector = getCssSelector(el);
840
+ return cssSelector;
841
+ });
842
+ newElementSelector = cssSelector;
843
+ }
844
+ else {
845
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
846
+ if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
847
+ const dataAttribute = "data-blinq-id";
848
+ await element.evaluate((el, [dataAttribute, randomToken]) => {
849
+ el.setAttribute(dataAttribute, randomToken);
850
+ }, [dataAttribute, randomToken]);
851
+ newElementSelector = `[${dataAttribute}="${randomToken}"]`;
852
+ }
853
+ else {
854
+ newElementSelector = await element.evaluate((el, token) => {
855
+ const id = el.id || "";
856
+ if (id) {
857
+ // use attribute and not id
858
+ const attrName = `data-blinq-id-${token}`;
859
+ el.setAttribute(attrName, "");
860
+ return `[${attrName}]`;
861
+ }
862
+ else {
863
+ // no id → assign the random token as the element's id
864
+ el.setAttribute("id", token);
865
+ return `#${token}`;
866
+ }
867
+ }, randomToken);
868
+ }
869
+ }
633
870
  const scope = element._frame ?? element.page();
634
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
635
871
  let prefixSelector = "";
636
872
  const frameControlSelector = " >> internal:control=enter-frame";
637
873
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
638
874
  if (frameSelectorIndex !== -1) {
639
875
  // remove everything after the >> internal:control=enter-frame
640
876
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
641
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
877
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
642
878
  }
643
879
  // if (element?._frame?._selector) {
644
880
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
645
881
  // }
646
882
  const newSelector = prefixSelector + newElementSelector;
647
- return scope.locator(newSelector);
883
+ return scope.locator(newSelector).first();
648
884
  }
649
885
  }
650
886
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -677,7 +913,7 @@ class StableBrowser {
677
913
  break;
678
914
  }
679
915
  catch (error) {
680
- console.error("frame not found " + frameLocator.css);
916
+ // console.error("frame not found " + frameLocator.css);
681
917
  }
682
918
  }
683
919
  }
@@ -743,6 +979,11 @@ class StableBrowser {
743
979
  });
744
980
  }
745
981
  async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
982
+ if (selectors.locators && Array.isArray(selectors.locators)) {
983
+ selectors.locators.forEach((locator) => {
984
+ locator.index = locator.index ?? 0;
985
+ });
986
+ }
746
987
  if (!info) {
747
988
  info = {};
748
989
  info.failCause = {};
@@ -755,7 +996,6 @@ class StableBrowser {
755
996
  let locatorsCount = 0;
756
997
  let lazy_scroll = false;
757
998
  //let arrayMode = Array.isArray(selectors);
758
- let scope = await this._findFrameScope(selectors, timeout, info);
759
999
  let selectorsLocators = null;
760
1000
  selectorsLocators = selectors.locators;
761
1001
  // group selectors by priority
@@ -783,6 +1023,7 @@ class StableBrowser {
783
1023
  let highPriorityOnly = true;
784
1024
  let visibleOnly = true;
785
1025
  while (true) {
1026
+ let scope = await this._findFrameScope(selectors, timeout, info);
786
1027
  locatorsCount = 0;
787
1028
  let result = [];
788
1029
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -898,9 +1139,13 @@ class StableBrowser {
898
1139
  }
899
1140
  }
900
1141
  if (foundLocators.length === 1) {
1142
+ let box = null;
1143
+ if (!this.onlyFailuresScreenshot) {
1144
+ box = await foundLocators[0].boundingBox();
1145
+ }
901
1146
  result.foundElements.push({
902
1147
  locator: foundLocators[0],
903
- box: await foundLocators[0].boundingBox(),
1148
+ box: box,
904
1149
  unique: true,
905
1150
  });
906
1151
  result.locatorIndex = i;
@@ -1055,11 +1300,16 @@ class StableBrowser {
1055
1300
  operation: "click",
1056
1301
  log: "***** click on " + selectors.element_name + " *****\n",
1057
1302
  };
1303
+ check_performance("click_all ***", this.context, true);
1058
1304
  try {
1305
+ check_performance("click_preCommand", this.context, true);
1059
1306
  await _preCommand(state, this);
1307
+ check_performance("click_preCommand", this.context, false);
1060
1308
  await performAction("click", state.element, options, this, state, _params);
1061
- if (!this.fastMode) {
1062
- await this.waitForPageLoad();
1309
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1310
+ check_performance("click_waitForPageLoad", this.context, true);
1311
+ await this.waitForPageLoad({ noSleep: true });
1312
+ check_performance("click_waitForPageLoad", this.context, false);
1063
1313
  }
1064
1314
  return state.info;
1065
1315
  }
@@ -1067,7 +1317,13 @@ class StableBrowser {
1067
1317
  await _commandError(state, e, this);
1068
1318
  }
1069
1319
  finally {
1320
+ check_performance("click_commandFinally", this.context, true);
1070
1321
  await _commandFinally(state, this);
1322
+ check_performance("click_commandFinally", this.context, false);
1323
+ check_performance("click_all ***", this.context, false);
1324
+ if (this.context.profile) {
1325
+ console.log(JSON.stringify(this.context.profile, null, 2));
1326
+ }
1071
1327
  }
1072
1328
  }
1073
1329
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1159,7 +1415,7 @@ class StableBrowser {
1159
1415
  }
1160
1416
  }
1161
1417
  }
1162
- await this.waitForPageLoad();
1418
+ //await this.waitForPageLoad();
1163
1419
  return state.info;
1164
1420
  }
1165
1421
  catch (e) {
@@ -1185,7 +1441,7 @@ class StableBrowser {
1185
1441
  await _preCommand(state, this);
1186
1442
  await performAction("hover", state.element, options, this, state, _params);
1187
1443
  await _screenshot(state, this);
1188
- await this.waitForPageLoad();
1444
+ //await this.waitForPageLoad();
1189
1445
  return state.info;
1190
1446
  }
1191
1447
  catch (e) {
@@ -1221,7 +1477,7 @@ class StableBrowser {
1221
1477
  state.info.log += "selectOption failed, will try force" + "\n";
1222
1478
  await state.element.selectOption(values, { timeout: 10000, force: true });
1223
1479
  }
1224
- await this.waitForPageLoad();
1480
+ //await this.waitForPageLoad();
1225
1481
  return state.info;
1226
1482
  }
1227
1483
  catch (e) {
@@ -1503,8 +1759,8 @@ class StableBrowser {
1503
1759
  if (enter) {
1504
1760
  await new Promise((resolve) => setTimeout(resolve, 2000));
1505
1761
  await this.page.keyboard.press("Enter");
1762
+ await this.waitForPageLoad();
1506
1763
  }
1507
- await this.waitForPageLoad();
1508
1764
  return state.info;
1509
1765
  }
1510
1766
  catch (e) {
@@ -2409,7 +2665,7 @@ class StableBrowser {
2409
2665
  }
2410
2666
  // Helper function to remove all style="" attributes
2411
2667
  const removeStyleAttributes = (htmlString) => {
2412
- return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, '');
2668
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2413
2669
  };
2414
2670
  // Remove style attributes for innerHTML and outerHTML properties
2415
2671
  if (property === "innerHTML" || property === "outerHTML") {
@@ -2418,47 +2674,54 @@ class StableBrowser {
2418
2674
  }
2419
2675
  state.info.value = val;
2420
2676
  let regex;
2421
- if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2422
- const patternBody = expectedValue.slice(1, -1);
2423
- const processedPattern = patternBody.replace(/\n/g, ".*");
2424
- regex = new RegExp(processedPattern, "gs");
2425
- state.info.regex = true;
2426
- }
2427
- else {
2428
- const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2429
- regex = new RegExp(escapedPattern, "g");
2430
- }
2431
- if (property === "innerText") {
2432
- if (state.info.regex) {
2433
- if (!regex.test(val)) {
2434
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2435
- state.info.failCause.assertionFailed = true;
2436
- state.info.failCause.lastError = errorMessage;
2437
- throw new Error(errorMessage);
2438
- }
2677
+ state.info.value = val;
2678
+ const isRegex = expectedValue.startsWith("regex:");
2679
+ const isContains = expectedValue.startsWith("contains:");
2680
+ const isExact = expectedValue.startsWith("exact:");
2681
+ let matchPassed = false;
2682
+ if (isRegex) {
2683
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2684
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2685
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2686
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2687
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2688
+ const regex = new RegExp(patternBody, flags);
2689
+ state.info.regex = true;
2690
+ matchPassed = regex.test(val);
2439
2691
  }
2440
2692
  else {
2441
- // Fix: Replace escaped newlines with actual newlines before splitting
2442
- const normalizedExpectedValue = expectedValue.replace(/\\n/g, '\n');
2443
- const valLines = val.split("\n");
2444
- const expectedLines = normalizedExpectedValue.split("\n");
2445
- // Check if all expected lines are present in the actual lines
2446
- const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2447
- if (!isPart) {
2448
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2449
- state.info.failCause.assertionFailed = true;
2450
- state.info.failCause.lastError = errorMessage;
2451
- throw new Error(errorMessage);
2452
- }
2693
+ // Fallback: treat as literal
2694
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2695
+ const regex = new RegExp(escapedPattern, "g");
2696
+ matchPassed = regex.test(val);
2453
2697
  }
2454
2698
  }
2699
+ else if (isContains) {
2700
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2701
+ matchPassed = val.includes(containsValue);
2702
+ }
2703
+ else if (isExact) {
2704
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2705
+ matchPassed = val === exactValue;
2706
+ }
2707
+ else if (property === "innerText") {
2708
+ // Default innerText logic
2709
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2710
+ const valLines = val.split("\n");
2711
+ const expectedLines = normalizedExpectedValue.split("\n");
2712
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2713
+ }
2455
2714
  else {
2456
- if (!val.match(regex)) {
2457
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2458
- state.info.failCause.assertionFailed = true;
2459
- state.info.failCause.lastError = errorMessage;
2460
- throw new Error(errorMessage);
2461
- }
2715
+ // Fallback exact or loose match
2716
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2717
+ const regex = new RegExp(escapedPattern, "g");
2718
+ matchPassed = regex.test(val);
2719
+ }
2720
+ if (!matchPassed) {
2721
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2722
+ state.info.failCause.assertionFailed = true;
2723
+ state.info.failCause.lastError = errorMessage;
2724
+ throw new Error(errorMessage);
2462
2725
  }
2463
2726
  return state.info;
2464
2727
  }
@@ -2469,6 +2732,133 @@ class StableBrowser {
2469
2732
  await _commandFinally(state, this);
2470
2733
  }
2471
2734
  }
2735
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2736
+ // Convert timeout from seconds to milliseconds
2737
+ const timeoutMs = timeout * 1000;
2738
+ const state = {
2739
+ selectors,
2740
+ _params,
2741
+ condition,
2742
+ timeout: timeoutMs, // Store as milliseconds for internal use
2743
+ options,
2744
+ world,
2745
+ type: Types.CONDITIONAL_WAIT,
2746
+ highlight: true,
2747
+ screenshot: true,
2748
+ text: `Conditional wait for element`,
2749
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2750
+ operation: "conditionalWait",
2751
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2752
+ allowDisabled: true,
2753
+ info: {},
2754
+ };
2755
+ state.options ??= { timeout: timeoutMs };
2756
+ // Initialize startTime outside try block to ensure it's always accessible
2757
+ const startTime = Date.now();
2758
+ let conditionMet = false;
2759
+ let currentValue = null;
2760
+ let lastError = null;
2761
+ // Main retry loop - continues until timeout or condition is met
2762
+ while (Date.now() - startTime < timeoutMs) {
2763
+ const elapsedTime = Date.now() - startTime;
2764
+ const remainingTime = timeoutMs - elapsedTime;
2765
+ try {
2766
+ // Try to execute _preCommand (element location)
2767
+ await _preCommand(state, this);
2768
+ // If _preCommand succeeds, start condition checking
2769
+ const checkCondition = async () => {
2770
+ try {
2771
+ switch (condition.toLowerCase()) {
2772
+ case "checked":
2773
+ currentValue = await state.element.isChecked();
2774
+ return currentValue === true;
2775
+ case "unchecked":
2776
+ currentValue = await state.element.isChecked();
2777
+ return currentValue === false;
2778
+ case "visible":
2779
+ currentValue = await state.element.isVisible();
2780
+ return currentValue === true;
2781
+ case "hidden":
2782
+ currentValue = await state.element.isVisible();
2783
+ return currentValue === false;
2784
+ case "enabled":
2785
+ currentValue = await state.element.isDisabled();
2786
+ return currentValue === false;
2787
+ case "disabled":
2788
+ currentValue = await state.element.isDisabled();
2789
+ return currentValue === true;
2790
+ case "editable":
2791
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2792
+ currentValue = await state.element.isContentEditable();
2793
+ return currentValue === true;
2794
+ default:
2795
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2796
+ state.info.success = false;
2797
+ return false;
2798
+ }
2799
+ }
2800
+ catch (error) {
2801
+ // Don't throw here, just return false to continue retrying
2802
+ return false;
2803
+ }
2804
+ };
2805
+ // Inner loop for condition checking (once element is located)
2806
+ while (Date.now() - startTime < timeoutMs) {
2807
+ const currentElapsedTime = Date.now() - startTime;
2808
+ conditionMet = await checkCondition();
2809
+ if (conditionMet) {
2810
+ break;
2811
+ }
2812
+ // Check if we still have time for another attempt
2813
+ if (Date.now() - startTime + 50 < timeoutMs) {
2814
+ await new Promise((res) => setTimeout(res, 50));
2815
+ }
2816
+ else {
2817
+ break;
2818
+ }
2819
+ }
2820
+ // If we got here and condition is met, break out of main loop
2821
+ if (conditionMet) {
2822
+ break;
2823
+ }
2824
+ // If condition not met but no exception, we've timed out
2825
+ break;
2826
+ }
2827
+ catch (e) {
2828
+ lastError = e;
2829
+ const currentElapsedTime = Date.now() - startTime;
2830
+ const timeLeft = timeoutMs - currentElapsedTime;
2831
+ // Check if we have enough time left to retry
2832
+ if (timeLeft > 100) {
2833
+ await new Promise((resolve) => setTimeout(resolve, 50));
2834
+ }
2835
+ else {
2836
+ break;
2837
+ }
2838
+ }
2839
+ }
2840
+ const actualWaitTime = Date.now() - startTime;
2841
+ state.info = {
2842
+ success: conditionMet,
2843
+ conditionMet,
2844
+ actualWaitTime,
2845
+ currentValue,
2846
+ lastError: lastError?.message || null,
2847
+ message: conditionMet
2848
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2849
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2850
+ };
2851
+ if (lastError) {
2852
+ state.log += `Last error: ${lastError.message}\n`;
2853
+ }
2854
+ try {
2855
+ await _commandFinally(state, this);
2856
+ }
2857
+ catch (finallyError) {
2858
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2859
+ }
2860
+ return state.info;
2861
+ }
2472
2862
  async extractEmailData(emailAddress, options, world) {
2473
2863
  if (!emailAddress) {
2474
2864
  throw new Error("email address is null");
@@ -3065,6 +3455,8 @@ class StableBrowser {
3065
3455
  operation: "verify_text_with_relation",
3066
3456
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3067
3457
  };
3458
+ const cmdStartTime = Date.now();
3459
+ let cmdEndTime = null;
3068
3460
  const timeout = this._getFindElementTimeout(options);
3069
3461
  await new Promise((resolve) => setTimeout(resolve, 2000));
3070
3462
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3100,6 +3492,17 @@ class StableBrowser {
3100
3492
  await new Promise((resolve) => setTimeout(resolve, 1000));
3101
3493
  continue;
3102
3494
  }
3495
+ else {
3496
+ cmdEndTime = Date.now();
3497
+ if (cmdEndTime - cmdStartTime > 55000) {
3498
+ if (foundAncore) {
3499
+ throw new Error(`Text ${textToVerify} not found in page`);
3500
+ }
3501
+ else {
3502
+ throw new Error(`Text ${textAnchor} not found in page`);
3503
+ }
3504
+ }
3505
+ }
3103
3506
  try {
3104
3507
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3105
3508
  foundAncore = true;
@@ -3238,7 +3641,7 @@ class StableBrowser {
3238
3641
  Object.assign(e, { info: info });
3239
3642
  error = e;
3240
3643
  // throw e;
3241
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3644
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3242
3645
  }
3243
3646
  finally {
3244
3647
  const endTime = Date.now();
@@ -3587,6 +3990,22 @@ class StableBrowser {
3587
3990
  }
3588
3991
  }
3589
3992
  async waitForPageLoad(options = {}, world = null) {
3993
+ // try {
3994
+ // let currentPagePath = null;
3995
+ // currentPagePath = new URL(this.page.url()).pathname;
3996
+ // if (this.latestPagePath) {
3997
+ // // get the currect page path and compare with the latest page path
3998
+ // if (this.latestPagePath === currentPagePath) {
3999
+ // // if the page path is the same, do not wait for page load
4000
+ // console.log("No page change: " + currentPagePath);
4001
+ // return;
4002
+ // }
4003
+ // }
4004
+ // this.latestPagePath = currentPagePath;
4005
+ // } catch (e) {
4006
+ // console.debug("Error getting current page path: ", e);
4007
+ // }
4008
+ //console.log("Waiting for page load");
3590
4009
  let timeout = this._getLoadTimeout(options);
3591
4010
  const promiseArray = [];
3592
4011
  // let waitForNetworkIdle = true;
@@ -3619,10 +4038,12 @@ class StableBrowser {
3619
4038
  else if (e.label === "domcontentloaded") {
3620
4039
  console.log("waited for the domcontent loaded timeout");
3621
4040
  }
3622
- console.log(".");
3623
4041
  }
3624
4042
  finally {
3625
- await new Promise((resolve) => setTimeout(resolve, 2000));
4043
+ await new Promise((resolve) => setTimeout(resolve, 500));
4044
+ if (options && !options.noSleep) {
4045
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4046
+ }
3626
4047
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3627
4048
  const endTime = Date.now();
3628
4049
  _reportToWorld(world, {
@@ -3663,7 +4084,6 @@ class StableBrowser {
3663
4084
  await this.page.close();
3664
4085
  }
3665
4086
  catch (e) {
3666
- console.log(".");
3667
4087
  await _commandError(state, e, this);
3668
4088
  }
3669
4089
  finally {
@@ -3677,7 +4097,7 @@ class StableBrowser {
3677
4097
  }
3678
4098
  operation = options.operation;
3679
4099
  // validate operation is one of the supported operations
3680
- if (operation != "click" && operation != "hover+click") {
4100
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3681
4101
  throw new Error("operation is not supported");
3682
4102
  }
3683
4103
  const state = {
@@ -3746,6 +4166,17 @@ class StableBrowser {
3746
4166
  state.element = results[0];
3747
4167
  await performAction("hover+click", state.element, options, this, state, _params);
3748
4168
  break;
4169
+ case "hover":
4170
+ if (!options.css) {
4171
+ throw new Error("css is not defined");
4172
+ }
4173
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4174
+ if (result1.length === 0) {
4175
+ throw new Error(`Element not found in cell area`);
4176
+ }
4177
+ state.element = result1[0];
4178
+ await performAction("hover", state.element, options, this, state, _params);
4179
+ break;
3749
4180
  default:
3750
4181
  throw new Error("operation is not supported");
3751
4182
  }
@@ -3778,7 +4209,6 @@ class StableBrowser {
3778
4209
  await this.page.setViewportSize({ width: width, height: hight });
3779
4210
  }
3780
4211
  catch (e) {
3781
- console.log(".");
3782
4212
  await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
3783
4213
  }
3784
4214
  finally {
@@ -3816,7 +4246,6 @@ class StableBrowser {
3816
4246
  await this.page.reload();
3817
4247
  }
3818
4248
  catch (e) {
3819
- console.log(".");
3820
4249
  await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
3821
4250
  }
3822
4251
  finally {
@@ -3860,6 +4289,10 @@ class StableBrowser {
3860
4289
  }
3861
4290
  }
3862
4291
  async beforeScenario(world, scenario) {
4292
+ if (world && world.attach) {
4293
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4294
+ }
4295
+ this.context.loadedRoutes = null;
3863
4296
  this.beforeScenarioCalled = true;
3864
4297
  if (scenario && scenario.pickle && scenario.pickle.name) {
3865
4298
  this.scenarioName = scenario.pickle.name;
@@ -3889,8 +4322,10 @@ class StableBrowser {
3889
4322
  }
3890
4323
  async afterScenario(world, scenario) { }
3891
4324
  async beforeStep(world, step) {
4325
+ this.stepTags = [];
3892
4326
  if (!this.beforeScenarioCalled) {
3893
4327
  this.beforeScenario(world, step);
4328
+ this.context.loadedRoutes = null;
3894
4329
  }
3895
4330
  if (this.stepIndex === undefined) {
3896
4331
  this.stepIndex = 0;
@@ -3900,7 +4335,12 @@ class StableBrowser {
3900
4335
  }
3901
4336
  if (step && step.pickleStep && step.pickleStep.text) {
3902
4337
  this.stepName = step.pickleStep.text;
3903
- this.logger.info("step: " + this.stepName);
4338
+ let printableStepName = this.stepName;
4339
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4340
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4341
+ return `\x1b[33m"${p1}"\x1b[0m`;
4342
+ });
4343
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
3904
4344
  }
3905
4345
  else if (step && step.text) {
3906
4346
  this.stepName = step.text;
@@ -3915,13 +4355,23 @@ class StableBrowser {
3915
4355
  }
3916
4356
  if (this.initSnapshotTaken === false) {
3917
4357
  this.initSnapshotTaken = true;
3918
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4358
+ if (world &&
4359
+ world.attach &&
4360
+ !process.env.DISABLE_SNAPSHOT &&
4361
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
3919
4362
  const snapshot = await this.getAriaSnapshot();
3920
4363
  if (snapshot) {
3921
4364
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3922
4365
  }
3923
4366
  }
3924
4367
  }
4368
+ this.context.routeResults = null;
4369
+ this.context.loadedRoutes = null;
4370
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4371
+ networkBeforeStep(this.stepName, this.context);
4372
+ }
4373
+ setStepTags(tags) {
4374
+ this.stepTags = tags;
3925
4375
  }
3926
4376
  async getAriaSnapshot() {
3927
4377
  try {
@@ -3941,12 +4391,18 @@ class StableBrowser {
3941
4391
  try {
3942
4392
  // Ensure frame is attached and has body
3943
4393
  const body = frame.locator("body");
3944
- await body.waitFor({ timeout: 200 }); // wait explicitly
4394
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
3945
4395
  const snapshot = await body.ariaSnapshot({ timeout });
4396
+ if (!snapshot) {
4397
+ continue;
4398
+ }
3946
4399
  content.push(`- frame: ${i}`);
3947
4400
  content.push(snapshot);
3948
4401
  }
3949
- catch (innerErr) { }
4402
+ catch (innerErr) {
4403
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4404
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4405
+ }
3950
4406
  }
3951
4407
  return content.join("\n");
3952
4408
  }
@@ -4018,13 +4474,23 @@ class StableBrowser {
4018
4474
  if (this.context) {
4019
4475
  this.context.examplesRow = null;
4020
4476
  }
4021
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4477
+ if (world &&
4478
+ world.attach &&
4479
+ !process.env.DISABLE_SNAPSHOT &&
4480
+ !this.fastMode &&
4481
+ !this.stepTags.includes("fast-mode")) {
4022
4482
  const snapshot = await this.getAriaSnapshot();
4023
4483
  if (snapshot) {
4024
4484
  const obj = {};
4025
4485
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4026
4486
  }
4027
4487
  }
4488
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4489
+ if (this.context.routeResults) {
4490
+ if (world && world.attach) {
4491
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4492
+ }
4493
+ }
4028
4494
  if (!process.env.TEMP_RUN) {
4029
4495
  const state = {
4030
4496
  world,
@@ -4048,6 +4514,13 @@ class StableBrowser {
4048
4514
  await _commandFinally(state, this);
4049
4515
  }
4050
4516
  }
4517
+ networkAfterStep(this.stepName, this.context);
4518
+ if (process.env.TEMP_RUN === "true") {
4519
+ // Put a sleep for some time to allow the browser to finish processing
4520
+ if (!this.stepTags.includes("fast-mode")) {
4521
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4522
+ }
4523
+ }
4051
4524
  }
4052
4525
  }
4053
4526
  function createTimedPromise(promise, label) {