automation_model 1.0.750-dev → 1.0.750-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.
@@ -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
  }
@@ -127,6 +137,7 @@ class StableBrowser {
127
137
  this.fastMode = true;
128
138
  }
129
139
  if (process.env.FAST_MODE === "true") {
140
+ // console.log("Fast mode enabled from environment variable");
130
141
  this.fastMode = true;
131
142
  }
132
143
  if (process.env.FAST_MODE === "false") {
@@ -169,6 +180,7 @@ class StableBrowser {
169
180
  registerNetworkEvents(this.world, this, context, this.page);
170
181
  registerDownloadEvent(this.page, this.world, context);
171
182
  page.on("close", async () => {
183
+ // return if browser context is already closed
172
184
  if (this.context && this.context.pages && this.context.pages.length > 1) {
173
185
  this.context.pages.pop();
174
186
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -178,7 +190,12 @@ class StableBrowser {
178
190
  console.log("Switched to page " + title);
179
191
  }
180
192
  catch (error) {
181
- 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
+ }
182
199
  }
183
200
  }
184
201
  });
@@ -187,7 +204,12 @@ class StableBrowser {
187
204
  console.log("Switch page: " + (await page.title()));
188
205
  }
189
206
  catch (e) {
190
- 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
+ }
191
213
  }
192
214
  context.pageLoading.status = false;
193
215
  }.bind(this));
@@ -215,7 +237,7 @@ class StableBrowser {
215
237
  if (newContextCreated) {
216
238
  this.registerEventListeners(this.context);
217
239
  await this.goto(this.context.environment.baseUrl);
218
- if (!this.fastMode) {
240
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
219
241
  await this.waitForPageLoad();
220
242
  }
221
243
  }
@@ -343,6 +365,64 @@ class StableBrowser {
343
365
  await _commandFinally(state, this);
344
366
  }
345
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
+ }
346
426
  async _getLocator(locator, scope, _params) {
347
427
  locator = _fixLocatorUsingParams(locator, _params);
348
428
  // locator = await this._replaceWithLocalData(locator);
@@ -441,12 +521,6 @@ class StableBrowser {
441
521
  if (!el.setAttribute) {
442
522
  el = el.parentElement;
443
523
  }
444
- // remove any attributes start with data-blinq-id
445
- // for (let i = 0; i < el.attributes.length; i++) {
446
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
447
- // el.removeAttribute(el.attributes[i].name);
448
- // }
449
- // }
450
524
  el.setAttribute("data-blinq-id-" + randomToken, "");
451
525
  return true;
452
526
  }, [tag1, randomToken]))) {
@@ -468,14 +542,13 @@ class StableBrowser {
468
542
  info.locatorLog = new LocatorLog(selectorHierarchy);
469
543
  }
470
544
  let locatorSearch = selectorHierarchy[index];
471
- let originalLocatorSearch = "";
472
545
  try {
473
- originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
474
- locatorSearch = JSON.parse(originalLocatorSearch);
546
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
475
547
  }
476
548
  catch (e) {
477
549
  console.error(e);
478
550
  }
551
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
479
552
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
480
553
  let locator = null;
481
554
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
@@ -617,40 +690,186 @@ class StableBrowser {
617
690
  }
618
691
  return { rerun: false };
619
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
+ }
620
757
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
621
758
  if (!timeout) {
622
759
  timeout = 30000;
623
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
+ }
624
768
  for (let i = 0; i < 3; i++) {
625
769
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
626
770
  for (let j = 0; j < selectors.locators.length; j++) {
627
771
  let selector = selectors.locators[j];
628
772
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
629
773
  }
630
- 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
+ }
631
817
  if (!element.rerun) {
632
- const randomToken = Math.random().toString(36).substring(7);
633
- await element.evaluate((el, randomToken) => {
634
- el.setAttribute("data-blinq-id-" + randomToken, "");
635
- }, randomToken);
636
- // if (element._frame) {
637
- // return element;
638
- // }
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
+ // the default case just return the located element
855
+ // will not work for click and type if the locator is placeholder and the placeholder change due to the click event
856
+ return element;
857
+ }
858
+ }
639
859
  const scope = element._frame ?? element.page();
640
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
641
860
  let prefixSelector = "";
642
861
  const frameControlSelector = " >> internal:control=enter-frame";
