automation_model 1.0.751-dev → 1.0.751-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,17 +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";
28
30
  import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
31
+ import { existsSync } from "node:fs";
29
32
  export const Types = {
30
33
  CLICK: "click_element",
31
34
  WAIT_ELEMENT: "wait_element",
32
- NAVIGATE: "navigate", ///
35
+ NAVIGATE: "navigate",
36
+ GO_BACK: "go_back",
37
+ GO_FORWARD: "go_forward",
33
38
  FILL: "fill_element",
34
39
  EXECUTE: "execute_page_method", //
35
40
  OPEN: "open_environment", //
@@ -42,6 +47,7 @@ export const Types = {
42
47
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
43
48
  ANALYZE_TABLE: "analyze_table",
44
49
  SELECT: "select_combobox", //
50
+ VERIFY_PROPERTY: "verify_element_property",
45
51
  VERIFY_PAGE_PATH: "verify_page_path",
46
52
  VERIFY_PAGE_TITLE: "verify_page_title",
47
53
  TYPE_PRESS: "type_press",
@@ -60,15 +66,15 @@ export const Types = {
60
66
  SET_INPUT: "set_input",
61
67
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
62
68
  VERIFY_ATTRIBUTE: "verify_element_attribute",
63
- VERIFY_PROPERTY: "verify_element_property",
64
69
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
65
70
  BRUNO: "bruno",
66
- SNAPSHOT_VALIDATION: "snapshot_validation",
67
71
  VERIFY_FILE_EXISTS: "verify_file_exists",
68
72
  SET_INPUT_FILES: "set_input_files",
73
+ SNAPSHOT_VALIDATION: "snapshot_validation",
69
74
  REPORT_COMMAND: "report_command",
70
75
  STEP_COMPLETE: "step_complete",
71
76
  SLEEP: "sleep",
77
+ CONDITIONAL_WAIT: "conditional_wait",
72
78
  };
73
79
  export const apps = {};
74
80
  const formatElementName = (elementName) => {
@@ -81,6 +87,7 @@ class StableBrowser {
81
87
  context;
82
88
  world;
83
89
  fastMode;
90
+ stepTags;
84
91
  project_path = null;
85
92
  webLogFile = null;
86
93
  networkLogger = null;
@@ -89,13 +96,15 @@ class StableBrowser {
89
96
  tags = null;
90
97
  isRecording = false;
91
98
  initSnapshotTaken = false;
92
- 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 = []) {
93
101
  this.browser = browser;
94
102
  this.page = page;
95
103
  this.logger = logger;
96
104
  this.context = context;
97
105
  this.world = world;
98
106
  this.fastMode = fastMode;
107
+ this.stepTags = stepTags;
99
108
  if (!this.logger) {
100
109
  this.logger = console;
101
110
  }
@@ -128,6 +137,7 @@ class StableBrowser {
128
137
  this.fastMode = true;
129
138
  }
130
139
  if (process.env.FAST_MODE === "true") {
140
+ // console.log("Fast mode enabled from environment variable");
131
141
  this.fastMode = true;
132
142
  }
133
143
  if (process.env.FAST_MODE === "false") {
@@ -170,6 +180,7 @@ class StableBrowser {
170
180
  registerNetworkEvents(this.world, this, context, this.page);
171
181
  registerDownloadEvent(this.page, this.world, context);
172
182
  page.on("close", async () => {
183
+ // return if browser context is already closed
173
184
  if (this.context && this.context.pages && this.context.pages.length > 1) {
174
185
  this.context.pages.pop();
175
186
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -179,7 +190,12 @@ class StableBrowser {
179
190
  console.log("Switched to page " + title);
180
191
  }
181
192
  catch (error) {
182
- 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
+ }
183
199
  }
184
200
  }
185
201
  });
@@ -188,7 +204,12 @@ class StableBrowser {
188
204
  console.log("Switch page: " + (await page.title()));
189
205
  }
190
206
  catch (e) {
191
- 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
+ }
192
213
  }
193
214
  context.pageLoading.status = false;
194
215
  }.bind(this));
@@ -216,7 +237,7 @@ class StableBrowser {
216
237
  if (newContextCreated) {
217
238
  this.registerEventListeners(this.context);
218
239
  await this.goto(this.context.environment.baseUrl);
219
- if (!this.fastMode) {
240
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
220
241
  await this.waitForPageLoad();
221
242
  }
222
243
  }
@@ -344,6 +365,64 @@ class StableBrowser {
344
365
  await _commandFinally(state, this);
345
366
  }
346
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
+ }
347
426
  async _getLocator(locator, scope, _params) {
348
427
  locator = _fixLocatorUsingParams(locator, _params);
349
428
  // locator = await this._replaceWithLocalData(locator);
@@ -442,12 +521,6 @@ class StableBrowser {
442
521
  if (!el.setAttribute) {
443
522
  el = el.parentElement;
444
523
  }
445
- // remove any attributes start with data-blinq-id
446
- // for (let i = 0; i < el.attributes.length; i++) {
447
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
448
- // el.removeAttribute(el.attributes[i].name);
449
- // }
450
- // }
451
524
  el.setAttribute("data-blinq-id-" + randomToken, "");
452
525
  return true;
453
526
  }, [tag1, randomToken]))) {
@@ -469,14 +542,13 @@ class StableBrowser {
469
542
  info.locatorLog = new LocatorLog(selectorHierarchy);
470
543
  }
471
544
  let locatorSearch = selectorHierarchy[index];
472
- let originalLocatorSearch = "";
473
545
  try {
474
- originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
475
- locatorSearch = JSON.parse(originalLocatorSearch);
546
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
476
547
  }
477
548
  catch (e) {
478
549
  console.error(e);
479
550
  }
551
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
480
552
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
481
553
  let locator = null;
482
554
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
@@ -618,40 +690,186 @@ class StableBrowser {
618
690
  }
619
691
  return { rerun: false };
620
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
+ }
621
757
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
622
758
  if (!timeout) {
623
759
  timeout = 30000;
624
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
+ }
625
768
  for (let i = 0; i < 3; i++) {
626
769
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
627
770
  for (let j = 0; j < selectors.locators.length; j++) {
628
771
  let selector = selectors.locators[j];
629
772
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
630
773
  }
631
- 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
+ }
632
817
  if (!element.rerun) {
633
- const randomToken = Math.random().toString(36).substring(7);
634
- await element.evaluate((el, randomToken) => {
635
- el.setAttribute("data-blinq-id-" + randomToken, "");
636
- }, randomToken);
637
- // if (element._frame) {
638
- // return element;
639
- // }
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
+ }
640
859
  const scope = element._frame ?? element.page();
641
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
642
860
  let prefixSelector = "";
643
861
  const frameControlSelector = " >> internal:control=enter-frame";
644
862
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
645
863
  if (frameSelectorIndex !== -1) {
646
864
  // remove everything after the >> internal:control=enter-frame
647
865
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
648
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
866
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
649
867
  }
650
868
  // if (element?._frame?._selector) {
651
869
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
652
870
  // }
653
871
  const newSelector = prefixSelector + newElementSelector;
654
- return scope.locator(newSelector);
872
+ return scope.locator(newSelector).first();
655
873
  }
