automation_model 1.0.753-dev → 1.0.753-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/README.md +1 -0
- package/lib/api.js +11 -7
- 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 +61 -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 +66 -16
- package/lib/route.js +539 -125
- 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 +11 -4
- package/lib/stable_browser.js +509 -87
- 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 +5 -2
- package/lib/utils.js +52 -9
- package/lib/utils.js.map +1 -1
- package/package.json +20 -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,19 +21,20 @@ import { getTestData } from "./auto_page.js";
|
|
|
19
21
|
import { locate_element } from "./locate_element.js";
|
|
20
22
|
import { randomUUID } from "crypto";
|
|
21
23
|
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
22
|
-
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
24
|
+
import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
23
25
|
import { LocatorLog } from "./locator_log.js";
|
|
24
26
|
import axios from "axios";
|
|
25
27
|
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
28
|
import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
27
29
|
import { loadBrunoParams } from "./bruno.js";
|
|
28
30
|
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
29
32
|
export const Types = {
|
|
30
33
|
CLICK: "click_element",
|
|
31
34
|
WAIT_ELEMENT: "wait_element",
|
|
32
35
|
NAVIGATE: "navigate",
|
|
33
36
|
GO_BACK: "go_back",
|
|
34
|
-
GO_FORWARD: "
|
|
37
|
+
GO_FORWARD: "go_forward",
|
|
35
38
|
FILL: "fill_element",
|
|
36
39
|
EXECUTE: "execute_page_method", //
|
|
37
40
|
OPEN: "open_environment", //
|
|
@@ -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,15 +66,15 @@ 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",
|
|
77
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
74
78
|
};
|
|
75
79
|
export const apps = {};
|
|
76
80
|
const formatElementName = (elementName) => {
|
|
@@ -83,6 +87,7 @@ class StableBrowser {
|
|
|
83
87
|
context;
|
|
84
88
|
world;
|
|
85
89
|
fastMode;
|
|
90
|
+
stepTags;
|
|
86
91
|
project_path = null;
|
|
87
92
|
webLogFile = null;
|
|
88
93
|
networkLogger = null;
|
|
@@ -91,13 +96,15 @@ class StableBrowser {
|
|
|
91
96
|
tags = null;
|
|
92
97
|
isRecording = false;
|
|
93
98
|
initSnapshotTaken = false;
|
|
94
|
-
|
|
99
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
100
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
95
101
|
this.browser = browser;
|
|
96
102
|
this.page = page;
|
|
97
103
|
this.logger = logger;
|
|
98
104
|
this.context = context;
|
|
99
105
|
this.world = world;
|
|
100
106
|
this.fastMode = fastMode;
|
|
107
|
+
this.stepTags = stepTags;
|
|
101
108
|
if (!this.logger) {
|
|
102
109
|
this.logger = console;
|
|
103
110
|
}
|
|
@@ -130,6 +137,7 @@ class StableBrowser {
|
|
|
130
137
|
this.fastMode = true;
|
|
131
138
|
}
|
|
132
139
|
if (process.env.FAST_MODE === "true") {
|
|
140
|
+
// console.log("Fast mode enabled from environment variable");
|
|
133
141
|
this.fastMode = true;
|
|
134
142
|
}
|
|
135
143
|
if (process.env.FAST_MODE === "false") {
|
|
@@ -172,6 +180,7 @@ class StableBrowser {
|
|
|
172
180
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
173
181
|
registerDownloadEvent(this.page, this.world, context);
|
|
174
182
|
page.on("close", async () => {
|
|
183
|
+
// return if browser context is already closed
|
|
175
184
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
176
185
|
this.context.pages.pop();
|
|
177
186
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -181,7 +190,12 @@ class StableBrowser {
|
|
|
181
190
|
console.log("Switched to page " + title);
|
|
182
191
|
}
|
|
183
192
|
catch (error) {
|
|
184
|
-
|
|
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
|
+
}
|
|
185
199
|
}
|
|
186
200
|
}
|
|
187
201
|
});
|
|
@@ -190,7 +204,12 @@ class StableBrowser {
|
|
|
190
204
|
console.log("Switch page: " + (await page.title()));
|
|
191
205
|
}
|
|
192
206
|
catch (e) {
|
|
193
|
-
|
|
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
|
+
}
|
|
194
213
|
}
|
|
195
214
|
context.pageLoading.status = false;
|
|
196
215
|
}.bind(this));
|
|
@@ -218,7 +237,7 @@ class StableBrowser {
|
|
|
218
237
|
if (newContextCreated) {
|
|
219
238
|
this.registerEventListeners(this.context);
|
|
220
239
|
await this.goto(this.context.environment.baseUrl);
|
|
221
|
-
if (!this.fastMode) {
|
|
240
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
222
241
|
await this.waitForPageLoad();
|
|
223
242
|
}
|
|
224
243
|
}
|
|
@@ -502,12 +521,6 @@ class StableBrowser {
|
|
|
502
521
|
if (!el.setAttribute) {
|
|
503
522
|
el = el.parentElement;
|
|
504
523
|
}
|
|
505
|
-
// remove any attributes start with data-blinq-id
|
|
506
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
507
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
508
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
509
|
-
// }
|
|
510
|
-
// }
|
|
511
524
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
512
525
|
return true;
|
|
513
526
|
}, [tag1, randomToken]))) {
|
|
@@ -529,14 +542,13 @@ class StableBrowser {
|
|
|
529
542
|
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
530
543
|
}
|
|
531
544
|
let locatorSearch = selectorHierarchy[index];
|
|
532
|
-
let originalLocatorSearch = "";
|
|
533
545
|
try {
|
|
534
|
-
|
|
535
|
-
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
546
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
536
547
|
}
|
|
537
548
|
catch (e) {
|
|
538
549
|
console.error(e);
|
|
539
550
|
}
|
|
551
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
540
552
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
541
553
|
let locator = null;
|
|
542
554
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
@@ -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,16 @@ 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);
|
|
1125
1296
|
try {
|
|
1297
|
+
check_performance("click_preCommand", this.context, true);
|
|
1126
1298
|
await _preCommand(state, this);
|
|
1299
|
+
check_performance("click_preCommand", this.context, false);
|
|
1127
1300
|
await performAction("click", state.element, options, this, state, _params);
|
|
1128
|
-
if (!this.fastMode) {
|
|
1129
|
-
|
|
1301
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1302
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1303
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1304
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1130
1305
|
}
|
|
1131
1306
|
return state.info;
|
|
1132
1307
|
}
|
|
@@ -1134,7 +1309,13 @@ class StableBrowser {
|
|
|
1134
1309
|
await _commandError(state, e, this);
|
|
1135
1310
|
}
|
|
1136
1311
|
finally {
|
|
1312
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1137
1313
|
await _commandFinally(state, this);
|
|
1314
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1315
|
+
check_performance("click_all ***", this.context, false);
|
|
1316
|
+
if (this.context.profile) {
|
|
1317
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1318
|
+
}
|
|
1138
1319
|
}
|
|
1139
1320
|
}
|
|
1140
1321
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1226,7 +1407,7 @@ class StableBrowser {
|
|
|
1226
1407
|
}
|
|
1227
1408
|
}
|
|
1228
1409
|
}
|
|
1229
|
-
await this.waitForPageLoad();
|
|
1410
|
+
//await this.waitForPageLoad();
|
|
1230
1411
|
return state.info;
|
|
1231
1412
|
}
|
|
1232
1413
|
catch (e) {
|
|
@@ -1252,7 +1433,7 @@ class StableBrowser {
|
|
|
1252
1433
|
await _preCommand(state, this);
|
|
1253
1434
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1254
1435
|
await _screenshot(state, this);
|
|
1255
|
-
await this.waitForPageLoad();
|
|
1436
|
+
//await this.waitForPageLoad();
|
|
1256
1437
|
return state.info;
|
|
1257
1438
|
}
|
|
1258
1439
|
catch (e) {
|
|
@@ -1288,7 +1469,7 @@ class StableBrowser {
|
|
|
1288
1469
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1289
1470
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1290
1471
|
}
|
|
1291
|
-
await this.waitForPageLoad();
|
|
1472
|
+
//await this.waitForPageLoad();
|
|
1292
1473
|
return state.info;
|
|
1293
1474
|
}
|
|
1294
1475
|
catch (e) {
|
|
@@ -1474,6 +1655,14 @@ class StableBrowser {
|
|
|
1474
1655
|
}
|
|
1475
1656
|
try {
|
|
1476
1657
|
await _preCommand(state, this);
|
|
1658
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1659
|
+
// tag the element
|
|
1660
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1661
|
+
// use attribute and not id
|
|
1662
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1663
|
+
el.setAttribute(attrName, "");
|
|
1664
|
+
return `[${attrName}]`;
|
|
1665
|
+
}, randomToken);
|
|
1477
1666
|
state.info.value = _value;
|
|
1478
1667
|
if (!options.press) {
|
|
1479
1668
|
try {
|
|
@@ -1499,6 +1688,25 @@ class StableBrowser {
|
|
|
1499
1688
|
}
|
|
1500
1689
|
}
|
|
1501
1690
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1691
|
+
// check if the element exist after the click (no wait)
|
|
1692
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1693
|
+
if (count === 0) {
|
|
1694
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1695
|
+
const scope = state.element._frame ?? element.page();
|
|
1696
|
+
let prefixSelector = "";
|
|
1697
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1698
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1699
|
+
if (frameSelectorIndex !== -1) {
|
|
1700
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1701
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1702
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1703
|
+
}
|
|
1704
|
+
// if (element?._frame?._selector) {
|
|
1705
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1706
|
+
// }
|
|
1707
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1708
|
+
state.element = scope.locator(newSelector).first();
|
|
1709
|
+
}
|
|
1502
1710
|
const valueSegment = state.value.split("&&");
|
|
1503
1711
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1504
1712
|
if (i > 0) {
|
|
@@ -1570,8 +1778,8 @@ class StableBrowser {
|
|
|
1570
1778
|
if (enter) {
|
|
1571
1779
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1572
1780
|
await this.page.keyboard.press("Enter");
|
|
1781
|
+
await this.waitForPageLoad();
|
|
1573
1782
|
}
|
|
1574
|
-
await this.waitForPageLoad();
|
|
1575
1783
|
return state.info;
|
|
1576
1784
|
}
|
|
1577
1785
|
catch (e) {
|
|
@@ -2485,47 +2693,54 @@ class StableBrowser {
|
|
|
2485
2693
|
}
|
|
2486
2694
|
state.info.value = val;
|
|
2487
2695
|
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
|
-
}
|
|
2696
|
+
state.info.value = val;
|
|
2697
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2698
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2699
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2700
|
+
let matchPassed = false;
|
|
2701
|
+
if (isRegex) {
|
|
2702
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2703
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2704
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2705
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2706
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2707
|
+
const regex = new RegExp(patternBody, flags);
|
|
2708
|
+
state.info.regex = true;
|
|
2709
|
+
matchPassed = regex.test(val);
|
|
2506
2710
|
}
|
|
2507
2711
|
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
|
-
}
|
|
2712
|
+
// Fallback: treat as literal
|
|
2713
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2714
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2715
|
+
matchPassed = regex.test(val);
|
|
2520
2716
|
}
|
|
2521
2717
|
}
|
|
2718
|
+
else if (isContains) {
|
|
2719
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2720
|
+
matchPassed = val.includes(containsValue);
|
|
2721
|
+
}
|
|
2722
|
+
else if (isExact) {
|
|
2723
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2724
|
+
matchPassed = val === exactValue;
|
|
2725
|
+
}
|
|
2726
|
+
else if (property === "innerText") {
|
|
2727
|
+
// Default innerText logic
|
|
2728
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2729
|
+
const valLines = val.split("\n");
|
|
2730
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2731
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2732
|
+
}
|
|
2522
2733
|
else {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2734
|
+
// Fallback exact or loose match
|
|
2735
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2736
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2737
|
+
matchPassed = regex.test(val);
|
|
2738
|
+
}
|
|
2739
|
+
if (!matchPassed) {
|
|
2740
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2741
|
+
state.info.failCause.assertionFailed = true;
|
|
2742
|
+
state.info.failCause.lastError = errorMessage;
|
|
2743
|
+
throw new Error(errorMessage);
|
|
2529
2744
|
}
|
|
2530
2745
|
return state.info;
|
|
2531
2746
|
}
|
|
@@ -2536,6 +2751,133 @@ class StableBrowser {
|
|
|
2536
2751
|
await _commandFinally(state, this);
|
|
2537
2752
|
}
|
|
2538
2753
|
}
|
|
2754
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2755
|
+
// Convert timeout from seconds to milliseconds
|
|
2756
|
+
const timeoutMs = timeout * 1000;
|
|
2757
|
+
const state = {
|
|
2758
|
+
selectors,
|
|
2759
|
+
_params,
|
|
2760
|
+
condition,
|
|
2761
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2762
|
+
options,
|
|
2763
|
+
world,
|
|
2764
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2765
|
+
highlight: true,
|
|
2766
|
+
screenshot: true,
|
|
2767
|
+
text: `Conditional wait for element`,
|
|
2768
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2769
|
+
operation: "conditionalWait",
|
|
2770
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2771
|
+
allowDisabled: true,
|
|
2772
|
+
info: {},
|
|
2773
|
+
};
|
|
2774
|
+
state.options ??= { timeout: timeoutMs };
|
|
2775
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2776
|
+
const startTime = Date.now();
|
|
2777
|
+
let conditionMet = false;
|
|
2778
|
+
let currentValue = null;
|
|
2779
|
+
let lastError = null;
|
|
2780
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2781
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2782
|
+
const elapsedTime = Date.now() - startTime;
|
|
2783
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2784
|
+
try {
|
|
2785
|
+
// Try to execute _preCommand (element location)
|
|
2786
|
+
await _preCommand(state, this);
|
|
2787
|
+
// If _preCommand succeeds, start condition checking
|
|
2788
|
+
const checkCondition = async () => {
|
|
2789
|
+
try {
|
|
2790
|
+
switch (condition.toLowerCase()) {
|
|
2791
|
+
case "checked":
|
|
2792
|
+
currentValue = await state.element.isChecked();
|
|
2793
|
+
return currentValue === true;
|
|
2794
|
+
case "unchecked":
|
|
2795
|
+
currentValue = await state.element.isChecked();
|
|
2796
|
+
return currentValue === false;
|
|
2797
|
+
case "visible":
|
|
2798
|
+
currentValue = await state.element.isVisible();
|
|
2799
|
+
return currentValue === true;
|
|
2800
|
+
case "hidden":
|
|
2801
|
+
currentValue = await state.element.isVisible();
|
|
2802
|
+
return currentValue === false;
|
|
2803
|
+
case "enabled":
|
|
2804
|
+
currentValue = await state.element.isDisabled();
|
|
2805
|
+
return currentValue === false;
|
|
2806
|
+
case "disabled":
|
|
2807
|
+
currentValue = await state.element.isDisabled();
|
|
2808
|
+
return currentValue === true;
|
|
2809
|
+
case "editable":
|
|
2810
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2811
|
+
currentValue = await state.element.isContentEditable();
|
|
2812
|
+
return currentValue === true;
|
|
2813
|
+
default:
|
|
2814
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2815
|
+
state.info.success = false;
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
catch (error) {
|
|
2820
|
+
// Don't throw here, just return false to continue retrying
|
|
2821
|
+
return false;
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
// Inner loop for condition checking (once element is located)
|
|
2825
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2826
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2827
|
+
conditionMet = await checkCondition();
|
|
2828
|
+
if (conditionMet) {
|
|
2829
|
+
break;
|
|
2830
|
+
}
|
|
2831
|
+
// Check if we still have time for another attempt
|
|
2832
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2833
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2834
|
+
}
|
|
2835
|
+
else {
|
|
2836
|
+
break;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
// If we got here and condition is met, break out of main loop
|
|
2840
|
+
if (conditionMet) {
|
|
2841
|
+
break;
|
|
2842
|
+
}
|
|
2843
|
+
// If condition not met but no exception, we've timed out
|
|
2844
|
+
break;
|
|
2845
|
+
}
|
|
2846
|
+
catch (e) {
|
|
2847
|
+
lastError = e;
|
|
2848
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2849
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2850
|
+
// Check if we have enough time left to retry
|
|
2851
|
+
if (timeLeft > 100) {
|
|
2852
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2853
|
+
}
|
|
2854
|
+
else {
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2860
|
+
state.info = {
|
|
2861
|
+
success: conditionMet,
|
|
2862
|
+
conditionMet,
|
|
2863
|
+
actualWaitTime,
|
|
2864
|
+
currentValue,
|
|
2865
|
+
lastError: lastError?.message || null,
|
|
2866
|
+
message: conditionMet
|
|
2867
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2868
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2869
|
+
};
|
|
2870
|
+
if (lastError) {
|
|
2871
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2872
|
+
}
|
|
2873
|
+
try {
|
|
2874
|
+
await _commandFinally(state, this);
|
|
2875
|
+
}
|
|
2876
|
+
catch (finallyError) {
|
|
2877
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2878
|
+
}
|
|
2879
|
+
return state.info;
|
|
2880
|
+
}
|
|
2539
2881
|
async extractEmailData(emailAddress, options, world) {
|
|
2540
2882
|
if (!emailAddress) {
|
|
2541
2883
|
throw new Error("email address is null");
|
|
@@ -3132,6 +3474,8 @@ class StableBrowser {
|
|
|
3132
3474
|
operation: "verify_text_with_relation",
|
|
3133
3475
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3134
3476
|
};
|
|
3477
|
+
const cmdStartTime = Date.now();
|
|
3478
|
+
let cmdEndTime = null;
|
|
3135
3479
|
const timeout = this._getFindElementTimeout(options);
|
|
3136
3480
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3137
3481
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3167,6 +3511,17 @@ class StableBrowser {
|
|
|
3167
3511
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3168
3512
|
continue;
|
|
3169
3513
|
}
|
|
3514
|
+
else {
|
|
3515
|
+
cmdEndTime = Date.now();
|
|
3516
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3517
|
+
if (foundAncore) {
|
|
3518
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3519
|
+
}
|
|
3520
|
+
else {
|
|
3521
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3170
3525
|
try {
|
|
3171
3526
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3172
3527
|
foundAncore = true;
|
|
@@ -3305,7 +3660,7 @@ class StableBrowser {
|
|
|
3305
3660
|
Object.assign(e, { info: info });
|
|
3306
3661
|
error = e;
|
|
3307
3662
|
// throw e;
|
|
3308
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3663
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3309
3664
|
}
|
|
3310
3665
|
finally {
|
|
3311
3666
|
const endTime = Date.now();
|
|
@@ -3654,6 +4009,22 @@ class StableBrowser {
|
|
|
3654
4009
|
}
|
|
3655
4010
|
}
|
|
3656
4011
|
async waitForPageLoad(options = {}, world = null) {
|
|
4012
|
+
// try {
|
|
4013
|
+
// let currentPagePath = null;
|
|
4014
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4015
|
+
// if (this.latestPagePath) {
|
|
4016
|
+
// // get the currect page path and compare with the latest page path
|
|
4017
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4018
|
+
// // if the page path is the same, do not wait for page load
|
|
4019
|
+
// console.log("No page change: " + currentPagePath);
|
|
4020
|
+
// return;
|
|
4021
|
+
// }
|
|
4022
|
+
// }
|
|
4023
|
+
// this.latestPagePath = currentPagePath;
|
|
4024
|
+
// } catch (e) {
|
|
4025
|
+
// console.debug("Error getting current page path: ", e);
|
|
4026
|
+
// }
|
|
4027
|
+
//console.log("Waiting for page load");
|
|
3657
4028
|
let timeout = this._getLoadTimeout(options);
|
|
3658
4029
|
const promiseArray = [];
|
|
3659
4030
|
// let waitForNetworkIdle = true;
|
|
@@ -3686,10 +4057,12 @@ class StableBrowser {
|
|
|
3686
4057
|
else if (e.label === "domcontentloaded") {
|
|
3687
4058
|
console.log("waited for the domcontent loaded timeout");
|
|
3688
4059
|
}
|
|
3689
|
-
console.log(".");
|
|
3690
4060
|
}
|
|
3691
4061
|
finally {
|
|
3692
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4062
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4063
|
+
if (options && !options.noSleep) {
|
|
4064
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4065
|
+
}
|
|
3693
4066
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3694
4067
|
const endTime = Date.now();
|
|
3695
4068
|
_reportToWorld(world, {
|
|
@@ -3730,7 +4103,6 @@ class StableBrowser {
|
|
|
3730
4103
|
await this.page.close();
|
|
3731
4104
|
}
|
|
3732
4105
|
catch (e) {
|
|
3733
|
-
console.log(".");
|
|
3734
4106
|
await _commandError(state, e, this);
|
|
3735
4107
|
}
|
|
3736
4108
|
finally {
|
|
@@ -3744,7 +4116,7 @@ class StableBrowser {
|
|
|
3744
4116
|
}
|
|
3745
4117
|
operation = options.operation;
|
|
3746
4118
|
// validate operation is one of the supported operations
|
|
3747
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4119
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3748
4120
|
throw new Error("operation is not supported");
|
|
3749
4121
|
}
|
|
3750
4122
|
const state = {
|
|
@@ -3813,6 +4185,17 @@ class StableBrowser {
|
|
|
3813
4185
|
state.element = results[0];
|
|
3814
4186
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3815
4187
|
break;
|
|
4188
|
+
case "hover":
|
|
4189
|
+
if (!options.css) {
|
|
4190
|
+
throw new Error("css is not defined");
|
|
4191
|
+
}
|
|
4192
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4193
|
+
if (result1.length === 0) {
|
|
4194
|
+
throw new Error(`Element not found in cell area`);
|
|
4195
|
+
}
|
|
4196
|
+
state.element = result1[0];
|
|
4197
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4198
|
+
break;
|
|
3816
4199
|
default:
|
|
3817
4200
|
throw new Error("operation is not supported");
|
|
3818
4201
|
}
|
|
@@ -3845,7 +4228,6 @@ class StableBrowser {
|
|
|
3845
4228
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3846
4229
|
}
|
|
3847
4230
|
catch (e) {
|
|
3848
|
-
console.log(".");
|
|
3849
4231
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3850
4232
|
}
|
|
3851
4233
|
finally {
|
|
@@ -3883,7 +4265,6 @@ class StableBrowser {
|
|
|
3883
4265
|
await this.page.reload();
|
|
3884
4266
|
}
|
|
3885
4267
|
catch (e) {
|
|
3886
|
-
console.log(".");
|
|
3887
4268
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3888
4269
|
}
|
|
3889
4270
|
finally {
|
|
@@ -3927,6 +4308,10 @@ class StableBrowser {
|
|
|
3927
4308
|
}
|
|
3928
4309
|
}
|
|
3929
4310
|
async beforeScenario(world, scenario) {
|
|
4311
|
+
if (world && world.attach) {
|
|
4312
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4313
|
+
}
|
|
4314
|
+
this.context.loadedRoutes = null;
|
|
3930
4315
|
this.beforeScenarioCalled = true;
|
|
3931
4316
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3932
4317
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3956,8 +4341,10 @@ class StableBrowser {
|
|
|
3956
4341
|
}
|
|
3957
4342
|
async afterScenario(world, scenario) { }
|
|
3958
4343
|
async beforeStep(world, step) {
|
|
4344
|
+
this.stepTags = [];
|
|
3959
4345
|
if (!this.beforeScenarioCalled) {
|
|
3960
4346
|
this.beforeScenario(world, step);
|
|
4347
|
+
this.context.loadedRoutes = null;
|
|
3961
4348
|
}
|
|
3962
4349
|
if (this.stepIndex === undefined) {
|
|
3963
4350
|
this.stepIndex = 0;
|
|
@@ -3967,7 +4354,12 @@ class StableBrowser {
|
|
|
3967
4354
|
}
|
|
3968
4355
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3969
4356
|
this.stepName = step.pickleStep.text;
|
|
3970
|
-
|
|
4357
|
+
let printableStepName = this.stepName;
|
|
4358
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4359
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4360
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4361
|
+
});
|
|
4362
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
3971
4363
|
}
|
|
3972
4364
|
else if (step && step.text) {
|
|
3973
4365
|
this.stepName = step.text;
|
|
@@ -3982,7 +4374,10 @@ class StableBrowser {
|
|
|
3982
4374
|
}
|
|
3983
4375
|
if (this.initSnapshotTaken === false) {
|
|
3984
4376
|
this.initSnapshotTaken = true;
|
|
3985
|
-
if (world &&
|
|
4377
|
+
if (world &&
|
|
4378
|
+
world.attach &&
|
|
4379
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4380
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
3986
4381
|
const snapshot = await this.getAriaSnapshot();
|
|
3987
4382
|
if (snapshot) {
|
|
3988
4383
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -3990,7 +4385,12 @@ class StableBrowser {
|
|
|
3990
4385
|
}
|
|
3991
4386
|
}
|
|
3992
4387
|
this.context.routeResults = null;
|
|
3993
|
-
|
|
4388
|
+
this.context.loadedRoutes = null;
|
|
4389
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4390
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4391
|
+
}
|
|
4392
|
+
setStepTags(tags) {
|
|
4393
|
+
this.stepTags = tags;
|
|
3994
4394
|
}
|
|
3995
4395
|
async getAriaSnapshot() {
|
|
3996
4396
|
try {
|
|
@@ -4010,12 +4410,18 @@ class StableBrowser {
|
|
|
4010
4410
|
try {
|
|
4011
4411
|
// Ensure frame is attached and has body
|
|
4012
4412
|
const body = frame.locator("body");
|
|
4013
|
-
await body.waitFor({ timeout:
|
|
4413
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4014
4414
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4415
|
+
if (!snapshot) {
|
|
4416
|
+
continue;
|
|
4417
|
+
}
|
|
4015
4418
|
content.push(`- frame: ${i}`);
|
|
4016
4419
|
content.push(snapshot);
|
|
4017
4420
|
}
|
|
4018
|
-
catch (innerErr) {
|
|
4421
|
+
catch (innerErr) {
|
|
4422
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4423
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4424
|
+
}
|
|
4019
4425
|
}
|
|
4020
4426
|
return content.join("\n");
|
|
4021
4427
|
}
|
|
@@ -4087,7 +4493,11 @@ class StableBrowser {
|
|
|
4087
4493
|
if (this.context) {
|
|
4088
4494
|
this.context.examplesRow = null;
|
|
4089
4495
|
}
|
|
4090
|
-
if (world &&
|
|
4496
|
+
if (world &&
|
|
4497
|
+
world.attach &&
|
|
4498
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4499
|
+
!this.fastMode &&
|
|
4500
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4091
4501
|
const snapshot = await this.getAriaSnapshot();
|
|
4092
4502
|
if (snapshot) {
|
|
4093
4503
|
const obj = {};
|
|
@@ -4095,6 +4505,11 @@ class StableBrowser {
|
|
|
4095
4505
|
}
|
|
4096
4506
|
}
|
|
4097
4507
|
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4508
|
+
if (this.context.routeResults) {
|
|
4509
|
+
if (world && world.attach) {
|
|
4510
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4098
4513
|
if (!process.env.TEMP_RUN) {
|
|
4099
4514
|
const state = {
|
|
4100
4515
|
world,
|
|
@@ -4118,6 +4533,13 @@ class StableBrowser {
|
|
|
4118
4533
|
await _commandFinally(state, this);
|
|
4119
4534
|
}
|
|
4120
4535
|
}
|
|
4536
|
+
networkAfterStep(this.stepName, this.context);
|
|
4537
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4538
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4539
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4540
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4121
4543
|
}
|
|
4122
4544
|
}
|
|
4123
4545
|
function createTimedPromise(promise, label) {
|