643
862
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
644
863
  if (frameSelectorIndex !== -1) {
645
864
  // remove everything after the >> internal:control=enter-frame
646
865
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
647
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
866
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
648
867
  }
649
868
  // if (element?._frame?._selector) {
650
869
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
651
870
  // }
652
871
  const newSelector = prefixSelector + newElementSelector;
653
- return scope.locator(newSelector);
872
+ return scope.locator(newSelector).first();
654
873
  }
655
874
  }
656
875
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -671,7 +890,7 @@ class StableBrowser {
671
890
  for (let i = 0; i < frame.selectors.length; i++) {
672
891
  let frameLocator = frame.selectors[i];
673
892
  if (frameLocator.css) {
674
- let testframescope = framescope.frameLocator(frameLocator.css);
893
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
675
894
  if (frameLocator.index) {
676
895
  testframescope = framescope.nth(frameLocator.index);
677
896
  }
@@ -683,7 +902,7 @@ class StableBrowser {
683
902
  break;
684
903
  }
685
904
  catch (error) {
686
- console.error("frame not found " + frameLocator.css);
905
+ // console.error("frame not found " + frameLocator.css);
687
906
  }
688
907
  }
689
908
  }
@@ -749,6 +968,11 @@ class StableBrowser {
749
968
  });
750
969
  }
751
970
  async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
971
+ if (selectors.locators && Array.isArray(selectors.locators)) {
972
+ selectors.locators.forEach((locator) => {
973
+ locator.index = locator.index ?? 0;
974
+ });
975
+ }
752
976
  if (!info) {
753
977
  info = {};
754
978
  info.failCause = {};
@@ -761,7 +985,6 @@ class StableBrowser {
761
985
  let locatorsCount = 0;
762
986
  let lazy_scroll = false;
763
987
  //let arrayMode = Array.isArray(selectors);
764
- let scope = await this._findFrameScope(selectors, timeout, info);
765
988
  let selectorsLocators = null;
766
989
  selectorsLocators = selectors.locators;
767
990
  // group selectors by priority
@@ -789,6 +1012,7 @@ class StableBrowser {
789
1012
  let highPriorityOnly = true;
790
1013
  let visibleOnly = true;
791
1014
  while (true) {
1015
+ let scope = await this._findFrameScope(selectors, timeout, info);
792
1016
  locatorsCount = 0;
793
1017
  let result = [];
794
1018
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -904,9 +1128,13 @@ class StableBrowser {
904
1128
  }
905
1129
  }
906
1130
  if (foundLocators.length === 1) {
1131
+ let box = null;
1132
+ if (!this.onlyFailuresScreenshot) {
1133
+ box = await foundLocators[0].boundingBox();
1134
+ }
907
1135
  result.foundElements.push({
908
1136
  locator: foundLocators[0],
909
- box: await foundLocators[0].boundingBox(),
1137
+ box: box,
910
1138
  unique: true,
911
1139
  });
912
1140
  result.locatorIndex = i;
@@ -1061,11 +1289,16 @@ class StableBrowser {
1061
1289
  operation: "click",
1062
1290
  log: "***** click on " + selectors.element_name + " *****\n",
1063
1291
  };
1292
+ check_performance("click_all ***", this.context, true);
1064
1293
  try {
1294
+ check_performance("click_preCommand", this.context, true);
1065
1295
  await _preCommand(state, this);
1296
+ check_performance("click_preCommand", this.context, false);
1066
1297
  await performAction("click", state.element, options, this, state, _params);
1067
- if (!this.fastMode) {
1068
- await this.waitForPageLoad();
1298
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1299
+ check_performance("click_waitForPageLoad", this.context, true);
1300
+ await this.waitForPageLoad({ noSleep: true });
1301
+ check_performance("click_waitForPageLoad", this.context, false);
1069
1302
  }
1070
1303
  return state.info;
1071
1304
  }
@@ -1073,7 +1306,13 @@ class StableBrowser {
1073
1306
  await _commandError(state, e, this);
1074
1307
  }
1075
1308
  finally {
1309
+ check_performance("click_commandFinally", this.context, true);
1076
1310
  await _commandFinally(state, this);
1311
+ check_performance("click_commandFinally", this.context, false);
1312
+ check_performance("click_all ***", this.context, false);
1313
+ if (this.context.profile) {
1314
+ console.log(JSON.stringify(this.context.profile, null, 2));
1315
+ }
1077
1316
  }
1078
1317
  }
1079
1318
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1165,7 +1404,7 @@ class StableBrowser {
1165
1404
  }
1166
1405
  }
1167
1406
  }
1168
- await this.waitForPageLoad();
1407
+ //await this.waitForPageLoad();
1169
1408
  return state.info;
1170
1409
  }