656
874
  }
657
875
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -672,7 +890,7 @@ class StableBrowser {
672
890
  for (let i = 0; i < frame.selectors.length; i++) {
673
891
  let frameLocator = frame.selectors[i];
674
892
  if (frameLocator.css) {
675
- let testframescope = framescope.frameLocator(frameLocator.css);
893
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
676
894
  if (frameLocator.index) {
677
895
  testframescope = framescope.nth(frameLocator.index);
678
896
  }
@@ -684,7 +902,7 @@ class StableBrowser {
684
902
  break;
685
903
  }
686
904
  catch (error) {
687
- console.error("frame not found " + frameLocator.css);
905
+ // console.error("frame not found " + frameLocator.css);
688
906
  }
689
907
  }
690
908
  }
@@ -750,6 +968,11 @@ class StableBrowser {
750
968
  });
751
969
  }
752
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
+ }
753
976
  if (!info) {
754
977
  info = {};
755
978
  info.failCause = {};
@@ -762,7 +985,6 @@ class StableBrowser {
762
985
  let locatorsCount = 0;
763
986
  let lazy_scroll = false;
764
987
  //let arrayMode = Array.isArray(selectors);
765
- let scope = await this._findFrameScope(selectors, timeout, info);
766
988
  let selectorsLocators = null;
767
989
  selectorsLocators = selectors.locators;
768
990
  // group selectors by priority
@@ -790,6 +1012,7 @@ class StableBrowser {
790
1012
  let highPriorityOnly = true;
791
1013
  let visibleOnly = true;
792
1014
  while (true) {
1015
+ let scope = await this._findFrameScope(selectors, timeout, info);
793
1016
  locatorsCount = 0;
794
1017
  let result = [];
795
1018
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -905,9 +1128,13 @@ class StableBrowser {
905
1128
  }
906
1129
  }
907
1130
  if (foundLocators.length === 1) {
1131
+ let box = null;
1132
+ if (!this.onlyFailuresScreenshot) {
1133
+ box = await foundLocators[0].boundingBox();
1134
+ }
908
1135
  result.foundElements.push({
909
1136
  locator: foundLocators[0],
910
- box: await foundLocators[0].boundingBox(),
1137
+ box: box,
911
1138
  unique: true,
912
1139
  });
913
1140
  result.locatorIndex = i;
@@ -1062,11 +1289,16 @@ class StableBrowser {
1062
1289
  operation: "click",
1063
1290
  log: "***** click on " + selectors.element_name + " *****\n",
1064
1291
  };
1292
+ check_performance("click_all ***", this.context, true);
1065
1293
  try {
1294
+ check_performance("click_preCommand", this.context, true);
1066
1295
  await _preCommand(state, this);
1296
+ check_performance("click_preCommand", this.context, false);
1067
1297
  await performAction("click", state.element, options, this, state, _params);
1068
- if (!this.fastMode) {
1069
- 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);
1070
1302
  }
1071
1303
  return state.info;
1072
1304
  }
@@ -1074,7 +1306,13 @@ class StableBrowser {
1074
1306
  await _commandError(state, e, this);
1075
1307
  }
1076
1308
  finally {
1309
+ check_performance("click_commandFinally", this.context, true);
1077
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
+ }
1078
1316
  }
1079
1317
  }
1080
1318
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1166,7 +1404,7 @@ class StableBrowser {
1166
1404
  }
1167
1405
  }
