automation_model 1.0.756-dev → 1.0.756-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 +563 -146
- 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 +479 -153
- 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 +53 -10
- 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,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]))) {
|
|
@@ -530,14 +542,13 @@ class StableBrowser {
|
|
|
530
542
|
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
531
543
|
}
|
|
532
544
|
let locatorSearch = selectorHierarchy[index];
|
|
533
|
-
let originalLocatorSearch = "";
|
|
534
545
|
try {
|
|
535
|
-
|
|
536
|
-
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
546
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
537
547
|
}
|
|
538
548
|
catch (e) {
|
|
539
549
|
console.error(e);
|
|
540
550
|
}
|
|
551
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
541
552
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
542
553
|
let locator = null;
|
|
543
554
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
@@ -679,40 +690,186 @@ class StableBrowser {
|
|
|
679
690
|
}
|
|
680
691
|
return { rerun: false };
|
|
681
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
|
+
}
|
|
682
757
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
683
758
|
if (!timeout) {
|
|
684
759
|
timeout = 30000;
|
|
685
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
|
+
}
|
|
686
768
|
for (let i = 0; i < 3; i++) {
|
|
687
769
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
688
770
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
689
771
|
let selector = selectors.locators[j];
|
|
690
772
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
691
773
|
}
|
|
692
|
-
|
|
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
|
+
}
|
|
693
817
|
if (!element.rerun) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
}
|
|
701
859
|
const scope = element._frame ?? element.page();
|
|
702
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
703
860
|
let prefixSelector = "";
|
|
704
861
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
705
862
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
706
863
|
if (frameSelectorIndex !== -1) {
|
|
707
864
|
// remove everything after the >> internal:control=enter-frame
|
|
708
865
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
709
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
866
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
710
867
|
}
|
|
711
868
|
// if (element?._frame?._selector) {
|
|
712
869
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
713
870
|
// }
|
|
714
871
|
const newSelector = prefixSelector + newElementSelector;
|
|
715
|
-
return scope.locator(newSelector);
|
|
872
|
+
return scope.locator(newSelector).first();
|
|
716
873
|
}
|
|
717
874
|
}
|
|
718
875
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -733,7 +890,7 @@ class StableBrowser {
|
|
|
733
890
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
734
891
|
let frameLocator = frame.selectors[i];
|
|
735
892
|
if (frameLocator.css) {
|
|
736
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
893
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
737
894
|
if (frameLocator.index) {
|
|
738
895
|
testframescope = framescope.nth(frameLocator.index);
|
|
739
896
|
}
|
|
@@ -745,7 +902,7 @@ class StableBrowser {
|
|
|
745
902
|
break;
|
|
746
903
|
}
|
|
747
904
|
catch (error) {
|
|
748
|
-
console.error("frame not found " + frameLocator.css);
|
|
905
|
+
// console.error("frame not found " + frameLocator.css);
|
|
749
906
|
}
|
|
750
907
|
}
|
|
751
908
|
}
|
|
@@ -811,6 +968,14 @@ class StableBrowser {
|
|
|
811
968
|
});
|
|
812
969
|
}
|
|
813
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
|
+
}
|
|
814
979
|
if (!info) {
|
|
815
980
|
info = {};
|
|
816
981
|
info.failCause = {};
|
|
@@ -823,7 +988,6 @@ class StableBrowser {
|
|
|
823
988
|
let locatorsCount = 0;
|
|
824
989
|
let lazy_scroll = false;
|
|
825
990
|
//let arrayMode = Array.isArray(selectors);
|
|
826
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
827
991
|
let selectorsLocators = null;
|
|
828
992
|
selectorsLocators = selectors.locators;
|
|
829
993
|
// group selectors by priority
|
|
@@ -851,6 +1015,7 @@ class StableBrowser {
|
|
|
851
1015
|
let highPriorityOnly = true;
|
|
852
1016
|
let visibleOnly = true;
|
|
853
1017
|
while (true) {
|
|
1018
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
854
1019
|
locatorsCount = 0;
|
|
855
1020
|
let result = [];
|
|
856
1021
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -966,9 +1131,13 @@ class StableBrowser {
|
|
|
966
1131
|
}
|
|
967
1132
|
}
|
|
968
1133
|
if (foundLocators.length === 1) {
|
|
1134
|
+
let box = null;
|
|
1135
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1136
|
+
box = await foundLocators[0].boundingBox();
|
|
1137
|
+
}
|
|
969
1138
|
result.foundElements.push({
|
|
970
1139
|
locator: foundLocators[0],
|
|
971
|
-
box:
|
|
1140
|
+
box: box,
|
|
972
1141
|
unique: true,
|
|
973
1142
|
});
|
|
974
1143
|
result.locatorIndex = i;
|
|
@@ -1123,11 +1292,16 @@ class StableBrowser {
|
|
|
1123
1292
|
operation: "click",
|
|
1124
1293
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1125
1294
|
};
|
|
1295
|
+
check_performance("click_all ***", this.context, true);
|
|
1126
1296
|
try {
|
|
1297
|
+
check_performance("click_preCommand", this.context, true);
|
|
1127
1298
|
await _preCommand(state, this);
|
|
1299
|
+
check_performance("click_preCommand", this.context, false);
|
|
1128
1300
|
await performAction("click", state.element, options, this, state, _params);
|
|
1129
|
-
if (!this.fastMode) {
|
|
1130
|
-
|
|
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);
|
|
1131
1305
|
}
|
|
1132
1306
|
return state.info;
|
|
1133
1307
|
}
|
|
@@ -1135,7 +1309,13 @@ class StableBrowser {
|
|
|
1135
1309
|
await _commandError(state, e, this);
|
|
1136
1310
|
}
|
|
1137
1311
|
finally {
|
|
1312
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1138
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
|
+
}
|
|
1139
1319
|
}
|
|
1140
1320
|
}
|
|
1141
1321
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1227,7 +1407,7 @@ class StableBrowser {
|
|
|
1227
1407
|
}
|
|
1228
1408
|
}
|
|
1229
1409
|
}
|
|
1230
|
-
await this.waitForPageLoad();
|
|
1410
|
+
//await this.waitForPageLoad();
|
|
1231
1411
|
return state.info;
|
|
1232
1412
|
}
|
|
1233
1413
|
catch (e) {
|
|
@@ -1253,7 +1433,7 @@ class StableBrowser {
|
|
|
1253
1433
|
await _preCommand(state, this);
|
|
1254
1434
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1255
1435
|
await _screenshot(state, this);
|
|
1256
|
-
await this.waitForPageLoad();
|
|
1436
|
+
//await this.waitForPageLoad();
|
|
1257
1437
|
return state.info;
|
|
1258
1438
|
}
|
|
1259
1439
|
catch (e) {
|
|
@@ -1289,7 +1469,7 @@ class StableBrowser {
|
|
|
1289
1469
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1290
1470
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1291
1471
|
}
|
|
1292
|
-
await this.waitForPageLoad();
|
|
1472
|
+
//await this.waitForPageLoad();
|
|
1293
1473
|
return state.info;
|
|
1294
1474
|
}
|
|
1295
1475
|
catch (e) {
|
|
@@ -1475,6 +1655,14 @@ class StableBrowser {
|
|
|
1475
1655
|
}
|
|
1476
1656
|
try {
|
|
1477
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);
|
|
1478
1666
|
state.info.value = _value;
|
|
1479
1667
|
if (!options.press) {
|
|
1480
1668
|
try {
|
|
@@ -1500,6 +1688,25 @@ class StableBrowser {
|
|
|
1500
1688
|
}
|
|
1501
1689
|
}
|
|
1502
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
|
+
}
|
|
1503
1710
|
const valueSegment = state.value.split("&&");
|
|
1504
1711
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1505
1712
|
if (i > 0) {
|
|
@@ -1571,8 +1778,8 @@ class StableBrowser {
|
|
|
1571
1778
|
if (enter) {
|
|
1572
1779
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1573
1780
|
await this.page.keyboard.press("Enter");
|
|
1781
|
+
await this.waitForPageLoad();
|
|
1574
1782
|
}
|
|
1575
|
-
await this.waitForPageLoad();
|
|
1576
1783
|
return state.info;
|
|
1577
1784
|
}
|
|
1578
1785
|
catch (e) {
|
|
@@ -2486,47 +2693,54 @@ class StableBrowser {
|
|
|
2486
2693
|
}
|
|
2487
2694
|
state.info.value = val;
|
|
2488
2695
|
let regex;
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
const
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
state.info.failCause.assertionFailed = true;
|
|
2504
|
-
state.info.failCause.lastError = errorMessage;
|
|
2505
|
-
throw new Error(errorMessage);
|
|
2506
|
-
}
|
|
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);
|
|
2507
2710
|
}
|
|
2508
2711
|
else {
|
|
2509
|
-
//
|
|
2510
|
-
const
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2513
|
-
// Check if all expected lines are present in the actual lines
|
|
2514
|
-
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2515
|
-
if (!isPart) {
|
|
2516
|
-
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2517
|
-
state.info.failCause.assertionFailed = true;
|
|
2518
|
-
state.info.failCause.lastError = errorMessage;
|
|
2519
|
-
throw new Error(errorMessage);
|
|
2520
|
-
}
|
|
2712
|
+
// Fallback: treat as literal
|
|
2713
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2714
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2715
|
+
matchPassed = regex.test(val);
|
|
2521
2716
|
}
|
|
2522
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
|
+
}
|
|
2523
2733
|
else {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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);
|
|
2530
2744
|
}
|
|
2531
2745
|
return state.info;
|
|
2532
2746
|
}
|
|
@@ -2555,82 +2769,114 @@ class StableBrowser {
|
|
|
2555
2769
|
operation: "conditionalWait",
|
|
2556
2770
|
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2557
2771
|
allowDisabled: true,
|
|
2558
|
-
info: {}
|
|
2772
|
+
info: {},
|
|
2559
2773
|
};
|
|
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
|
-
|
|
2592
|
-
|
|
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;
|
|
2593
2837
|
}
|
|
2594
2838
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2839
|
+
// If we got here and condition is met, break out of main loop
|
|
2840
|
+
if (conditionMet) {
|
|
2841
|
+
break;
|
|
2597
2842
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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 {
|
|
2602
2855
|
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;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2617
2858
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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`;
|
|
2630
2872
|
}
|
|
2631
|
-
|
|
2873
|
+
try {
|
|
2632
2874
|
await _commandFinally(state, this);
|
|
2633
2875
|
}
|
|
2876
|
+
catch (finallyError) {
|
|
2877
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2878
|
+
}
|
|
2879
|
+
return state.info;
|
|
2634
2880
|
}
|
|
2635
2881
|
async extractEmailData(emailAddress, options, world) {
|
|
2636
2882
|
if (!emailAddress) {
|
|
@@ -3228,6 +3474,8 @@ class StableBrowser {
|
|
|
3228
3474
|
operation: "verify_text_with_relation",
|
|
3229
3475
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3230
3476
|
};
|
|
3477
|
+
const cmdStartTime = Date.now();
|
|
3478
|
+
let cmdEndTime = null;
|
|
3231
3479
|
const timeout = this._getFindElementTimeout(options);
|
|
3232
3480
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3233
3481
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3263,6 +3511,17 @@ class StableBrowser {
|
|
|
3263
3511
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3264
3512
|
continue;
|
|
3265
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
|
+
}
|
|
3266
3525
|
try {
|
|
3267
3526
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3268
3527
|
foundAncore = true;
|
|
@@ -3401,7 +3660,7 @@ class StableBrowser {
|
|
|
3401
3660
|
Object.assign(e, { info: info });
|
|
3402
3661
|
error = e;
|
|
3403
3662
|
// throw e;
|
|
3404
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3663
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3405
3664
|
}
|
|
3406
3665
|
finally {
|
|
3407
3666
|
const endTime = Date.now();
|
|
@@ -3750,6 +4009,22 @@ class StableBrowser {
|
|
|
3750
4009
|
}
|
|
3751
4010
|
}
|
|
3752
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");
|
|
3753
4028
|
let timeout = this._getLoadTimeout(options);
|
|
3754
4029
|
const promiseArray = [];
|
|
3755
4030
|
// let waitForNetworkIdle = true;
|
|
@@ -3782,10 +4057,12 @@ class StableBrowser {
|
|
|
3782
4057
|
else if (e.label === "domcontentloaded") {
|
|
3783
4058
|
console.log("waited for the domcontent loaded timeout");
|
|
3784
4059
|
}
|
|
3785
|
-
console.log(".");
|
|
3786
4060
|
}
|
|
3787
4061
|
finally {
|
|
3788
|
-
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
|
+
}
|
|
3789
4066
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3790
4067
|
const endTime = Date.now();
|
|
3791
4068
|
_reportToWorld(world, {
|
|
@@ -3826,7 +4103,6 @@ class StableBrowser {
|
|
|
3826
4103
|
await this.page.close();
|
|
3827
4104
|
}
|
|
3828
4105
|
catch (e) {
|
|
3829
|
-
console.log(".");
|
|
3830
4106
|
await _commandError(state, e, this);
|
|
3831
4107
|
}
|
|
3832
4108
|
finally {
|
|
@@ -3840,7 +4116,7 @@ class StableBrowser {
|
|
|
3840
4116
|
}
|
|
3841
4117
|
operation = options.operation;
|
|
3842
4118
|
// validate operation is one of the supported operations
|
|
3843
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4119
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3844
4120
|
throw new Error("operation is not supported");
|
|
3845
4121
|
}
|
|
3846
4122
|
const state = {
|
|
@@ -3909,6 +4185,17 @@ class StableBrowser {
|
|
|
3909
4185
|
state.element = results[0];
|
|
3910
4186
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3911
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;
|
|
3912
4199
|
default:
|
|
3913
4200
|
throw new Error("operation is not supported");
|
|
3914
4201
|
}
|
|
@@ -3941,7 +4228,6 @@ class StableBrowser {
|
|
|
3941
4228
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3942
4229
|
}
|
|
3943
4230
|
catch (e) {
|
|
3944
|
-
console.log(".");
|
|
3945
4231
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3946
4232
|
}
|
|
3947
4233
|
finally {
|
|
@@ -3979,7 +4265,6 @@ class StableBrowser {
|
|
|
3979
4265
|
await this.page.reload();
|
|
3980
4266
|
}
|
|
3981
4267
|
catch (e) {
|
|
3982
|
-
console.log(".");
|
|
3983
4268
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3984
4269
|
}
|
|
3985
4270
|
finally {
|
|
@@ -4023,6 +4308,10 @@ class StableBrowser {
|
|
|
4023
4308
|
}
|
|
4024
4309
|
}
|
|
4025
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;
|
|
4026
4315
|
this.beforeScenarioCalled = true;
|
|
4027
4316
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4028
4317
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -4052,8 +4341,10 @@ class StableBrowser {
|
|
|
4052
4341
|
}
|
|
4053
4342
|
async afterScenario(world, scenario) { }
|
|
4054
4343
|
async beforeStep(world, step) {
|
|
4344
|
+
this.stepTags = [];
|
|
4055
4345
|
if (!this.beforeScenarioCalled) {
|
|
4056
4346
|
this.beforeScenario(world, step);
|
|
4347
|
+
this.context.loadedRoutes = null;
|
|
4057
4348
|
}
|
|
4058
4349
|
if (this.stepIndex === undefined) {
|
|
4059
4350
|
this.stepIndex = 0;
|
|
@@ -4063,7 +4354,12 @@ class StableBrowser {
|
|
|
4063
4354
|
}
|
|
4064
4355
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4065
4356
|
this.stepName = step.pickleStep.text;
|
|
4066
|
-
|
|
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);
|
|
4067
4363
|
}
|
|
4068
4364
|
else if (step && step.text) {
|
|
4069
4365
|
this.stepName = step.text;
|
|
@@ -4078,7 +4374,10 @@ class StableBrowser {
|
|
|
4078
4374
|
}
|
|
4079
4375
|
if (this.initSnapshotTaken === false) {
|
|
4080
4376
|
this.initSnapshotTaken = true;
|
|
4081
|
-
if (world &&
|
|
4377
|
+
if (world &&
|
|
4378
|
+
world.attach &&
|
|
4379
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4380
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4082
4381
|
const snapshot = await this.getAriaSnapshot();
|
|
4083
4382
|
if (snapshot) {
|
|
4084
4383
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -4086,7 +4385,12 @@ class StableBrowser {
|
|
|
4086
4385
|
}
|
|
4087
4386
|
}
|
|
4088
4387
|
this.context.routeResults = null;
|
|
4089
|
-
|
|
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;
|
|
4090
4394
|
}
|
|
4091
4395
|
async getAriaSnapshot() {
|
|
4092
4396
|
try {
|
|
@@ -4106,12 +4410,18 @@ class StableBrowser {
|
|
|
4106
4410
|
try {
|
|
4107
4411
|
// Ensure frame is attached and has body
|
|
4108
4412
|
const body = frame.locator("body");
|
|
4109
|
-
await body.waitFor({ timeout:
|
|
4413
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4110
4414
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4415
|
+
if (!snapshot) {
|
|
4416
|
+
continue;
|
|
4417
|
+
}
|
|
4111
4418
|
content.push(`- frame: ${i}`);
|
|
4112
4419
|
content.push(snapshot);
|
|
4113
4420
|
}
|
|
4114
|
-
catch (innerErr) {
|
|
4421
|
+
catch (innerErr) {
|
|
4422
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4423
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4424
|
+
}
|
|
4115
4425
|
}
|
|
4116
4426
|
return content.join("\n");
|
|
4117
4427
|
}
|
|
@@ -4183,7 +4493,11 @@ class StableBrowser {
|
|
|
4183
4493
|
if (this.context) {
|
|
4184
4494
|
this.context.examplesRow = null;
|
|
4185
4495
|
}
|
|
4186
|
-
if (world &&
|
|
4496
|
+
if (world &&
|
|
4497
|
+
world.attach &&
|
|
4498
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4499
|
+
!this.fastMode &&
|
|
4500
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4187
4501
|
const snapshot = await this.getAriaSnapshot();
|
|
4188
4502
|
if (snapshot) {
|
|
4189
4503
|
const obj = {};
|
|
@@ -4191,6 +4505,11 @@ class StableBrowser {
|
|
|
4191
4505
|
}
|
|
4192
4506
|
}
|
|
4193
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
|
+
}
|
|
4194
4513
|
if (!process.env.TEMP_RUN) {
|
|
4195
4514
|
const state = {
|
|
4196
4515
|
world,
|
|
@@ -4214,6 +4533,13 @@ class StableBrowser {
|
|
|
4214
4533
|
await _commandFinally(state, this);
|
|
4215
4534
|
}
|
|
4216
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
|
+
}
|
|
4217
4543
|
}
|
|
4218
4544
|
}
|
|
4219
4545
|
function createTimedPromise(promise, label) {
|