automation_model 1.0.764-dev → 1.0.764-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.
- package/lib/api.js +17 -10
- package/lib/api.js.map +1 -1
- package/lib/auto_page.js +66 -18
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +56 -32
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.js.map +1 -1
- package/lib/check_performance.d.ts +1 -0
- package/lib/check_performance.js +57 -0
- package/lib/check_performance.js.map +1 -0
- package/lib/command_common.js +17 -1
- package/lib/command_common.js.map +1 -1
- package/lib/file_checker.js +136 -25
- package/lib/file_checker.js.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +1 -2
- package/lib/init_browser.js +122 -126
- package/lib/init_browser.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +2 -0
- package/lib/network.js +398 -87
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +65 -15
- package/lib/route.js +541 -175
- package/lib/route.js.map +1 -1
- package/lib/scripts/axe.mini.js +23994 -1
- package/lib/snapshot_validation.js.map +1 -1
- package/lib/stable_browser.d.ts +9 -4
- package/lib/stable_browser.js +490 -148
- package/lib/stable_browser.js.map +1 -1
- package/lib/table_helper.js +14 -0
- package/lib/table_helper.js.map +1 -1
- package/lib/test_context.d.ts +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +6 -2
- package/lib/utils.js +121 -14
- package/lib/utils.js.map +1 -1
- package/package.json +17 -10
package/lib/stable_browser.js
CHANGED
|
@@ -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,13 +21,14 @@ 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",
|
|
@@ -44,6 +47,7 @@ export const Types = {
|
|
|
44
47
|
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
45
48
|
ANALYZE_TABLE: "analyze_table",
|
|
46
49
|
SELECT: "select_combobox", //
|
|
50
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
47
51
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
48
52
|
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
49
53
|
TYPE_PRESS: "type_press",
|
|
@@ -62,12 +66,11 @@ export const Types = {
|
|
|
62
66
|
SET_INPUT: "set_input",
|
|
63
67
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
64
68
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
65
|
-
VERIFY_PROPERTY: "verify_element_property",
|
|
66
69
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
67
70
|
BRUNO: "bruno",
|
|
68
|
-
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
69
71
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
70
72
|
SET_INPUT_FILES: "set_input_files",
|
|
73
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
71
74
|
REPORT_COMMAND: "report_command",
|
|
72
75
|
STEP_COMPLETE: "step_complete",
|
|
73
76
|
SLEEP: "sleep",
|
|
@@ -84,6 +87,7 @@ class StableBrowser {
|
|
|
84
87
|
context;
|
|
85
88
|
world;
|
|
86
89
|
fastMode;
|
|
90
|
+
stepTags;
|
|
87
91
|
project_path = null;
|
|
88
92
|
webLogFile = null;
|
|
89
93
|
networkLogger = null;
|
|
@@ -92,13 +96,15 @@ class StableBrowser {
|
|
|
92
96
|
tags = null;
|
|
93
97
|
isRecording = false;
|
|
94
98
|
initSnapshotTaken = false;
|
|
95
|
-
|
|
99
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
100
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
96
101
|
this.browser = browser;
|
|
97
102
|
this.page = page;
|
|
98
103
|
this.logger = logger;
|
|
99
104
|
this.context = context;
|
|
100
105
|
this.world = world;
|
|
101
106
|
this.fastMode = fastMode;
|
|
107
|
+
this.stepTags = stepTags;
|
|
102
108
|
if (!this.logger) {
|
|
103
109
|
this.logger = console;
|
|
104
110
|
}
|
|
@@ -131,6 +137,7 @@ class StableBrowser {
|
|
|
131
137
|
this.fastMode = true;
|
|
132
138
|
}
|
|
133
139
|
if (process.env.FAST_MODE === "true") {
|
|
140
|
+
// console.log("Fast mode enabled from environment variable");
|
|
134
141
|
this.fastMode = true;
|
|
135
142
|
}
|
|
136
143
|
if (process.env.FAST_MODE === "false") {
|
|
@@ -173,6 +180,7 @@ class StableBrowser {
|
|
|
173
180
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
174
181
|
registerDownloadEvent(this.page, this.world, context);
|
|
175
182
|
page.on("close", async () => {
|
|
183
|
+
// return if browser context is already closed
|
|
176
184
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
177
185
|
this.context.pages.pop();
|
|
178
186
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -182,7 +190,12 @@ class StableBrowser {
|
|
|
182
190
|
console.log("Switched to page " + title);
|
|
183
191
|
}
|
|
184
192
|
catch (error) {
|
|
185
|
-
|
|
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
|
+
}
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
});
|
|
@@ -191,7 +204,12 @@ class StableBrowser {
|
|
|
191
204
|
console.log("Switch page: " + (await page.title()));
|
|
192
205
|
}
|
|
193
206
|
catch (e) {
|
|
194
|
-
|
|
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
|
+
}
|
|
195
213
|
}
|
|
196
214
|
context.pageLoading.status = false;
|
|
197
215
|
}.bind(this));
|
|
@@ -219,7 +237,7 @@ class StableBrowser {
|
|
|
219
237
|
if (newContextCreated) {
|
|
220
238
|
this.registerEventListeners(this.context);
|
|
221
239
|
await this.goto(this.context.environment.baseUrl);
|
|
222
|
-
if (!this.fastMode) {
|
|
240
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
223
241
|
await this.waitForPageLoad();
|
|
224
242
|
}
|
|
225
243
|
}
|
|
@@ -503,12 +521,6 @@ class StableBrowser {
|
|
|
503
521
|
if (!el.setAttribute) {
|
|
504
522
|
el = el.parentElement;
|
|
505
523
|
}
|
|
506
|
-
// remove any attributes start with data-blinq-id
|
|
507
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
508
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
509
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
510
|
-
// }
|
|
511
|
-
// }
|
|
512
524
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
513
525
|
return true;
|
|
514
526
|
}, [tag1, randomToken]))) {
|
|
@@ -678,40 +690,186 @@ class StableBrowser {
|
|
|
678
690
|
}
|
|
679
691
|
return { rerun: false };
|
|
680
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
|
+
}
|
|
681
757
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
682
758
|
if (!timeout) {
|
|
683
759
|
timeout = 30000;
|
|
684
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
|
+
}
|
|
685
768
|
for (let i = 0; i < 3; i++) {
|
|
686
769
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
687
770
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
688
771
|
let selector = selectors.locators[j];
|
|
689
772
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
690
773
|
}
|
|
691
|
-
|
|
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
|
+
}
|
|
692
817
|
if (!element.rerun) {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
+
}
|
|
700
859
|
const scope = element._frame ?? element.page();
|
|
701
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
702
860
|
let prefixSelector = "";
|
|
703
861
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
704
862
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
705
863
|
if (frameSelectorIndex !== -1) {
|
|
706
864
|
// remove everything after the >> internal:control=enter-frame
|
|
707
865
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
708
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
866
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
709
867
|
}
|
|
710
868
|
// if (element?._frame?._selector) {
|
|
711
869
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
712
870
|
// }
|
|
713
871
|
const newSelector = prefixSelector + newElementSelector;
|
|
714
|
-
return scope.locator(newSelector);
|
|
872
|
+
return scope.locator(newSelector).first();
|
|
715
873
|
}
|
|
716
874
|
}
|
|
717
875
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -732,7 +890,7 @@ class StableBrowser {
|
|
|
732
890
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
733
891
|
let frameLocator = frame.selectors[i];
|
|
734
892
|
if (frameLocator.css) {
|
|
735
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
893
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
736
894
|
if (frameLocator.index) {
|
|
737
895
|
testframescope = framescope.nth(frameLocator.index);
|
|
738
896
|
}
|
|
@@ -744,7 +902,7 @@ class StableBrowser {
|
|
|
744
902
|
break;
|
|
745
903
|
}
|
|
746
904
|
catch (error) {
|
|
747
|
-
console.error("frame not found " + frameLocator.css);
|
|
905
|
+
// console.error("frame not found " + frameLocator.css);
|
|
748
906
|
}
|
|
749
907
|
}
|
|
750
908
|
}
|
|
@@ -810,6 +968,15 @@ class StableBrowser {
|
|
|
810
968
|
});
|
|
811
969
|
}
|
|
812
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
|
+
locator.visible = locator.visible ?? true;
|
|
975
|
+
if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
976
|
+
locator.css = locator.css + " >> visible=true";
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
}
|
|
813
980
|
if (!info) {
|
|
814
981
|
info = {};
|
|
815
982
|
info.failCause = {};
|
|
@@ -822,7 +989,6 @@ class StableBrowser {
|
|
|
822
989
|
let locatorsCount = 0;
|
|
823
990
|
let lazy_scroll = false;
|
|
824
991
|
//let arrayMode = Array.isArray(selectors);
|
|
825
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
826
992
|
let selectorsLocators = null;
|
|
827
993
|
selectorsLocators = selectors.locators;
|
|
828
994
|
// group selectors by priority
|
|
@@ -850,6 +1016,7 @@ class StableBrowser {
|
|
|
850
1016
|
let highPriorityOnly = true;
|
|
851
1017
|
let visibleOnly = true;
|
|
852
1018
|
while (true) {
|
|
1019
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
853
1020
|
locatorsCount = 0;
|
|
854
1021
|
let result = [];
|
|
855
1022
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -965,9 +1132,13 @@ class StableBrowser {
|
|
|
965
1132
|
}
|
|
966
1133
|
}
|
|
967
1134
|
if (foundLocators.length === 1) {
|
|
1135
|
+
let box = null;
|
|
1136
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1137
|
+
box = await foundLocators[0].boundingBox();
|
|
1138
|
+
}
|
|
968
1139
|
result.foundElements.push({
|
|
969
1140
|
locator: foundLocators[0],
|
|
970
|
-
box:
|
|
1141
|
+
box: box,
|
|
971
1142
|
unique: true,
|
|
972
1143
|
});
|
|
973
1144
|
result.locatorIndex = i;
|
|
@@ -1122,11 +1293,22 @@ class StableBrowser {
|
|
|
1122
1293
|
operation: "click",
|
|
1123
1294
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1124
1295
|
};
|
|
1296
|
+
check_performance("click_all ***", this.context, true);
|
|
1297
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
1298
|
+
if (stepFastMode) {
|
|
1299
|
+
state.onlyFailuresScreenshot = true;
|
|
1300
|
+
state.scroll = false;
|
|
1301
|
+
state.highlight = false;
|
|
1302
|
+
}
|
|
1125
1303
|
try {
|
|
1304
|
+
check_performance("click_preCommand", this.context, true);
|
|
1126
1305
|
await _preCommand(state, this);
|
|
1306
|
+
check_performance("click_preCommand", this.context, false);
|
|
1127
1307
|
await performAction("click", state.element, options, this, state, _params);
|
|
1128
|
-
if (!this.fastMode) {
|
|
1129
|
-
|
|
1308
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1309
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1310
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1311
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1130
1312
|
}
|
|
1131
1313
|
return state.info;
|
|
1132
1314
|
}
|
|
@@ -1134,7 +1316,13 @@ class StableBrowser {
|
|
|
1134
1316
|
await _commandError(state, e, this);
|
|
1135
1317
|
}
|
|
1136
1318
|
finally {
|
|
1319
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1137
1320
|
await _commandFinally(state, this);
|
|
1321
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1322
|
+
check_performance("click_all ***", this.context, false);
|
|
1323
|
+
if (this.context.profile) {
|
|
1324
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1325
|
+
}
|
|
1138
1326
|
}
|
|
1139
1327
|
}
|
|
1140
1328
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1226,7 +1414,7 @@ class StableBrowser {
|
|
|
1226
1414
|
}
|
|
1227
1415
|
}
|
|
1228
1416
|
}
|
|
1229
|
-
await this.waitForPageLoad();
|
|
1417
|
+
//await this.waitForPageLoad();
|
|
1230
1418
|
return state.info;
|
|
1231
1419
|
}
|
|
1232
1420
|
catch (e) {
|
|
@@ -1252,7 +1440,7 @@ class StableBrowser {
|
|
|
1252
1440
|
await _preCommand(state, this);
|
|
1253
1441
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1254
1442
|
await _screenshot(state, this);
|
|
1255
|
-
await this.waitForPageLoad();
|
|
1443
|
+
//await this.waitForPageLoad();
|
|
1256
1444
|
return state.info;
|
|
1257
1445
|
}
|
|
1258
1446
|
catch (e) {
|
|
@@ -1288,7 +1476,7 @@ class StableBrowser {
|
|
|
1288
1476
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1289
1477
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1290
1478
|
}
|
|
1291
|
-
await this.waitForPageLoad();
|
|
1479
|
+
//await this.waitForPageLoad();
|
|
1292
1480
|
return state.info;
|
|
1293
1481
|
}
|
|
1294
1482
|
catch (e) {
|
|
@@ -1474,6 +1662,14 @@ class StableBrowser {
|
|
|
1474
1662
|
}
|
|
1475
1663
|
try {
|
|
1476
1664
|
await _preCommand(state, this);
|
|
1665
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1666
|
+
// tag the element
|
|
1667
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1668
|
+
// use attribute and not id
|
|
1669
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1670
|
+
el.setAttribute(attrName, "");
|
|
1671
|
+
return `[${attrName}]`;
|
|
1672
|
+
}, randomToken);
|
|
1477
1673
|
state.info.value = _value;
|
|
1478
1674
|
if (!options.press) {
|
|
1479
1675
|
try {
|
|
@@ -1499,6 +1695,25 @@ class StableBrowser {
|
|
|
1499
1695
|
}
|
|
1500
1696
|
}
|
|
1501
1697
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1698
|
+
// check if the element exist after the click (no wait)
|
|
1699
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1700
|
+
if (count === 0) {
|
|
1701
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1702
|
+
const scope = state.element._frame ?? element.page();
|
|
1703
|
+
let prefixSelector = "";
|
|
1704
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1705
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1706
|
+
if (frameSelectorIndex !== -1) {
|
|
1707
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1708
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1709
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1710
|
+
}
|
|
1711
|
+
// if (element?._frame?._selector) {
|
|
1712
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1713
|
+
// }
|
|
1714
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1715
|
+
state.element = scope.locator(newSelector).first();
|
|
1716
|
+
}
|
|
1502
1717
|
const valueSegment = state.value.split("&&");
|
|
1503
1718
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1504
1719
|
if (i > 0) {
|
|
@@ -1570,8 +1785,8 @@ class StableBrowser {
|
|
|
1570
1785
|
if (enter) {
|
|
1571
1786
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1572
1787
|
await this.page.keyboard.press("Enter");
|
|
1788
|
+
await this.waitForPageLoad();
|
|
1573
1789
|
}
|
|
1574
|
-
await this.waitForPageLoad();
|
|
1575
1790
|
return state.info;
|
|
1576
1791
|
}
|
|
1577
1792
|
catch (e) {
|
|
@@ -2437,7 +2652,7 @@ class StableBrowser {
|
|
|
2437
2652
|
let expectedValue;
|
|
2438
2653
|
try {
|
|
2439
2654
|
await _preCommand(state, this);
|
|
2440
|
-
expectedValue = await
|
|
2655
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2441
2656
|
state.info.expectedValue = expectedValue;
|
|
2442
2657
|
switch (property) {
|
|
2443
2658
|
case "innerText":
|
|
@@ -2485,47 +2700,54 @@ class StableBrowser {
|
|
|
2485
2700
|
}
|
|
2486
2701
|
state.info.value = val;
|
|
2487
2702
|
let regex;
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
state.info.failCause.assertionFailed = true;
|
|
2503
|
-
state.info.failCause.lastError = errorMessage;
|
|
2504
|
-
throw new Error(errorMessage);
|
|
2505
|
-
}
|
|
2703
|
+
state.info.value = val;
|
|
2704
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2705
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2706
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2707
|
+
let matchPassed = false;
|
|
2708
|
+
if (isRegex) {
|
|
2709
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2710
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2711
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2712
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2713
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2714
|
+
const regex = new RegExp(patternBody, flags);
|
|
2715
|
+
state.info.regex = true;
|
|
2716
|
+
matchPassed = regex.test(val);
|
|
2506
2717
|
}
|
|
2507
2718
|
else {
|
|
2508
|
-
//
|
|
2509
|
-
const
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
// Check if all expected lines are present in the actual lines
|
|
2513
|
-
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2514
|
-
if (!isPart) {
|
|
2515
|
-
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2516
|
-
state.info.failCause.assertionFailed = true;
|
|
2517
|
-
state.info.failCause.lastError = errorMessage;
|
|
2518
|
-
throw new Error(errorMessage);
|
|
2519
|
-
}
|
|
2719
|
+
// Fallback: treat as literal
|
|
2720
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2721
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2722
|
+
matchPassed = regex.test(val);
|
|
2520
2723
|
}
|
|
2521
2724
|
}
|
|
2725
|
+
else if (isContains) {
|
|
2726
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2727
|
+
matchPassed = val.includes(containsValue);
|
|
2728
|
+
}
|
|
2729
|
+
else if (isExact) {
|
|
2730
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2731
|
+
matchPassed = val === exactValue;
|
|
2732
|
+
}
|
|
2733
|
+
else if (property === "innerText") {
|
|
2734
|
+
// Default innerText logic
|
|
2735
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2736
|
+
const valLines = val.split("\n");
|
|
2737
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2738
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2739
|
+
}
|
|
2522
2740
|
else {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2741
|
+
// Fallback exact or loose match
|
|
2742
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2743
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2744
|
+
matchPassed = regex.test(val);
|
|
2745
|
+
}
|
|
2746
|
+
if (!matchPassed) {
|
|
2747
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2748
|
+
state.info.failCause.assertionFailed = true;
|
|
2749
|
+
state.info.failCause.lastError = errorMessage;
|
|
2750
|
+
throw new Error(errorMessage);
|
|
2529
2751
|
}
|
|
2530
2752
|
return state.info;
|
|
2531
2753
|
}
|
|
@@ -2556,81 +2778,112 @@ class StableBrowser {
|
|
|
2556
2778
|
allowDisabled: true,
|
|
2557
2779
|
info: {},
|
|
2558
2780
|
};
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2781
|
+
state.options ??= { timeout: timeoutMs };
|
|
2782
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2783
|
+
const startTime = Date.now();
|
|
2784
|
+
let conditionMet = false;
|
|
2785
|
+
let currentValue = null;
|
|
2786
|
+
let lastError = null;
|
|
2787
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2788
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2789
|
+
const elapsedTime = Date.now() - startTime;
|
|
2790
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2791
|
+
try {
|
|
2792
|
+
// Try to execute _preCommand (element location)
|
|
2793
|
+
await _preCommand(state, this);
|
|
2794
|
+
// If _preCommand succeeds, start condition checking
|
|
2795
|
+
const checkCondition = async () => {
|
|
2796
|
+
try {
|
|
2797
|
+
switch (condition.toLowerCase()) {
|
|
2798
|
+
case "checked":
|
|
2799
|
+
currentValue = await state.element.isChecked();
|
|
2800
|
+
return currentValue === true;
|
|
2801
|
+
case "unchecked":
|
|
2802
|
+
currentValue = await state.element.isChecked();
|
|
2803
|
+
return currentValue === false;
|
|
2804
|
+
case "visible":
|
|
2805
|
+
currentValue = await state.element.isVisible();
|
|
2806
|
+
return currentValue === true;
|
|
2807
|
+
case "hidden":
|
|
2808
|
+
currentValue = await state.element.isVisible();
|
|
2809
|
+
return currentValue === false;
|
|
2810
|
+
case "enabled":
|
|
2811
|
+
currentValue = await state.element.isDisabled();
|
|
2812
|
+
return currentValue === false;
|
|
2813
|
+
case "disabled":
|
|
2814
|
+
currentValue = await state.element.isDisabled();
|
|
2815
|
+
return currentValue === true;
|
|
2816
|
+
case "editable":
|
|
2817
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2818
|
+
currentValue = await state.element.isContentEditable();
|
|
2819
|
+
return currentValue === true;
|
|
2820
|
+
default:
|
|
2821
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2822
|
+
state.info.success = false;
|
|
2823
|
+
return false;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
catch (error) {
|
|
2827
|
+
// Don't throw here, just return false to continue retrying
|
|
2828
|
+
return false;
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
// Inner loop for condition checking (once element is located)
|
|
2832
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2833
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2834
|
+
conditionMet = await checkCondition();
|
|
2835
|
+
if (conditionMet) {
|
|
2836
|
+
break;
|
|
2837
|
+
}
|
|
2838
|
+
// Check if we still have time for another attempt
|
|
2839
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2840
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2841
|
+
}
|
|
2842
|
+
else {
|
|
2843
|
+
break;
|
|
2592
2844
|
}
|
|
2593
2845
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2846
|
+
// If we got here and condition is met, break out of main loop
|
|
2847
|
+
if (conditionMet) {
|
|
2848
|
+
break;
|
|
2596
2849
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2850
|
+
// If condition not met but no exception, we've timed out
|
|
2851
|
+
break;
|
|
2852
|
+
}
|
|
2853
|
+
catch (e) {
|
|
2854
|
+
lastError = e;
|
|
2855
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2856
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2857
|
+
// Check if we have enough time left to retry
|
|
2858
|
+
if (timeLeft > 100) {
|
|
2859
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2860
|
+
}
|
|
2861
|
+
else {
|
|
2602
2862
|
break;
|
|
2603
|
-
|
|
2604
|
-
}
|
|
2605
|
-
const actualWaitTime = Date.now() - startTime;
|
|
2606
|
-
state.info = {
|
|
2607
|
-
success: conditionMet,
|
|
2608
|
-
conditionMet,
|
|
2609
|
-
actualWaitTime,
|
|
2610
|
-
currentValue,
|
|
2611
|
-
message: conditionMet
|
|
2612
|
-
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2613
|
-
: `Condition '${condition}' not met within ${timeout}s timeout`, // Use original seconds value
|
|
2614
|
-
};
|
|
2615
|
-
state.log += state.info.message + "\n";
|
|
2616
|
-
return state.info;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2617
2865
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2866
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2867
|
+
state.info = {
|
|
2868
|
+
success: conditionMet,
|
|
2869
|
+
conditionMet,
|
|
2870
|
+
actualWaitTime,
|
|
2871
|
+
currentValue,
|
|
2872
|
+
lastError: lastError?.message || null,
|
|
2873
|
+
message: conditionMet
|
|
2874
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2875
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2876
|
+
};
|
|
2877
|
+
if (lastError) {
|
|
2878
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2630
2879
|
}
|
|
2631
|
-
|
|
2880
|
+
try {
|
|
2632
2881
|
await _commandFinally(state, this);
|
|
2633
2882
|
}
|
|
2883
|
+
catch (finallyError) {
|
|
2884
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2885
|
+
}
|
|
2886
|
+
return state.info;
|
|
2634
2887
|
}
|
|
2635
2888
|
async extractEmailData(emailAddress, options, world) {
|
|
2636
2889
|
if (!emailAddress) {
|
|
@@ -3101,7 +3354,15 @@ class StableBrowser {
|
|
|
3101
3354
|
text = text.replace(/\\"/g, '"');
|
|
3102
3355
|
}
|
|
3103
3356
|
const timeout = this._getFindElementTimeout(options);
|
|
3104
|
-
|
|
3357
|
+
//if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
3358
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
3359
|
+
if (!this.fastMode) {
|
|
3360
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3361
|
+
}
|
|
3362
|
+
else {
|
|
3363
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3105
3366
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
3106
3367
|
if (newValue !== text) {
|
|
3107
3368
|
this.logger.info(text + "=" + newValue);
|
|
@@ -3228,6 +3489,8 @@ class StableBrowser {
|
|
|
3228
3489
|
operation: "verify_text_with_relation",
|
|
3229
3490
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3230
3491
|
};
|
|
3492
|
+
const cmdStartTime = Date.now();
|
|
3493
|
+
let cmdEndTime = null;
|
|
3231
3494
|
const timeout = this._getFindElementTimeout(options);
|
|
3232
3495
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3233
3496
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3263,6 +3526,17 @@ class StableBrowser {
|
|
|
3263
3526
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3264
3527
|
continue;
|
|
3265
3528
|
}
|
|
3529
|
+
else {
|
|
3530
|
+
cmdEndTime = Date.now();
|
|
3531
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3532
|
+
if (foundAncore) {
|
|
3533
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3534
|
+
}
|
|
3535
|
+
else {
|
|
3536
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3266
3540
|
try {
|
|
3267
3541
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3268
3542
|
foundAncore = true;
|
|
@@ -3401,7 +3675,7 @@ class StableBrowser {
|
|
|
3401
3675
|
Object.assign(e, { info: info });
|
|
3402
3676
|
error = e;
|
|
3403
3677
|
// throw e;
|
|
3404
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3678
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3405
3679
|
}
|
|
3406
3680
|
finally {
|
|
3407
3681
|
const endTime = Date.now();
|
|
@@ -3750,6 +4024,22 @@ class StableBrowser {
|
|
|
3750
4024
|
}
|
|
3751
4025
|
}
|
|
3752
4026
|
async waitForPageLoad(options = {}, world = null) {
|
|
4027
|
+
// try {
|
|
4028
|
+
// let currentPagePath = null;
|
|
4029
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4030
|
+
// if (this.latestPagePath) {
|
|
4031
|
+
// // get the currect page path and compare with the latest page path
|
|
4032
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4033
|
+
// // if the page path is the same, do not wait for page load
|
|
4034
|
+
// console.log("No page change: " + currentPagePath);
|
|
4035
|
+
// return;
|
|
4036
|
+
// }
|
|
4037
|
+
// }
|
|
4038
|
+
// this.latestPagePath = currentPagePath;
|
|
4039
|
+
// } catch (e) {
|
|
4040
|
+
// console.debug("Error getting current page path: ", e);
|
|
4041
|
+
// }
|
|
4042
|
+
//console.log("Waiting for page load");
|
|
3753
4043
|
let timeout = this._getLoadTimeout(options);
|
|
3754
4044
|
const promiseArray = [];
|
|
3755
4045
|
// let waitForNetworkIdle = true;
|
|
@@ -3784,7 +4074,10 @@ class StableBrowser {
|
|
|
3784
4074
|
}
|
|
3785
4075
|
}
|
|
3786
4076
|
finally {
|
|
3787
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4077
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4078
|
+
if (options && !options.noSleep) {
|
|
4079
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4080
|
+
}
|
|
3788
4081
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3789
4082
|
const endTime = Date.now();
|
|
3790
4083
|
_reportToWorld(world, {
|
|
@@ -3838,7 +4131,7 @@ class StableBrowser {
|
|
|
3838
4131
|
}
|
|
3839
4132
|
operation = options.operation;
|
|
3840
4133
|
// validate operation is one of the supported operations
|
|
3841
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4134
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3842
4135
|
throw new Error("operation is not supported");
|
|
3843
4136
|
}
|
|
3844
4137
|
const state = {
|
|
@@ -3907,6 +4200,17 @@ class StableBrowser {
|
|
|
3907
4200
|
state.element = results[0];
|
|
3908
4201
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3909
4202
|
break;
|
|
4203
|
+
case "hover":
|
|
4204
|
+
if (!options.css) {
|
|
4205
|
+
throw new Error("css is not defined");
|
|
4206
|
+
}
|
|
4207
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4208
|
+
if (result1.length === 0) {
|
|
4209
|
+
throw new Error(`Element not found in cell area`);
|
|
4210
|
+
}
|
|
4211
|
+
state.element = result1[0];
|
|
4212
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4213
|
+
break;
|
|
3910
4214
|
default:
|
|
3911
4215
|
throw new Error("operation is not supported");
|
|
3912
4216
|
}
|
|
@@ -4022,6 +4326,7 @@ class StableBrowser {
|
|
|
4022
4326
|
if (world && world.attach) {
|
|
4023
4327
|
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4024
4328
|
}
|
|
4329
|
+
this.context.loadedRoutes = null;
|
|
4025
4330
|
this.beforeScenarioCalled = true;
|
|
4026
4331
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4027
4332
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -4051,8 +4356,10 @@ class StableBrowser {
|
|
|
4051
4356
|
}
|
|
4052
4357
|
async afterScenario(world, scenario) { }
|
|
4053
4358
|
async beforeStep(world, step) {
|
|
4359
|
+
this.stepTags = [];
|
|
4054
4360
|
if (!this.beforeScenarioCalled) {
|
|
4055
4361
|
this.beforeScenario(world, step);
|
|
4362
|
+
this.context.loadedRoutes = null;
|
|
4056
4363
|
}
|
|
4057
4364
|
if (this.stepIndex === undefined) {
|
|
4058
4365
|
this.stepIndex = 0;
|
|
@@ -4062,7 +4369,12 @@ class StableBrowser {
|
|
|
4062
4369
|
}
|
|
4063
4370
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4064
4371
|
this.stepName = step.pickleStep.text;
|
|
4065
|
-
|
|
4372
|
+
let printableStepName = this.stepName;
|
|
4373
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4374
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4375
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4376
|
+
});
|
|
4377
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4066
4378
|
}
|
|
4067
4379
|
else if (step && step.text) {
|
|
4068
4380
|
this.stepName = step.text;
|
|
@@ -4077,7 +4389,10 @@ class StableBrowser {
|
|
|
4077
4389
|
}
|
|
4078
4390
|
if (this.initSnapshotTaken === false) {
|
|
4079
4391
|
this.initSnapshotTaken = true;
|
|
4080
|
-
if (world &&
|
|
4392
|
+
if (world &&
|
|
4393
|
+
world.attach &&
|
|
4394
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4395
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4081
4396
|
const snapshot = await this.getAriaSnapshot();
|
|
4082
4397
|
if (snapshot) {
|
|
4083
4398
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -4085,7 +4400,12 @@ class StableBrowser {
|
|
|
4085
4400
|
}
|
|
4086
4401
|
}
|
|
4087
4402
|
this.context.routeResults = null;
|
|
4088
|
-
|
|
4403
|
+
this.context.loadedRoutes = null;
|
|
4404
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4405
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4406
|
+
}
|
|
4407
|
+
setStepTags(tags) {
|
|
4408
|
+
this.stepTags = tags;
|
|
4089
4409
|
}
|
|
4090
4410
|
async getAriaSnapshot() {
|
|
4091
4411
|
try {
|
|
@@ -4105,12 +4425,18 @@ class StableBrowser {
|
|
|
4105
4425
|
try {
|
|
4106
4426
|
// Ensure frame is attached and has body
|
|
4107
4427
|
const body = frame.locator("body");
|
|
4108
|
-
await body.waitFor({ timeout:
|
|
4428
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4109
4429
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4430
|
+
if (!snapshot) {
|
|
4431
|
+
continue;
|
|
4432
|
+
}
|
|
4110
4433
|
content.push(`- frame: ${i}`);
|
|
4111
4434
|
content.push(snapshot);
|
|
4112
4435
|
}
|
|
4113
|
-
catch (innerErr) {
|
|
4436
|
+
catch (innerErr) {
|
|
4437
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4438
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4439
|
+
}
|
|
4114
4440
|
}
|
|
4115
4441
|
return content.join("\n");
|
|
4116
4442
|
}
|
|
@@ -4182,7 +4508,11 @@ class StableBrowser {
|
|
|
4182
4508
|
if (this.context) {
|
|
4183
4509
|
this.context.examplesRow = null;
|
|
4184
4510
|
}
|
|
4185
|
-
if (world &&
|
|
4511
|
+
if (world &&
|
|
4512
|
+
world.attach &&
|
|
4513
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4514
|
+
!this.fastMode &&
|
|
4515
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4186
4516
|
const snapshot = await this.getAriaSnapshot();
|
|
4187
4517
|
if (snapshot) {
|
|
4188
4518
|
const obj = {};
|
|
@@ -4190,6 +4520,11 @@ class StableBrowser {
|
|
|
4190
4520
|
}
|
|
4191
4521
|
}
|
|
4192
4522
|
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4523
|
+
if (this.context.routeResults) {
|
|
4524
|
+
if (world && world.attach) {
|
|
4525
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4193
4528
|
if (!process.env.TEMP_RUN) {
|
|
4194
4529
|
const state = {
|
|
4195
4530
|
world,
|
|
@@ -4213,6 +4548,13 @@ class StableBrowser {
|
|
|
4213
4548
|
await _commandFinally(state, this);
|
|
4214
4549
|
}
|
|
4215
4550
|
}
|
|
4551
|
+
networkAfterStep(this.stepName, this.context);
|
|
4552
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4553
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4554
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4555
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4216
4558
|
}
|
|
4217
4559
|
}
|
|
4218
4560
|
function createTimedPromise(promise, label) {
|