1168
1406
  }
1169
- await this.waitForPageLoad();
1407
+ //await this.waitForPageLoad();
1170
1408
  return state.info;
1171
1409
  }
1172
1410
  catch (e) {
@@ -1192,7 +1430,7 @@ class StableBrowser {
1192
1430
  await _preCommand(state, this);
1193
1431
  await performAction("hover", state.element, options, this, state, _params);
1194
1432
  await _screenshot(state, this);
1195
- await this.waitForPageLoad();
1433
+ //await this.waitForPageLoad();
1196
1434
  return state.info;
1197
1435
  }
1198
1436
  catch (e) {
@@ -1228,7 +1466,7 @@ class StableBrowser {
1228
1466
  state.info.log += "selectOption failed, will try force" + "\n";
1229
1467
  await state.element.selectOption(values, { timeout: 10000, force: true });
1230
1468
  }
1231
- await this.waitForPageLoad();
1469
+ //await this.waitForPageLoad();
1232
1470
  return state.info;
1233
1471
  }
1234
1472
  catch (e) {
@@ -1414,6 +1652,14 @@ class StableBrowser {
1414
1652
  }
1415
1653
  try {
1416
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);
1417
1663
  state.info.value = _value;
1418
1664
  if (!options.press) {
1419
1665
  try {
@@ -1439,6 +1685,25 @@ class StableBrowser {
1439
1685
  }
1440
1686
  }
1441
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
+ }
1442
1707
  const valueSegment = state.value.split("&&");
1443
1708
  for (let i = 0; i < valueSegment.length; i++) {
1444
1709
  if (i > 0) {
@@ -1510,8 +1775,8 @@ class StableBrowser {
1510
1775
  if (enter) {
1511
1776
  await new Promise((resolve) => setTimeout(resolve, 2000));
1512
1777
  await this.page.keyboard.press("Enter");
1778
+ await this.waitForPageLoad();
1513
1779
  }
1514
- await this.waitForPageLoad();
1515
1780
  return state.info;
1516
1781
  }
1517
1782
  catch (e) {
@@ -2425,47 +2690,54 @@ class StableBrowser {
2425
2690
  }
2426
2691
  state.info.value = val;
2427
2692
  let regex;
2428
- if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2429
- const patternBody = expectedValue.slice(1, -1);
2430
- const processedPattern = patternBody.replace(/\n/g, ".*");
2431
- regex = new RegExp(processedPattern, "gs");
2432
- state.info.regex = true;
2433
- }
2434
- else {
2435
- const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2436
- regex = new RegExp(escapedPattern, "g");
2437
- }
2438
- if (property === "innerText") {
2439
- if (state.info.regex) {
2440
- if (!regex.test(val)) {
2441
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2442
- state.info.failCause.assertionFailed = true;
2443
- state.info.failCause.lastError = errorMessage;
2444
- throw new Error(errorMessage);
2445
- }
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);
2446
2707
  }
2447
2708
  else {
2448
- // Fix: Replace escaped newlines with actual newlines before splitting
2449
- const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2450
- const valLines = val.split("\n");
2451
- const expectedLines = normalizedExpectedValue.split("\n");
2452
- // Check if all expected lines are present in the actual lines
2453
- const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2454
- if (!isPart) {
2455
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2456
- state.info.failCause.assertionFailed = true;
2457
- state.info.failCause.lastError = errorMessage;
2458
- throw new Error(errorMessage);
2459
- }
2709
+ // Fallback: treat as literal
2710
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2711
+ const regex = new RegExp(escapedPattern, "g");
2712
+ matchPassed = regex.test(val);
2460
2713
  }
2461
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
+ }
2462
2730
  else {
2463
- if (!val.match(regex)) {
2464
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2465
- state.info.failCause.assertionFailed = true;
2466
- state.info.failCause.lastError = errorMessage;
2467
- throw new Error(errorMessage);
2468
- }
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);
2469
2741
  }
2470
2742
  return state.info;
2471
2743
  }
@@ -2476,6 +2748,133 @@ class StableBrowser {
2476
2748
  await _commandFinally(state, this);
2477
2749
  }
2478
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
+ }
2479
2878
  async extractEmailData(emailAddress, options, world) {
2480
2879
  if (!emailAddress) {
2481
2880
  throw new Error("email address is null");
@@ -3072,6 +3471,8 @@ class StableBrowser {
3072
3471
  operation: "verify_text_with_relation",
3073
3472
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3074
3473
  };
3474
+ const cmdStartTime = Date.now();
3475
+ let cmdEndTime = null;
3075
3476
  const timeout = this._getFindElementTimeout(options);
3076
3477
  await new Promise((resolve) => setTimeout(resolve, 2000));
3077
3478
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3107,6 +3508,17 @@ class StableBrowser {
3107
3508
  await new Promise((resolve) => setTimeout(resolve, 1000));
3108
3509
  continue;
3109
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
+ }
3110
3522
  try {
3111
3523
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3112
3524
  foundAncore = true;
@@ -3245,7 +3657,7 @@ class StableBrowser {
3245
3657
  Object.assign(e, { info: info });
3246
3658
  error = e;
3247
3659
  // throw e;
3248
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3660
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3249
3661
  }
3250
3662
  finally {
3251
3663
  const endTime = Date.now();
@@ -3594,6 +4006,22 @@ class StableBrowser {
3594
4006
  }
3595
4007
  }
3596
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");
3597
4025
  let timeout = this._getLoadTimeout(options);
3598
4026
  const promiseArray = [];
3599
4027
  // let waitForNetworkIdle = true;
@@ -3626,10 +4054,12 @@ class StableBrowser {
3626
4054
  else if (e.label === "domcontentloaded") {
3627
4055
  console.log("waited for the domcontent loaded timeout");
3628
4056
  }
3629
- console.log(".");
3630
4057
  }
3631
4058
  finally {
3632
- 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
+ }
3633
4063
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3634
4064
  const endTime = Date.now();
3635
4065
  _reportToWorld(world, {
@@ -3670,7 +4100,6 @@ class StableBrowser {
3670
4100
  await this.page.close();
3671
4101
  }
3672
4102
  catch (e) {
3673
- console.log(".");
3674
4103
  await _commandError(state, e, this);
3675
4104
  }
3676
4105
  finally {
@@ -3684,7 +4113,7 @@ class StableBrowser {
3684
4113
  }
3685
4114
  operation = options.operation;
3686
4115
  // validate operation is one of the supported operations
3687
- if (operation != "click" && operation != "hover+click") {
4116
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3688
4117
  throw new Error("operation is not supported");
3689
4118
  }
3690
4119
  const state = {
@@ -3753,6 +4182,17 @@ class StableBrowser {
3753
4182
  state.element = results[0];
3754
4183
  await performAction("hover+click", state.element, options, this, state, _params);
3755
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;
3756
4196
  default:
3757
4197
  throw new Error("operation is not supported");
3758
4198
  }
@@ -3785,7 +4225,6 @@ class StableBrowser {
3785
4225
  await this.page.setViewportSize({ width: width, height: hight });
3786
4226
  }
3787
4227
  catch (e) {
3788
- console.log(".");
3789
4228
  await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
3790
4229
  }
3791
4230
  finally {
@@ -3823,7 +4262,6 @@ class StableBrowser {
3823
4262
  await this.page.reload();
3824
4263
  }
3825
4264
  catch (e) {
3826
- console.log(".");
3827
4265
  await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
3828
4266
  }
3829
4267
  finally {
@@ -3867,6 +4305,10 @@ class StableBrowser {
3867
4305
  }
3868
4306
  }
3869
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;
3870
4312
  this.beforeScenarioCalled = true;
3871
4313
  if (scenario && scenario.pickle && scenario.pickle.name) {
3872
4314
  this.scenarioName = scenario.pickle.name;
@@ -3896,8 +4338,10 @@ class StableBrowser {
3896
4338
  }
3897
4339
  async afterScenario(world, scenario) { }
3898
4340
  async beforeStep(world, step) {
4341
+ this.stepTags = [];
3899
4342
  if (!this.beforeScenarioCalled) {
3900
4343
  this.beforeScenario(world, step);
4344
+ this.context.loadedRoutes = null;
3901
4345
  }
3902
4346
  if (this.stepIndex === undefined) {
3903
4347
  this.stepIndex = 0;
@@ -3907,7 +4351,12 @@ class StableBrowser {
3907
4351
  }
3908
4352
  if (step && step.pickleStep && step.pickleStep.text) {
3909
4353
  this.stepName = step.pickleStep.text;
3910
- 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);
3911
4360
  }
3912
4361
  else if (step && step.text) {
3913
4362
  this.stepName = step.text;
@@ -3922,7 +4371,10 @@ class StableBrowser {
3922
4371
  }
3923
4372
  if (this.initSnapshotTaken === false) {
3924
4373
  this.initSnapshotTaken = true;
3925
- 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"))) {
3926
4378
  const snapshot = await this.getAriaSnapshot();
3927
4379
  if (snapshot) {
3928
4380
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -3930,7 +4382,12 @@ class StableBrowser {
3930
4382
  }
3931
4383
  }
3932
4384
  this.context.routeResults = null;
3933
- await registerBeforeStepRoutes(this.context, this.stepName);
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;
3934
4391
  }
3935
4392
  async getAriaSnapshot() {
3936
4393
  try {
@@ -3950,12 +4407,18 @@ class StableBrowser {
3950
4407
  try {
3951
4408
  // Ensure frame is attached and has body
3952
4409
  const body = frame.locator("body");
3953
- await body.waitFor({ timeout: 200 }); // wait explicitly
4410
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
3954
4411
  const snapshot = await body.ariaSnapshot({ timeout });
4412
+ if (!snapshot) {
4413
+ continue;
4414
+ }
3955
4415
  content.push(`- frame: ${i}`);
3956
4416
  content.push(snapshot);
3957
4417
  }
3958
- catch (innerErr) { }
4418
+ catch (innerErr) {
4419
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4420
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4421
+ }
3959
4422
  }
3960
4423
  return content.join("\n");
3961
4424
  }
@@ -4027,7 +4490,11 @@ class StableBrowser {
4027
4490
  if (this.context) {
4028
4491
  this.context.examplesRow = null;
4029
4492
  }
4030
- 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")) {
4031
4498
  const snapshot = await this.getAriaSnapshot();
4032
4499
  if (snapshot) {
4033
4500
  const obj = {};
@@ -4035,6 +4502,11 @@ class StableBrowser {
4035
4502
  }
4036
4503
  }
4037
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
+ }
4038
4510
  if (!process.env.TEMP_RUN) {
4039
4511
  const state = {
4040
4512
  world,
@@ -4058,6 +4530,13 @@ class StableBrowser {
4058
4530
  await _commandFinally(state, this);
4059
4531
  }
4060
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
+ }
4061
4540
  }
4062
4541
  }
4063
4542
  function createTimedPromise(promise, label) {