automation_model 1.0.762-dev → 1.0.762-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.d.ts +3 -1
- package/lib/auto_page.js +67 -19
- 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 +492 -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 +18 -11
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,14 @@ 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
|
+
if (locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
975
|
+
locator.css = locator.css + " >> visible=true";
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
813
979
|
if (!info) {
|
|
814
980
|
info = {};
|
|
815
981
|
info.failCause = {};
|
|
@@ -822,7 +988,6 @@ class StableBrowser {
|
|
|
822
988
|
let locatorsCount = 0;
|
|
823
989
|
let lazy_scroll = false;
|
|
824
990
|
//let arrayMode = Array.isArray(selectors);
|
|
825
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
826
991
|
let selectorsLocators = null;
|
|
827
992
|
selectorsLocators = selectors.locators;
|
|
828
993
|
// group selectors by priority
|
|
@@ -850,6 +1015,7 @@ class StableBrowser {
|
|
|
850
1015
|
let highPriorityOnly = true;
|
|
851
1016
|
let visibleOnly = true;
|
|
852
1017
|
while (true) {
|
|
1018
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
853
1019
|
locatorsCount = 0;
|
|
854
1020
|
let result = [];
|
|
855
1021
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -965,9 +1131,13 @@ class StableBrowser {
|
|
|
965
1131
|
}
|
|
966
1132
|
}
|
|
967
1133
|
if (foundLocators.length === 1) {
|
|
1134
|
+
let box = null;
|
|
1135
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1136
|
+
box = await foundLocators[0].boundingBox();
|
|
1137
|
+
}
|
|
968
1138
|
result.foundElements.push({
|
|
969
1139
|
locator: foundLocators[0],
|
|
970
|
-
box:
|
|
1140
|
+
box: box,
|
|
971
1141
|
unique: true,
|
|
972
1142
|
});
|
|
973
1143
|
result.locatorIndex = i;
|
|
@@ -1122,11 +1292,22 @@ class StableBrowser {
|
|
|
1122
1292
|
operation: "click",
|
|
1123
1293
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1124
1294
|
};
|
|
1295
|
+
check_performance("click_all ***", this.context, true);
|
|
1296
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
1297
|
+
if (stepFastMode) {
|
|
1298
|
+
state.onlyFailuresScreenshot = true;
|
|
1299
|
+
state.scroll = false;
|
|
1300
|
+
state.highlight = false;
|
|
1301
|
+
}
|
|
1125
1302
|
try {
|
|
1303
|
+
check_performance("click_preCommand", this.context, true);
|
|
1126
1304
|
await _preCommand(state, this);
|
|
1305
|
+
check_performance("click_preCommand", this.context, false);
|
|
1127
1306
|
await performAction("click", state.element, options, this, state, _params);
|
|
1128
|
-
if (!this.fastMode) {
|
|
1129
|
-
|
|
1307
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1308
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1309
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1310
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1130
1311
|
}
|
|
1131
1312
|
return state.info;
|
|
1132
1313
|
}
|
|
@@ -1134,7 +1315,13 @@ class StableBrowser {
|
|
|
1134
1315
|
await _commandError(state, e, this);
|
|
1135
1316
|
}
|
|
1136
1317
|
finally {
|
|
1318
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1137
1319
|
await _commandFinally(state, this);
|
|
1320
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1321
|
+
check_performance("click_all ***", this.context, false);
|
|
1322
|
+
if (this.context.profile) {
|
|
1323
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1324
|
+
}
|
|
1138
1325
|
}
|
|
1139
1326
|
}
|
|
1140
1327
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1226,7 +1413,7 @@ class StableBrowser {
|
|
|
1226
1413
|
}
|
|
1227
1414
|
}
|
|
1228
1415
|
}
|
|
1229
|
-
await this.waitForPageLoad();
|
|
1416
|
+
//await this.waitForPageLoad();
|
|
1230
1417
|
return state.info;
|
|
1231
1418
|
}
|
|
1232
1419
|
catch (e) {
|
|
@@ -1252,7 +1439,7 @@ class StableBrowser {
|
|
|
1252
1439
|
await _preCommand(state, this);
|
|
1253
1440
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1254
1441
|
await _screenshot(state, this);
|
|
1255
|
-
await this.waitForPageLoad();
|
|
1442
|
+
//await this.waitForPageLoad();
|
|
1256
1443
|
return state.info;
|
|
1257
1444
|
}
|
|
1258
1445
|
catch (e) {
|
|
@@ -1288,7 +1475,7 @@ class StableBrowser {
|
|
|
1288
1475
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1289
1476
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1290
1477
|
}
|
|
1291
|
-
await this.waitForPageLoad();
|
|
1478
|
+
//await this.waitForPageLoad();
|
|
1292
1479
|
return state.info;
|
|
1293
1480
|
}
|
|
1294
1481
|
catch (e) {
|
|
@@ -1474,6 +1661,14 @@ class StableBrowser {
|
|
|
1474
1661
|
}
|
|
1475
1662
|
try {
|
|
1476
1663
|
await _preCommand(state, this);
|
|
1664
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1665
|
+
// tag the element
|
|
1666
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1667
|
+
// use attribute and not id
|
|
1668
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1669
|
+
el.setAttribute(attrName, "");
|
|
1670
|
+
return `[${attrName}]`;
|
|
1671
|
+
}, randomToken);
|
|
1477
1672
|
state.info.value = _value;
|
|
1478
1673
|
if (!options.press) {
|
|
1479
1674
|
try {
|
|
@@ -1499,6 +1694,25 @@ class StableBrowser {
|
|
|
1499
1694
|
}
|
|
1500
1695
|
}
|
|
1501
1696
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1697
|
+
// check if the element exist after the click (no wait)
|
|
1698
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1699
|
+
if (count === 0) {
|
|
1700
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1701
|
+
const scope = state.element._frame ?? element.page();
|
|
1702
|
+
let prefixSelector = "";
|
|
1703
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1704
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1705
|
+
if (frameSelectorIndex !== -1) {
|
|
1706
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1707
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1708
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1709
|
+
}
|
|
1710
|
+
// if (element?._frame?._selector) {
|
|
1711
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1712
|
+
// }
|
|
1713
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1714
|
+
state.element = scope.locator(newSelector).first();
|
|
1715
|
+
}
|
|
1502
1716
|
const valueSegment = state.value.split("&&");
|
|
1503
1717
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1504
1718
|
if (i > 0) {
|
|
@@ -1570,8 +1784,8 @@ class StableBrowser {
|
|
|
1570
1784
|
if (enter) {
|
|
1571
1785
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1572
1786
|
await this.page.keyboard.press("Enter");
|
|
1787
|
+
await this.waitForPageLoad();
|
|
1573
1788
|
}
|
|
1574
|
-
await this.waitForPageLoad();
|
|
1575
1789
|
return state.info;
|
|
1576
1790
|
}
|
|
1577
1791
|
catch (e) {
|
|
@@ -2437,7 +2651,7 @@ class StableBrowser {
|
|
|
2437
2651
|
let expectedValue;
|
|
2438
2652
|
try {
|
|
2439
2653
|
await _preCommand(state, this);
|
|
2440
|
-
expectedValue = await
|
|
2654
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2441
2655
|
state.info.expectedValue = expectedValue;
|
|
2442
2656
|
switch (property) {
|
|
2443
2657
|
case "innerText":
|
|
@@ -2485,47 +2699,54 @@ class StableBrowser {
|
|
|
2485
2699
|
}
|
|
2486
2700
|
state.info.value = val;
|
|
2487
2701
|
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
|
-
}
|
|
2702
|
+
state.info.value = val;
|
|
2703
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2704
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2705
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2706
|
+
let matchPassed = false;
|
|
2707
|
+
if (isRegex) {
|
|
2708
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2709
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2710
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2711
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2712
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2713
|
+
const regex = new RegExp(patternBody, flags);
|
|
2714
|
+
state.info.regex = true;
|
|
2715
|
+
matchPassed = regex.test(val);
|
|
2506
2716
|
}
|
|
2507
2717
|
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
|
-
}
|
|
2718
|
+
// Fallback: treat as literal
|
|
2719
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2720
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2721
|
+
matchPassed = regex.test(val);
|
|
2520
2722
|
}
|
|
2521
2723
|
}
|
|
2724
|
+
else if (isContains) {
|
|
2725
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2726
|
+
matchPassed = val.includes(containsValue);
|
|
2727
|
+
}
|
|
2728
|
+
else if (isExact) {
|
|
2729
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2730
|
+
matchPassed = val === exactValue;
|
|
2731
|
+
}
|
|
2732
|
+
else if (property === "innerText") {
|
|
2733
|
+
// Default innerText logic
|
|
2734
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2735
|
+
const valLines = val.split("\n");
|
|
2736
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2737
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2738
|
+
}
|
|
2522
2739
|
else {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2740
|
+
// Fallback exact or loose match
|
|
2741
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2742
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2743
|
+
matchPassed = regex.test(val);
|
|
2744
|
+
}
|
|
2745
|
+
if (!matchPassed) {
|
|
2746
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2747
|
+
state.info.failCause.assertionFailed = true;
|
|
2748
|
+
state.info.failCause.lastError = errorMessage;
|
|
2749
|
+
throw new Error(errorMessage);
|
|
2529
2750
|
}
|
|
2530
2751
|
return state.info;
|
|
2531
2752
|
}
|
|
@@ -2556,81 +2777,112 @@ class StableBrowser {
|
|
|
2556
2777
|
allowDisabled: true,
|
|
2557
2778
|
info: {},
|
|
2558
2779
|
};
|
|
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
|
-
|
|
2780
|
+
state.options ??= { timeout: timeoutMs };
|
|
2781
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2782
|
+
const startTime = Date.now();
|
|
2783
|
+
let conditionMet = false;
|
|
2784
|
+
let currentValue = null;
|
|
2785
|
+
let lastError = null;
|
|
2786
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2787
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2788
|
+
const elapsedTime = Date.now() - startTime;
|
|
2789
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2790
|
+
try {
|
|
2791
|
+
// Try to execute _preCommand (element location)
|
|
2792
|
+
await _preCommand(state, this);
|
|
2793
|
+
// If _preCommand succeeds, start condition checking
|
|
2794
|
+
const checkCondition = async () => {
|
|
2795
|
+
try {
|
|
2796
|
+
switch (condition.toLowerCase()) {
|
|
2797
|
+
case "checked":
|
|
2798
|
+
currentValue = await state.element.isChecked();
|
|
2799
|
+
return currentValue === true;
|
|
2800
|
+
case "unchecked":
|
|
2801
|
+
currentValue = await state.element.isChecked();
|
|
2802
|
+
return currentValue === false;
|
|
2803
|
+
case "visible":
|
|
2804
|
+
currentValue = await state.element.isVisible();
|
|
2805
|
+
return currentValue === true;
|
|
2806
|
+
case "hidden":
|
|
2807
|
+
currentValue = await state.element.isVisible();
|
|
2808
|
+
return currentValue === false;
|
|
2809
|
+
case "enabled":
|
|
2810
|
+
currentValue = await state.element.isDisabled();
|
|
2811
|
+
return currentValue === false;
|
|
2812
|
+
case "disabled":
|
|
2813
|
+
currentValue = await state.element.isDisabled();
|
|
2814
|
+
return currentValue === true;
|
|
2815
|
+
case "editable":
|
|
2816
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2817
|
+
currentValue = await state.element.isContentEditable();
|
|
2818
|
+
return currentValue === true;
|
|
2819
|
+
default:
|
|
2820
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2821
|
+
state.info.success = false;
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
catch (error) {
|
|
2826
|
+
// Don't throw here, just return false to continue retrying
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2830
|
+
// Inner loop for condition checking (once element is located)
|
|
2831
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2832
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2833
|
+
conditionMet = await checkCondition();
|
|
2834
|
+
if (conditionMet) {
|
|
2835
|
+
break;
|
|
2836
|
+
}
|
|
2837
|
+
// Check if we still have time for another attempt
|
|
2838
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2839
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2840
|
+
}
|
|
2841
|
+
else {
|
|
2842
|
+
break;
|
|
2592
2843
|
}
|
|
2593
2844
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2845
|
+
// If we got here and condition is met, break out of main loop
|
|
2846
|
+
if (conditionMet) {
|
|
2847
|
+
break;
|
|
2596
2848
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2849
|
+
// If condition not met but no exception, we've timed out
|
|
2850
|
+
break;
|
|
2851
|
+
}
|
|
2852
|
+
catch (e) {
|
|
2853
|
+
lastError = e;
|
|
2854
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2855
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2856
|
+
// Check if we have enough time left to retry
|
|
2857
|
+
if (timeLeft > 100) {
|
|
2858
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2859
|
+
}
|
|
2860
|
+
else {
|
|
2602
2861
|
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;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2617
2864
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2865
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2866
|
+
state.info = {
|
|
2867
|
+
success: conditionMet,
|
|
2868
|
+
conditionMet,
|
|
2869
|
+
actualWaitTime,
|
|
2870
|
+
currentValue,
|
|
2871
|
+
lastError: lastError?.message || null,
|
|
2872
|
+
message: conditionMet
|
|
2873
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2874
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2875
|
+
};
|
|
2876
|
+
if (lastError) {
|
|
2877
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2630
2878
|
}
|
|
2631
|
-
|
|
2879
|
+
try {
|
|
2632
2880
|
await _commandFinally(state, this);
|
|
2633
2881
|
}
|
|
2882
|
+
catch (finallyError) {
|
|
2883
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2884
|
+
}
|
|
2885
|
+
return state.info;
|
|
2634
2886
|
}
|
|
2635
2887
|
async extractEmailData(emailAddress, options, world) {
|
|
2636
2888
|
if (!emailAddress) {
|
|
@@ -3101,7 +3353,15 @@ class StableBrowser {
|
|
|
3101
3353
|
text = text.replace(/\\"/g, '"');
|
|
3102
3354
|
}
|
|
3103
3355
|
const timeout = this._getFindElementTimeout(options);
|
|
3104
|
-
|
|
3356
|
+
//if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
3357
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
3358
|
+
if (!this.fastMode) {
|
|
3359
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3360
|
+
}
|
|
3361
|
+
else {
|
|
3362
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3105
3365
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
3106
3366
|
if (newValue !== text) {
|
|
3107
3367
|
this.logger.info(text + "=" + newValue);
|
|
@@ -3228,6 +3488,8 @@ class StableBrowser {
|
|
|
3228
3488
|
operation: "verify_text_with_relation",
|
|
3229
3489
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3230
3490
|
};
|
|
3491
|
+
const cmdStartTime = Date.now();
|
|
3492
|
+
let cmdEndTime = null;
|
|
3231
3493
|
const timeout = this._getFindElementTimeout(options);
|
|
3232
3494
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3233
3495
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3263,6 +3525,17 @@ class StableBrowser {
|
|
|
3263
3525
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3264
3526
|
continue;
|
|
3265
3527
|
}
|
|
3528
|
+
else {
|
|
3529
|
+
cmdEndTime = Date.now();
|
|
3530
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3531
|
+
if (foundAncore) {
|
|
3532
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3533
|
+
}
|
|
3534
|
+
else {
|
|
3535
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3266
3539
|
try {
|
|
3267
3540
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3268
3541
|
foundAncore = true;
|
|
@@ -3401,7 +3674,7 @@ class StableBrowser {
|
|
|
3401
3674
|
Object.assign(e, { info: info });
|
|
3402
3675
|
error = e;
|
|
3403
3676
|
// throw e;
|
|
3404
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3677
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3405
3678
|
}
|
|
3406
3679
|
finally {
|
|
3407
3680
|
const endTime = Date.now();
|
|
@@ -3750,6 +4023,22 @@ class StableBrowser {
|
|
|
3750
4023
|
}
|
|
3751
4024
|
}
|
|
3752
4025
|
async waitForPageLoad(options = {}, world = null) {
|
|
4026
|
+
// try {
|
|
4027
|
+
// let currentPagePath = null;
|
|
4028
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4029
|
+
// if (this.latestPagePath) {
|
|
4030
|
+
// // get the currect page path and compare with the latest page path
|
|
4031
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4032
|
+
// // if the page path is the same, do not wait for page load
|
|
4033
|
+
// console.log("No page change: " + currentPagePath);
|
|
4034
|
+
// return;
|
|
4035
|
+
// }
|
|
4036
|
+
// }
|
|
4037
|
+
// this.latestPagePath = currentPagePath;
|
|
4038
|
+
// } catch (e) {
|
|
4039
|
+
// console.debug("Error getting current page path: ", e);
|
|
4040
|
+
// }
|
|
4041
|
+
//console.log("Waiting for page load");
|
|
3753
4042
|
let timeout = this._getLoadTimeout(options);
|
|
3754
4043
|
const promiseArray = [];
|
|
3755
4044
|
// let waitForNetworkIdle = true;
|
|
@@ -3784,7 +4073,10 @@ class StableBrowser {
|
|
|
3784
4073
|
}
|
|
3785
4074
|
}
|
|
3786
4075
|
finally {
|
|
3787
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4076
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4077
|
+
if (options && !options.noSleep) {
|
|
4078
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4079
|
+
}
|
|
3788
4080
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3789
4081
|
const endTime = Date.now();
|
|
3790
4082
|
_reportToWorld(world, {
|
|
@@ -3838,7 +4130,7 @@ class StableBrowser {
|
|
|
3838
4130
|
}
|
|
3839
4131
|
operation = options.operation;
|
|
3840
4132
|
// validate operation is one of the supported operations
|
|
3841
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4133
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3842
4134
|
throw new Error("operation is not supported");
|
|
3843
4135
|
}
|
|
3844
4136
|
const state = {
|
|
@@ -3907,6 +4199,17 @@ class StableBrowser {
|
|
|
3907
4199
|
state.element = results[0];
|
|
3908
4200
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3909
4201
|
break;
|
|
4202
|
+
case "hover":
|
|
4203
|
+
if (!options.css) {
|
|
4204
|
+
throw new Error("css is not defined");
|
|
4205
|
+
}
|
|
4206
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4207
|
+
if (result1.length === 0) {
|
|
4208
|
+
throw new Error(`Element not found in cell area`);
|
|
4209
|
+
}
|
|
4210
|
+
state.element = result1[0];
|
|
4211
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4212
|
+
break;
|
|
3910
4213
|
default:
|
|
3911
4214
|
throw new Error("operation is not supported");
|
|
3912
4215
|
}
|
|
@@ -4019,6 +4322,10 @@ class StableBrowser {
|
|
|
4019
4322
|
}
|
|
4020
4323
|
}
|
|
4021
4324
|
async beforeScenario(world, scenario) {
|
|
4325
|
+
if (world && world.attach) {
|
|
4326
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4327
|
+
}
|
|
4328
|
+
this.context.loadedRoutes = null;
|
|
4022
4329
|
this.beforeScenarioCalled = true;
|
|
4023
4330
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4024
4331
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -4048,8 +4355,10 @@ class StableBrowser {
|
|
|
4048
4355
|
}
|
|
4049
4356
|
async afterScenario(world, scenario) { }
|
|
4050
4357
|
async beforeStep(world, step) {
|
|
4358
|
+
this.stepTags = [];
|
|
4051
4359
|
if (!this.beforeScenarioCalled) {
|
|
4052
4360
|
this.beforeScenario(world, step);
|
|
4361
|
+
this.context.loadedRoutes = null;
|
|
4053
4362
|
}
|
|
4054
4363
|
if (this.stepIndex === undefined) {
|
|
4055
4364
|
this.stepIndex = 0;
|
|
@@ -4059,7 +4368,12 @@ class StableBrowser {
|
|
|
4059
4368
|
}
|
|
4060
4369
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4061
4370
|
this.stepName = step.pickleStep.text;
|
|
4062
|
-
|
|
4371
|
+
let printableStepName = this.stepName;
|
|
4372
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4373
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4374
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4375
|
+
});
|
|
4376
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4063
4377
|
}
|
|
4064
4378
|
else if (step && step.text) {
|
|
4065
4379
|
this.stepName = step.text;
|
|
@@ -4074,7 +4388,10 @@ class StableBrowser {
|
|
|
4074
4388
|
}
|
|
4075
4389
|
if (this.initSnapshotTaken === false) {
|
|
4076
4390
|
this.initSnapshotTaken = true;
|
|
4077
|
-
if (world &&
|
|
4391
|
+
if (world &&
|
|
4392
|
+
world.attach &&
|
|
4393
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4394
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4078
4395
|
const snapshot = await this.getAriaSnapshot();
|
|
4079
4396
|
if (snapshot) {
|
|
4080
4397
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -4082,7 +4399,12 @@ class StableBrowser {
|
|
|
4082
4399
|
}
|
|
4083
4400
|
}
|
|
4084
4401
|
this.context.routeResults = null;
|
|
4085
|
-
|
|
4402
|
+
this.context.loadedRoutes = null;
|
|
4403
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4404
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4405
|
+
}
|
|
4406
|
+
setStepTags(tags) {
|
|
4407
|
+
this.stepTags = tags;
|
|
4086
4408
|
}
|
|
4087
4409
|
async getAriaSnapshot() {
|
|
4088
4410
|
try {
|
|
@@ -4102,12 +4424,18 @@ class StableBrowser {
|
|
|
4102
4424
|
try {
|
|
4103
4425
|
// Ensure frame is attached and has body
|
|
4104
4426
|
const body = frame.locator("body");
|
|
4105
|
-
await body.waitFor({ timeout:
|
|
4427
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4106
4428
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4429
|
+
if (!snapshot) {
|
|
4430
|
+
continue;
|
|
4431
|
+
}
|
|
4107
4432
|
content.push(`- frame: ${i}`);
|
|
4108
4433
|
content.push(snapshot);
|
|
4109
4434
|
}
|
|
4110
|
-
catch (innerErr) {
|
|
4435
|
+
catch (innerErr) {
|
|
4436
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4437
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4438
|
+
}
|
|
4111
4439
|
}
|
|
4112
4440
|
return content.join("\n");
|
|
4113
4441
|
}
|
|
@@ -4179,7 +4507,11 @@ class StableBrowser {
|
|
|
4179
4507
|
if (this.context) {
|
|
4180
4508
|
this.context.examplesRow = null;
|
|
4181
4509
|
}
|
|
4182
|
-
if (world &&
|
|
4510
|
+
if (world &&
|
|
4511
|
+
world.attach &&
|
|
4512
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4513
|
+
!this.fastMode &&
|
|
4514
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4183
4515
|
const snapshot = await this.getAriaSnapshot();
|
|
4184
4516
|
if (snapshot) {
|
|
4185
4517
|
const obj = {};
|
|
@@ -4187,6 +4519,11 @@ class StableBrowser {
|
|
|
4187
4519
|
}
|
|
4188
4520
|
}
|
|
4189
4521
|
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4522
|
+
if (this.context.routeResults) {
|
|
4523
|
+
if (world && world.attach) {
|
|
4524
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4190
4527
|
if (!process.env.TEMP_RUN) {
|
|
4191
4528
|
const state = {
|
|
4192
4529
|
world,
|
|
@@ -4210,6 +4547,13 @@ class StableBrowser {
|
|
|
4210
4547
|
await _commandFinally(state, this);
|
|
4211
4548
|
}
|
|
4212
4549
|
}
|
|
4550
|
+
networkAfterStep(this.stepName, this.context);
|
|
4551
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4552
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4553
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4554
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4213
4557
|
}
|
|
4214
4558
|
}
|
|
4215
4559
|
function createTimedPromise(promise, label) {
|