1171
1410
  catch (e) {
@@ -1191,7 +1430,7 @@ class StableBrowser {
1191
1430
  await _preCommand(state, this);
1192
1431
  await performAction("hover", state.element, options, this, state, _params);
1193
1432
  await _screenshot(state, this);
1194
- await this.waitForPageLoad();
1433
+ //await this.waitForPageLoad();
1195
1434
  return state.info;
1196
1435
  }
1197
1436
  catch (e) {
@@ -1227,7 +1466,7 @@ class StableBrowser {
1227
1466
  state.info.log += "selectOption failed, will try force" + "\n";
1228
1467
  await state.element.selectOption(values, { timeout: 10000, force: true });
1229
1468
  }
1230
- await this.waitForPageLoad();
1469
+ //await this.waitForPageLoad();
1231
1470
  return state.info;
1232
1471
  }
1233
1472
  catch (e) {
@@ -1413,6 +1652,14 @@ class StableBrowser {
1413
1652
  }
1414
1653
  try {
1415
1654
  await _preCommand(state, this);
1655
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1656
+ // tag the element
1657
+ let newElementSelector = await state.element.evaluate((el, token) => {
1658
+ // use attribute and not id
1659
+ const attrName = `data-blinq-id-${token}`;
1660
+ el.setAttribute(attrName, "");
1661
+ return `[${attrName}]`;
1662
+ }, randomToken);
1416
1663
  state.info.value = _value;
1417
1664
  if (!options.press) {
1418
1665
  try {
@@ -1438,6 +1685,25 @@ class StableBrowser {
1438
1685
  }
1439
1686
  }
1440
1687
  await new Promise((resolve) => setTimeout(resolve, 500));
1688
+ // check if the element exist after the click (no wait)
1689
+ const count = await state.element.count({ timeout: 0 });
1690
+ if (count === 0) {
1691
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1692
+ const scope = state.element._frame ?? element.page();
1693
+ let prefixSelector = "";
1694
+ const frameControlSelector = " >> internal:control=enter-frame";
1695
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1696
+ if (frameSelectorIndex !== -1) {
1697
+ // remove everything after the >> internal:control=enter-frame
1698
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1699
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1700
+ }
1701
+ // if (element?._frame?._selector) {
1702
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1703
+ // }
1704
+ const newSelector = prefixSelector + newElementSelector;
1705
+ state.element = scope.locator(newSelector).first();
1706
+ }
1441
1707
  const valueSegment = state.value.split("&&");
1442
1708
  for (let i = 0; i < valueSegment.length; i++) {
1443
1709
  if (i > 0) {
@@ -1509,8 +1775,8 @@ class StableBrowser {
1509
1775
  if (enter) {
1510
1776
  await new Promise((resolve) => setTimeout(resolve, 2000));
1511
1777
  await this.page.keyboard.press("Enter");
1778
+ await this.waitForPageLoad();
1512
1779
  }
1513
- await this.waitForPageLoad();
1514
1780
  return state.info;
1515
1781
  }
1516
1782
  catch (e) {
@@ -2424,47 +2690,54 @@ class StableBrowser {
2424
2690
  }
2425
2691
  state.info.value = val;
2426
2692
  let regex;
2427
- if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2428
- const patternBody = expectedValue.slice(1, -1);
2429
- const processedPattern = patternBody.replace(/\n/g, ".*");
2430
- regex = new RegExp(processedPattern, "gs");
2431
- state.info.regex = true;
2432
- }
2433
- else {
2434
- const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2435
- regex = new RegExp(escapedPattern, "g");
2436
- }
2437
- if (property === "innerText") {
2438
- if (state.info.regex) {
2439
- if (!regex.test(val)) {
2440
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2441
- state.info.failCause.assertionFailed = true;
2442
- state.info.failCause.lastError = errorMessage;
2443
- throw new Error(errorMessage);
2444
- }
2693
+ state.info.value = val;
2694
+ const isRegex = expectedValue.startsWith("regex:");
2695
+ const isContains = expectedValue.startsWith("contains:");
2696
+ const isExact = expectedValue.startsWith("exact:");
2697
+ let matchPassed = false;
2698
+ if (isRegex) {
2699
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2700
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2701
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2702
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2703
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2704
+ const regex = new RegExp(patternBody, flags);
2705
+ state.info.regex = true;
2706
+ matchPassed = regex.test(val);
2445
2707
  }
2446
2708
  else {
2447
- // Fix: Replace escaped newlines with actual newlines before splitting
2448
- const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2449
- const valLines = val.split("\n");
2450
- const expectedLines = normalizedExpectedValue.split("\n");
2451
- // Check if all expected lines are present in the actual lines
2452
- const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2453
- if (!isPart) {
2454
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2455
- state.info.failCause.assertionFailed = true;
2456
- state.info.failCause.lastError = errorMessage;
2457
- throw new Error(errorMessage);
2458
- }
2709
+ // Fallback: treat as literal
2710
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2711
+ const regex = new RegExp(escapedPattern, "g");
2712
+ matchPassed = regex.test(val);
2459
2713
  }
2460
2714
  }
2715
+ else if (isContains) {
2716
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2717
+ matchPassed = val.includes(containsValue);
2718
+ }
2719
+ else if (isExact) {
2720
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2721
+ matchPassed = val === exactValue;
2722
+ }
2723
+ else if (property === "innerText") {
2724
+ // Default innerText logic
2725
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2726
+ const valLines = val.split("\n");
2727
+ const expectedLines = normalizedExpectedValue.split("\n");
2728
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2729
+ }
2461
2730
  else {
2462
- if (!val.match(regex)) {
2463
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2464
- state.info.failCause.assertionFailed = true;
2465
- state.info.failCause.lastError = errorMessage;
2466
- throw new Error(errorMessage);
2467
- }
2731
+ // Fallback exact or loose match
2732
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2733
+ const regex = new RegExp(escapedPattern, "g");
2734
+ matchPassed = regex.test(val);
2735
+ }
2736
+ if (!matchPassed) {
2737
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2738
+ state.info.failCause.assertionFailed = true;
2739
+ state.info.failCause.lastError = errorMessage;
2740
+ throw new Error(errorMessage);
2468
2741
  }
2469
2742
  return state.info;
2470
2743
  }
@@ -2475,6 +2748,133 @@ class StableBrowser {
2475
2748
  await _commandFinally(state, this);
2476
2749
  }
2477
2750
  }
2751
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2752
+ // Convert timeout from seconds to milliseconds
2753
+ const timeoutMs = timeout * 1000;
2754
+ const state = {
2755
+ selectors,
2756
+ _params,
2757
+ condition,
2758
+ timeout: timeoutMs, // Store as milliseconds for internal use
2759
+ options,
2760
+ world,
2761
+ type: Types.CONDITIONAL_WAIT,
2762
+ highlight: true,
2763
+ screenshot: true,
2764
+ text: `Conditional wait for element`,
2765
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2766
+ operation: "conditionalWait",
2767
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2768
+ allowDisabled: true,
2769
+ info: {},
2770
+ };
2771
+ state.options ??= { timeout: timeoutMs };
2772
+ // Initialize startTime outside try block to ensure it's always accessible
2773
+ const startTime = Date.now();
2774
+ let conditionMet = false;
2775
+ let currentValue = null;
2776
+ let lastError = null;
2777
+ // Main retry loop - continues until timeout or condition is met
2778
+ while (Date.now() - startTime < timeoutMs) {
2779
+ const elapsedTime = Date.now() - startTime;
2780
+ const remainingTime = timeoutMs - elapsedTime;
2781
+ try {
2782
+ // Try to execute _preCommand (element location)
2783
+ await _preCommand(state, this);
2784
+ // If _preCommand succeeds, start condition checking
2785
+ const checkCondition = async () => {
2786
+ try {
2787
+ switch (condition.toLowerCase()) {
2788
+ case "checked":
2789
+ currentValue = await state.element.isChecked();
2790
+ return currentValue === true;
2791
+ case "unchecked":
2792
+ currentValue = await state.element.isChecked();
2793
+ return currentValue === false;
2794
+ case "visible":
2795
+ currentValue = await state.element.isVisible();
2796
+ return currentValue === true;
2797
+ case "hidden":
2798
+ currentValue = await state.element.isVisible();
2799
+ return currentValue === false;
2800
+ case "enabled":
2801
+ currentValue = await state.element.isDisabled();
2802
+ return currentValue === false;
2803
+ case "disabled":
2804
+ currentValue = await state.element.isDisabled();
2805
+ return currentValue === true;
2806
+ case "editable":
2807
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2808
+ currentValue = await state.element.isContentEditable();
2809
+ return currentValue === true;
2810
+ default:
2811
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2812
+ state.info.success = false;
2813
+ return false;
2814
+ }
2815
+ }
2816
+ catch (error) {
2817
+ // Don't throw here, just return false to continue retrying
2818
+ return false;
2819
+ }
2820
+ };
2821
+ // Inner loop for condition checking (once element is located)
2822
+ while (Date.now() - startTime < timeoutMs) {
2823
+ const currentElapsedTime = Date.now() - startTime;
2824
+ conditionMet = await checkCondition();
2825
+ if (conditionMet) {
2826
+ break;
2827
+ }
2828
+ // Check if we still have time for another attempt
2829
+ if (Date.now() - startTime + 50 < timeoutMs) {
2830
+ await new Promise((res) => setTimeout(res, 50));
2831
+ }
2832
+ else {
2833
+ break;
2834
+ }
2835
+ }
2836
+ // If we got here and condition is met, break out of main loop
2837
+ if (conditionMet) {
2838
+ break;
2839
+ }
2840
+ // If condition not met but no exception, we've timed out
2841
+ break;
2842
+ }
2843
+ catch (e) {
2844
+ lastError = e;
2845
+ const currentElapsedTime = Date.now() - startTime;
2846
+ const timeLeft = timeoutMs - currentElapsedTime;
2847
+ // Check if we have enough time left to retry
2848
+ if (timeLeft > 100) {
2849
+ await new Promise((resolve) => setTimeout(resolve, 50));
2850
+ }
2851
+ else {
2852
+ break;
2853
+ }
2854
+ }
2855
+ }
2856
+ const actualWaitTime = Date.now() - startTime;
2857
+ state.info = {
2858
+ success: conditionMet,
2859
+ conditionMet,
2860
+ actualWaitTime,
2861
+ currentValue,
2862
+ lastError: lastError?.message || null,
2863
+ message: conditionMet
2864
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2865
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2866
+ };
2867
+ if (lastError) {
2868
+ state.log += `Last error: ${lastError.message}\n`;
2869
+ }
2870
+ try {
2871
+ await _commandFinally(state, this);
2872
+ }
2873
+ catch (finallyError) {
2874
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2875
+ }
2876
+ return state.info;
2877
+ }
2478
2878
  async extractEmailData(emailAddress, options, world) {
2479
2879
  if (!emailAddress) {
2480
2880
  throw new Error("email address is null");
@@ -3071,6 +3471,8 @@ class StableBrowser {
3071
3471
  operation: "verify_text_with_relation",
3072
3472
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3073
3473
  };
3474
+ const cmdStartTime = Date.now();
3475
+ let cmdEndTime = null;
3074
3476
  const timeout = this._getFindElementTimeout(options);
3075
3477
  await new Promise((resolve) => setTimeout(resolve, 2000));
3076
3478
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3106,6 +3508,17 @@ class StableBrowser {
3106
3508
  await new Promise((resolve) => setTimeout(resolve, 1000));
3107
3509
  continue;
3108
3510
  }
3511
+ else {
3512
+ cmdEndTime = Date.now();
3513
+ if (cmdEndTime - cmdStartTime > 55000) {
3514
+ if (foundAncore) {
3515
+ throw new Error(`Text ${textToVerify} not found in page`);
3516
+ }
3517
+ else {
3518
+ throw new Error(`Text ${textAnchor} not found in page`);
3519
+ }
3520
+ }
3521
+ }
3109
3522
  try {
3110
3523
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3111
3524
  foundAncore = true;
@@ -3244,7 +3657,7 @@ class StableBrowser {
3244
3657
  Object.assign(e, { info: info });
3245
3658
  error = e;
3246
3659
  // throw e;
3247
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3660
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3248
3661
  }
3249
3662
  finally {
3250
3663
  const endTime = Date.now();
@@ -3593,6 +4006,22 @@ class StableBrowser {
3593
4006
  }
3594
4007
  }
3595
4008
  async waitForPageLoad(options = {}, world = null) {
4009
+ // try {
4010
+ // let currentPagePath = null;
4011
+ // currentPagePath = new URL(this.page.url()).pathname;
4012
+ // if (this.latestPagePath) {
4013
+ // // get the currect page path and compare with the latest page path
4014
+ // if (this.latestPagePath === currentPagePath) {
4015
+ // // if the page path is the same, do not wait for page load
4016
+ // console.log("No page change: " + currentPagePath);
4017
+ // return;
4018
+ // }
4019
+ // }
4020
+ // this.latestPagePath = currentPagePath;
4021
+ // } catch (e) {
4022
+ // console.debug("Error getting current page path: ", e);
4023
+ // }
4024
+ //console.log("Waiting for page load");
3596
4025
  let timeout = this._getLoadTimeout(options);
3597
4026
  const promiseArray = [];
3598
4027
  // let waitForNetworkIdle = true;
@@ -3625,10 +4054,12 @@ class StableBrowser {
3625
4054
  else if (e.label === "domcontentloaded") {
3626
4055
  console.log("waited for the domcontent loaded timeout");
3627
4056
  }
3628
- console.log(".");
3629
4057
  }
3630
4058
  finally {
3631
- await new Promise((resolve) => setTimeout(resolve, 2000));
4059
+ await new Promise((resolve) => setTimeout(resolve, 500));
4060
+ if (options && !options.noSleep) {
4061
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4062
+ }
3632
4063
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3633
4064
  const endTime = Date.now();
3634
4065
  _reportToWorld(world, {
@@ -3669,7 +4100,6 @@ class StableBrowser {
3669
4100
  await this.page.close();
3670
4101
  }
3671
4102
  catch (e) {
3672
- console.log(".");
3673
4103
  await _commandError(state, e, this);
3674
4104
  }
3675
4105
  finally {
@@ -3683,7 +4113,7 @@ class StableBrowser {
3683
4113
  }
3684
4114
  operation = options.operation;
3685
4115
  // validate operation is one of the supported operations
3686
- if (operation != "click" && operation != "hover+click") {
4116
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3687
4117
  throw new Error("operation is not supported");
3688
4118
  }
3689
4119
  const state = {
@@ -3752,6 +4182,17 @@ class StableBrowser {
3752
4182
  state.element = results[0];
3753
4183
  await performAction("hover+click", state.element, options, this, state, _params);
3754
4184
  break;
4185
+ case "hover":
4186
+ if (!options.css) {
4187
+ throw new Error("css is not defined");
4188
+ }
4189
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4190
+ if (result1.length === 0) {
4191
+ throw new Error(`Element not found in cell area`);
4192
+ }
4193
+ state.element = result1[0];
4194
+ await performAction("hover", state.element, options, this, state, _params);
4195
+ break;
3755
4196
  default:
3756
4197
  throw new Error("operation is not supported");
3757
4198
  }
@@ -3784,7 +4225,6 @@ class StableBrowser {
3784
4225
  await this.page.setViewportSize({ width: width, height: hight });
3785
4226
  }
3786
4227
  catch (e) {
3787
- console.log(".");
3788
4228
  await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
3789
4229
  }
3790
4230
  finally {
@@ -3822,7 +4262,6 @@ class StableBrowser {
3822
4262
  await this.page.reload();
3823
4263
  }
3824
4264
  catch (e) {
3825
- console.log(".");
3826
4265
  await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
3827
4266
  }
3828
4267
  finally {
@@ -3866,6 +4305,10 @@ class StableBrowser {
3866
4305
  }
3867
4306
  }
3868
4307
  async beforeScenario(world, scenario) {
4308
+ if (world && world.attach) {
4309
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4310
+ }
4311
+ this.context.loadedRoutes = null;
3869
4312
  this.beforeScenarioCalled = true;
3870
4313
  if (scenario && scenario.pickle && scenario.pickle.name) {
3871
4314
  this.scenarioName = scenario.pickle.name;
@@ -3895,8 +4338,10 @@ class StableBrowser {
3895
4338
  }
3896
4339
  async afterScenario(world, scenario) { }
3897
4340
  async beforeStep(world, step) {
4341
+ this.stepTags = [];
3898
4342
  if (!this.beforeScenarioCalled) {
3899
4343
  this.beforeScenario(world, step);
4344
+ this.context.loadedRoutes = null;
3900
4345
  }
3901
4346
  if (this.stepIndex === undefined) {
3902
4347
  this.stepIndex = 0;
@@ -3906,7 +4351,12 @@ class StableBrowser {
3906
4351
  }
3907
4352
  if (step && step.pickleStep && step.pickleStep.text) {
3908
4353
  this.stepName = step.pickleStep.text;
3909
- this.logger.info("step: " + this.stepName);
4354
+ let printableStepName = this.stepName;
4355
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4356
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4357
+ return `\x1b[33m"${p1}"\x1b[0m`;
4358
+ });
4359
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
3910
4360
  }
3911
4361
  else if (step && step.text) {
3912
4362
  this.stepName = step.text;
@@ -3921,13 +4371,23 @@ class StableBrowser {
3921
4371
  }
3922
4372
  if (this.initSnapshotTaken === false) {
3923
4373
  this.initSnapshotTaken = true;
3924
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4374
+ if (world &&
4375
+ world.attach &&
4376
+ !process.env.DISABLE_SNAPSHOT &&
4377
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
3925
4378
  const snapshot = await this.getAriaSnapshot();
3926
4379
  if (snapshot) {
3927
4380
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3928
4381
  }
3929
4382
  }
3930
4383
  }
4384
+ this.context.routeResults = null;
4385
+ this.context.loadedRoutes = null;
4386
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4387
+ networkBeforeStep(this.stepName, this.context);
4388
+ }
4389
+ setStepTags(tags) {
4390
+ this.stepTags = tags;
3931
4391
  }
3932
4392
  async getAriaSnapshot() {
3933
4393
  try {
@@ -3947,12 +4407,18 @@ class StableBrowser {
3947
4407
  try {
3948
4408
  // Ensure frame is attached and has body
3949
4409
  const body = frame.locator("body");
3950
- await body.waitFor({ timeout: 200 }); // wait explicitly
4410
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
3951
4411
  const snapshot = await body.ariaSnapshot({ timeout });
4412
+ if (!snapshot) {
4413
+ continue;
4414
+ }
3952
4415
  content.push(`- frame: ${i}`);
3953
4416
  content.push(snapshot);
3954
4417
  }
3955
- catch (innerErr) { }
4418
+ catch (innerErr) {
4419
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4420
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4421
+ }
3956
4422
  }
3957
4423
  return content.join("\n");
3958
4424
  }
@@ -4024,13 +4490,23 @@ class StableBrowser {
4024
4490
  if (this.context) {
4025
4491
  this.context.examplesRow = null;
4026
4492
  }
4027
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4493
+ if (world &&
4494
+ world.attach &&
4495
+ !process.env.DISABLE_SNAPSHOT &&
4496
+ !this.fastMode &&
4497
+ !this.stepTags.includes("fast-mode")) {
4028
4498
  const snapshot = await this.getAriaSnapshot();
4029
4499
  if (snapshot) {
4030
4500
  const obj = {};
4031
4501
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4032
4502
  }
4033
4503
  }
4504
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4505
+ if (this.context.routeResults) {
4506
+ if (world && world.attach) {
4507
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4508
+ }
4509
+ }
4034
4510
  if (!process.env.TEMP_RUN) {
4035
4511
  const state = {
4036
4512
  world,
@@ -4054,6 +4530,13 @@ class StableBrowser {
4054
4530
  await _commandFinally(state, this);
4055
4531
  }
4056
4532
  }
4533
+ networkAfterStep(this.stepName, this.context);
4534
+ if (process.env.TEMP_RUN === "true") {
4535
+ // Put a sleep for some time to allow the browser to finish processing
4536
+ if (!this.stepTags.includes("fast-mode")) {
4537
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4538
+ }
4539
+ }
4057
4540
  }
4058
4541
  }
4059
4542
  function createTimedPromise(promise, label) {