automation_model 1.0.785-dev → 1.0.785-stage
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api.js +28 -11
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +58 -17
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +2 -2
- package/lib/browser_manager.js +102 -52
- 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.d.ts +1 -1
- package/lib/command_common.js +26 -16
- package/lib/command_common.js.map +1 -1
- package/lib/file_checker.js +7 -0
- 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 +124 -128
- package/lib/init_browser.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +2 -2
- package/lib/network.js +341 -178
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +64 -2
- package/lib/route.js +493 -192
- package/lib/route.js.map +1 -1
- package/lib/scripts/axe.mini.js +23978 -1
- package/lib/snapshot_validation.js +3 -0
- package/lib/snapshot_validation.js.map +1 -1
- package/lib/stable_browser.d.ts +13 -7
- package/lib/stable_browser.js +474 -114
- package/lib/stable_browser.js.map +1 -1
- package/lib/table_helper.js +14 -0
- package/lib/table_helper.js.map +1 -1
- package/lib/test_context.d.ts +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +6 -2
- package/lib/utils.js +121 -14
- package/lib/utils.js.map +1 -1
- package/package.json +18 -11
package/lib/stable_browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
+
import { check_performance } from "./check_performance.js";
|
|
2
3
|
import { expect } from "@playwright/test";
|
|
3
4
|
import dayjs from "dayjs";
|
|
4
5
|
import fs from "fs";
|
|
@@ -10,6 +11,7 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
11
|
import drawRectangle from "./drawRect.js";
|
|
11
12
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
13
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
14
|
+
import errorStackParser from "error-stack-parser";
|
|
13
15
|
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
14
16
|
import csv from "csv-parser";
|
|
15
17
|
import { Readable } from "node:stream";
|
|
@@ -26,6 +28,7 @@ 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,17 @@ 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
|
+
// set to true if the step issue a report
|
|
101
|
+
inStepReport = false;
|
|
102
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
96
103
|
this.browser = browser;
|
|
97
104
|
this.page = page;
|
|
98
105
|
this.logger = logger;
|
|
99
106
|
this.context = context;
|
|
100
107
|
this.world = world;
|
|
101
108
|
this.fastMode = fastMode;
|
|
109
|
+
this.stepTags = stepTags;
|
|
102
110
|
if (!this.logger) {
|
|
103
111
|
this.logger = console;
|
|
104
112
|
}
|
|
@@ -131,7 +139,7 @@ class StableBrowser {
|
|
|
131
139
|
this.fastMode = true;
|
|
132
140
|
}
|
|
133
141
|
if (process.env.FAST_MODE === "true") {
|
|
134
|
-
console.log("Fast mode enabled from environment variable");
|
|
142
|
+
// console.log("Fast mode enabled from environment variable");
|
|
135
143
|
this.fastMode = true;
|
|
136
144
|
}
|
|
137
145
|
if (process.env.FAST_MODE === "false") {
|
|
@@ -174,6 +182,7 @@ class StableBrowser {
|
|
|
174
182
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
175
183
|
registerDownloadEvent(this.page, this.world, context);
|
|
176
184
|
page.on("close", async () => {
|
|
185
|
+
// return if browser context is already closed
|
|
177
186
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
178
187
|
this.context.pages.pop();
|
|
179
188
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -183,7 +192,12 @@ class StableBrowser {
|
|
|
183
192
|
console.log("Switched to page " + title);
|
|
184
193
|
}
|
|
185
194
|
catch (error) {
|
|
186
|
-
|
|
195
|
+
if (error?.message?.includes("Target page, context or browser has been closed")) {
|
|
196
|
+
// Ignore this error
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.error("Error on page close", error);
|
|
200
|
+
}
|
|
187
201
|
}
|
|
188
202
|
}
|
|
189
203
|
});
|
|
@@ -192,7 +206,12 @@ class StableBrowser {
|
|
|
192
206
|
console.log("Switch page: " + (await page.title()));
|
|
193
207
|
}
|
|
194
208
|
catch (e) {
|
|
195
|
-
|
|
209
|
+
if (e?.message?.includes("Target page, context or browser has been closed")) {
|
|
210
|
+
// Ignore this error
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.logger.error("error on page load " + e);
|
|
214
|
+
}
|
|
196
215
|
}
|
|
197
216
|
context.pageLoading.status = false;
|
|
198
217
|
}.bind(this));
|
|
@@ -204,7 +223,7 @@ class StableBrowser {
|
|
|
204
223
|
}
|
|
205
224
|
let newContextCreated = false;
|
|
206
225
|
if (!apps[appName]) {
|
|
207
|
-
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
226
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder, null, null, this.tags);
|
|
208
227
|
newContextCreated = true;
|
|
209
228
|
apps[appName] = {
|
|
210
229
|
context: newContext,
|
|
@@ -220,7 +239,7 @@ class StableBrowser {
|
|
|
220
239
|
if (newContextCreated) {
|
|
221
240
|
this.registerEventListeners(this.context);
|
|
222
241
|
await this.goto(this.context.environment.baseUrl);
|
|
223
|
-
if (!this.fastMode) {
|
|
242
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
224
243
|
await this.waitForPageLoad();
|
|
225
244
|
}
|
|
226
245
|
}
|
|
@@ -312,7 +331,7 @@ class StableBrowser {
|
|
|
312
331
|
// async closeUnexpectedPopups() {
|
|
313
332
|
// await closeUnexpectedPopups(this.page);
|
|
314
333
|
// }
|
|
315
|
-
async goto(url, world = null) {
|
|
334
|
+
async goto(url, world = null, options = {}) {
|
|
316
335
|
if (!url) {
|
|
317
336
|
throw new Error("url is null, verify that the environment file is correct");
|
|
318
337
|
}
|
|
@@ -333,10 +352,17 @@ class StableBrowser {
|
|
|
333
352
|
screenshot: false,
|
|
334
353
|
highlight: false,
|
|
335
354
|
};
|
|
355
|
+
let timeout = 60000;
|
|
356
|
+
if (this.configuration && this.configuration.page_timeout) {
|
|
357
|
+
timeout = this.configuration.page_timeout;
|
|
358
|
+
}
|
|
359
|
+
if (options && options["timeout"]) {
|
|
360
|
+
timeout = options["timeout"];
|
|
361
|
+
}
|
|
336
362
|
try {
|
|
337
363
|
await _preCommand(state, this);
|
|
338
364
|
await this.page.goto(url, {
|
|
339
|
-
timeout:
|
|
365
|
+
timeout: timeout,
|
|
340
366
|
});
|
|
341
367
|
await _screenshot(state, this);
|
|
342
368
|
}
|
|
@@ -504,12 +530,6 @@ class StableBrowser {
|
|
|
504
530
|
if (!el.setAttribute) {
|
|
505
531
|
el = el.parentElement;
|
|
506
532
|
}
|
|
507
|
-
// remove any attributes start with data-blinq-id
|
|
508
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
509
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
510
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
511
|
-
// }
|
|
512
|
-
// }
|
|
513
533
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
514
534
|
return true;
|
|
515
535
|
}, [tag1, randomToken]))) {
|
|
@@ -679,40 +699,186 @@ class StableBrowser {
|
|
|
679
699
|
}
|
|
680
700
|
return { rerun: false };
|
|
681
701
|
}
|
|
702
|
+
getFilePath() {
|
|
703
|
+
const stackFrames = errorStackParser.parse(new Error());
|
|
704
|
+
const mjsFrames = stackFrames.filter((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
|
|
705
|
+
const stackFrame = mjsFrames[mjsFrames.length - 2];
|
|
706
|
+
const filepath = stackFrame?.fileName;
|
|
707
|
+
if (filepath) {
|
|
708
|
+
let jsonFilePath = filepath.replace(".mjs", ".json");
|
|
709
|
+
if (existsSync(jsonFilePath)) {
|
|
710
|
+
return jsonFilePath;
|
|
711
|
+
}
|
|
712
|
+
const config = this.configuration ?? {};
|
|
713
|
+
if (!config?.locatorsMetadataDir) {
|
|
714
|
+
config.locatorsMetadataDir = "features/step_definitions/locators";
|
|
715
|
+
}
|
|
716
|
+
if (config && config.locatorsMetadataDir) {
|
|
717
|
+
jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
|
|
718
|
+
}
|
|
719
|
+
if (existsSync(jsonFilePath)) {
|
|
720
|
+
return jsonFilePath;
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
getFullElementLocators(selectors, filePath) {
|
|
727
|
+
if (!filePath || !existsSync(filePath)) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
731
|
+
try {
|
|
732
|
+
const allElements = JSON.parse(content);
|
|
733
|
+
const element_key = selectors?.element_key;
|
|
734
|
+
if (element_key && allElements[element_key]) {
|
|
735
|
+
return allElements[element_key];
|
|
736
|
+
}
|
|
737
|
+
for (const elementKey in allElements) {
|
|
738
|
+
const element = allElements[elementKey];
|
|
739
|
+
let foundStrategy = null;
|
|
740
|
+
for (const key in element) {
|
|
741
|
+
if (key === "strategy") {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const locators = element[key];
|
|
745
|
+
if (!locators || !locators.length) {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
for (const locator of locators) {
|
|
749
|
+
delete locator.score;
|
|
750
|
+
}
|
|
751
|
+
if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
|
|
752
|
+
foundStrategy = key;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (foundStrategy) {
|
|
757
|
+
return element;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
console.error("Error parsing locators from file: " + filePath, error);
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
682
766
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
683
767
|
if (!timeout) {
|
|
684
768
|
timeout = 30000;
|
|
685
769
|
}
|
|
770
|
+
let element = null;
|
|
771
|
+
let allStrategyLocators = null;
|
|
772
|
+
let selectedStrategy = null;
|
|
773
|
+
if (this.tryAllStrategies) {
|
|
774
|
+
allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
|
|
775
|
+
selectedStrategy = allStrategyLocators?.strategy;
|
|
776
|
+
}
|
|
686
777
|
for (let i = 0; i < 3; i++) {
|
|
687
778
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
688
779
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
689
780
|
let selector = selectors.locators[j];
|
|
690
781
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
691
782
|
}
|
|
692
|
-
|
|
783
|
+
if (this.tryAllStrategies && selectedStrategy) {
|
|
784
|
+
const strategyLocators = allStrategyLocators[selectedStrategy];
|
|
785
|
+
let err;
|
|
786
|
+
if (strategyLocators && strategyLocators.length) {
|
|
787
|
+
try {
|
|
788
|
+
selectors.locators = strategyLocators;
|
|
789
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
790
|
+
info.selectedStrategy = selectedStrategy;
|
|
791
|
+
info.log += "element found using strategy " + selectedStrategy + "\n";
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
err = error;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (!element) {
|
|
798
|
+
for (const key in allStrategyLocators) {
|
|
799
|
+
if (key === "strategy" || key === selectedStrategy) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const strategyLocators = allStrategyLocators[key];
|
|
803
|
+
if (strategyLocators && strategyLocators.length) {
|
|
804
|
+
try {
|
|
805
|
+
info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
|
|
806
|
+
selectors.locators = strategyLocators;
|
|
807
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
808
|
+
err = null;
|
|
809
|
+
info.selectedStrategy = key;
|
|
810
|
+
info.log += "element found using strategy " + key + "\n";
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
err = error;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (err) {
|
|
820
|
+
throw err;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
825
|
+
}
|
|
693
826
|
if (!element.rerun) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
827
|
+
let newElementSelector = "";
|
|
828
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
|
|
829
|
+
const cssSelector = await element.evaluate((el) => {
|
|
830
|
+
function getCssSelector(el) {
|
|
831
|
+
if (!el || el.nodeType !== 1 || el === document.body)
|
|
832
|
+
return el.tagName.toLowerCase();
|
|
833
|
+
const parent = el.parentElement;
|
|
834
|
+
const tag = el.tagName.toLowerCase();
|
|
835
|
+
// Find the index of the element among its siblings of the same tag
|
|
836
|
+
let index = 1;
|
|
837
|
+
for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
838
|
+
if (sibling.tagName === el.tagName) {
|
|
839
|
+
index++;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Use nth-child if necessary (i.e., if there's more than one of the same tag)
|
|
843
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
844
|
+
const needsNthChild = siblings.length > 1;
|
|
845
|
+
const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
|
|
846
|
+
return getCssSelector(parent) + " > " + selector;
|
|
847
|
+
}
|
|
848
|
+
const cssSelector = getCssSelector(el);
|
|
849
|
+
return cssSelector;
|
|
850
|
+
});
|
|
851
|
+
newElementSelector = cssSelector;
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
855
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
|
|
856
|
+
const dataAttribute = "data-blinq-id";
|
|
857
|
+
await element.evaluate((el, [dataAttribute, randomToken]) => {
|
|
858
|
+
el.setAttribute(dataAttribute, randomToken);
|
|
859
|
+
}, [dataAttribute, randomToken]);
|
|
860
|
+
newElementSelector = `[${dataAttribute}="${randomToken}"]`;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
// the default case just return the located element
|
|
864
|
+
// will not work for click and type if the locator is placeholder and the placeholder change due to the click event
|
|
865
|
+
return element;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
701
868
|
const scope = element._frame ?? element.page();
|
|
702
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
703
869
|
let prefixSelector = "";
|
|
704
870
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
705
871
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
706
872
|
if (frameSelectorIndex !== -1) {
|
|
707
873
|
// remove everything after the >> internal:control=enter-frame
|
|
708
874
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
709
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
875
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
710
876
|
}
|
|
711
877
|
// if (element?._frame?._selector) {
|
|
712
878
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
713
879
|
// }
|
|
714
880
|
const newSelector = prefixSelector + newElementSelector;
|
|
715
|
-
return scope.locator(newSelector);
|
|
881
|
+
return scope.locator(newSelector).first();
|
|
716
882
|
}
|
|
717
883
|
}
|
|
718
884
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -733,7 +899,7 @@ class StableBrowser {
|
|
|
733
899
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
734
900
|
let frameLocator = frame.selectors[i];
|
|
735
901
|
if (frameLocator.css) {
|
|
736
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
902
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
737
903
|
if (frameLocator.index) {
|
|
738
904
|
testframescope = framescope.nth(frameLocator.index);
|
|
739
905
|
}
|
|
@@ -811,6 +977,15 @@ class StableBrowser {
|
|
|
811
977
|
});
|
|
812
978
|
}
|
|
813
979
|
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
980
|
+
if (selectors.locators && Array.isArray(selectors.locators)) {
|
|
981
|
+
selectors.locators.forEach((locator) => {
|
|
982
|
+
locator.index = locator.index ?? 0;
|
|
983
|
+
locator.visible = locator.visible ?? true;
|
|
984
|
+
if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
985
|
+
locator.css = locator.css + " >> visible=true";
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}
|
|
814
989
|
if (!info) {
|
|
815
990
|
info = {};
|
|
816
991
|
info.failCause = {};
|
|
@@ -823,7 +998,6 @@ class StableBrowser {
|
|
|
823
998
|
let locatorsCount = 0;
|
|
824
999
|
let lazy_scroll = false;
|
|
825
1000
|
//let arrayMode = Array.isArray(selectors);
|
|
826
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
827
1001
|
let selectorsLocators = null;
|
|
828
1002
|
selectorsLocators = selectors.locators;
|
|
829
1003
|
// group selectors by priority
|
|
@@ -851,6 +1025,7 @@ class StableBrowser {
|
|
|
851
1025
|
let highPriorityOnly = true;
|
|
852
1026
|
let visibleOnly = true;
|
|
853
1027
|
while (true) {
|
|
1028
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
854
1029
|
locatorsCount = 0;
|
|
855
1030
|
let result = [];
|
|
856
1031
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -966,9 +1141,13 @@ class StableBrowser {
|
|
|
966
1141
|
}
|
|
967
1142
|
}
|
|
968
1143
|
if (foundLocators.length === 1) {
|
|
1144
|
+
let box = null;
|
|
1145
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1146
|
+
box = await foundLocators[0].boundingBox();
|
|
1147
|
+
}
|
|
969
1148
|
result.foundElements.push({
|
|
970
1149
|
locator: foundLocators[0],
|
|
971
|
-
box:
|
|
1150
|
+
box: box,
|
|
972
1151
|
unique: true,
|
|
973
1152
|
});
|
|
974
1153
|
result.locatorIndex = i;
|
|
@@ -1123,11 +1302,22 @@ class StableBrowser {
|
|
|
1123
1302
|
operation: "click",
|
|
1124
1303
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1125
1304
|
};
|
|
1305
|
+
check_performance("click_all ***", this.context, true);
|
|
1306
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
1307
|
+
if (stepFastMode) {
|
|
1308
|
+
state.onlyFailuresScreenshot = true;
|
|
1309
|
+
state.scroll = false;
|
|
1310
|
+
state.highlight = false;
|
|
1311
|
+
}
|
|
1126
1312
|
try {
|
|
1313
|
+
check_performance("click_preCommand", this.context, true);
|
|
1127
1314
|
await _preCommand(state, this);
|
|
1315
|
+
check_performance("click_preCommand", this.context, false);
|
|
1128
1316
|
await performAction("click", state.element, options, this, state, _params);
|
|
1129
|
-
if (!this.fastMode) {
|
|
1130
|
-
|
|
1317
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1318
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1319
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1320
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1131
1321
|
}
|
|
1132
1322
|
return state.info;
|
|
1133
1323
|
}
|
|
@@ -1135,7 +1325,13 @@ class StableBrowser {
|
|
|
1135
1325
|
await _commandError(state, e, this);
|
|
1136
1326
|
}
|
|
1137
1327
|
finally {
|
|
1328
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1138
1329
|
await _commandFinally(state, this);
|
|
1330
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1331
|
+
check_performance("click_all ***", this.context, false);
|
|
1332
|
+
if (this.context.profile) {
|
|
1333
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1334
|
+
}
|
|
1139
1335
|
}
|
|
1140
1336
|
}
|
|
1141
1337
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1227,7 +1423,7 @@ class StableBrowser {
|
|
|
1227
1423
|
}
|
|
1228
1424
|
}
|
|
1229
1425
|
}
|
|
1230
|
-
await this.waitForPageLoad();
|
|
1426
|
+
//await this.waitForPageLoad();
|
|
1231
1427
|
return state.info;
|
|
1232
1428
|
}
|
|
1233
1429
|
catch (e) {
|
|
@@ -1253,7 +1449,7 @@ class StableBrowser {
|
|
|
1253
1449
|
await _preCommand(state, this);
|
|
1254
1450
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1255
1451
|
await _screenshot(state, this);
|
|
1256
|
-
await this.waitForPageLoad();
|
|
1452
|
+
//await this.waitForPageLoad();
|
|
1257
1453
|
return state.info;
|
|
1258
1454
|
}
|
|
1259
1455
|
catch (e) {
|
|
@@ -1289,7 +1485,7 @@ class StableBrowser {
|
|
|
1289
1485
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1290
1486
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1291
1487
|
}
|
|
1292
|
-
await this.waitForPageLoad();
|
|
1488
|
+
//await this.waitForPageLoad();
|
|
1293
1489
|
return state.info;
|
|
1294
1490
|
}
|
|
1295
1491
|
catch (e) {
|
|
@@ -1475,6 +1671,14 @@ class StableBrowser {
|
|
|
1475
1671
|
}
|
|
1476
1672
|
try {
|
|
1477
1673
|
await _preCommand(state, this);
|
|
1674
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1675
|
+
// tag the element
|
|
1676
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1677
|
+
// use attribute and not id
|
|
1678
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1679
|
+
el.setAttribute(attrName, "");
|
|
1680
|
+
return `[${attrName}]`;
|
|
1681
|
+
}, randomToken);
|
|
1478
1682
|
state.info.value = _value;
|
|
1479
1683
|
if (!options.press) {
|
|
1480
1684
|
try {
|
|
@@ -1500,6 +1704,25 @@ class StableBrowser {
|
|
|
1500
1704
|
}
|
|
1501
1705
|
}
|
|
1502
1706
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1707
|
+
// check if the element exist after the click (no wait)
|
|
1708
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1709
|
+
if (count === 0) {
|
|
1710
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1711
|
+
const scope = state.element._frame ?? element.page();
|
|
1712
|
+
let prefixSelector = "";
|
|
1713
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1714
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1715
|
+
if (frameSelectorIndex !== -1) {
|
|
1716
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1717
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1718
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1719
|
+
}
|
|
1720
|
+
// if (element?._frame?._selector) {
|
|
1721
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1722
|
+
// }
|
|
1723
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1724
|
+
state.element = scope.locator(newSelector).first();
|
|
1725
|
+
}
|
|
1503
1726
|
const valueSegment = state.value.split("&&");
|
|
1504
1727
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1505
1728
|
if (i > 0) {
|
|
@@ -1571,8 +1794,8 @@ class StableBrowser {
|
|
|
1571
1794
|
if (enter) {
|
|
1572
1795
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1573
1796
|
await this.page.keyboard.press("Enter");
|
|
1797
|
+
await this.waitForPageLoad();
|
|
1574
1798
|
}
|
|
1575
|
-
await this.waitForPageLoad();
|
|
1576
1799
|
return state.info;
|
|
1577
1800
|
}
|
|
1578
1801
|
catch (e) {
|
|
@@ -1831,11 +2054,12 @@ class StableBrowser {
|
|
|
1831
2054
|
throw new Error("referanceSnapshot is null");
|
|
1832
2055
|
}
|
|
1833
2056
|
let text = null;
|
|
1834
|
-
|
|
1835
|
-
|
|
2057
|
+
const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
|
|
2058
|
+
if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
|
|
2059
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
|
|
1836
2060
|
}
|
|
1837
|
-
else if (fs.existsSync(path.join(
|
|
1838
|
-
text = fs.readFileSync(path.join(
|
|
2061
|
+
else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
|
|
2062
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
|
|
1839
2063
|
}
|
|
1840
2064
|
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1841
2065
|
text = referanceSnapshot.substring(5);
|
|
@@ -1859,7 +2083,13 @@ class StableBrowser {
|
|
|
1859
2083
|
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1860
2084
|
}
|
|
1861
2085
|
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
2086
|
+
if (snapshot && snapshot.length <= 10) {
|
|
2087
|
+
console.log("Page snapshot length is suspiciously small:", snapshot);
|
|
2088
|
+
}
|
|
1862
2089
|
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
2090
|
+
if (matchResult === undefined) {
|
|
2091
|
+
console.log("snapshotValidation returned undefined");
|
|
2092
|
+
}
|
|
1863
2093
|
if (matchResult.errorLine !== -1) {
|
|
1864
2094
|
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1865
2095
|
}
|
|
@@ -2030,6 +2260,9 @@ class StableBrowser {
|
|
|
2030
2260
|
return _getTestData(world, this.context, this);
|
|
2031
2261
|
}
|
|
2032
2262
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2263
|
+
if (!options) {
|
|
2264
|
+
options = {};
|
|
2265
|
+
}
|
|
2033
2266
|
// collect url/path/title
|
|
2034
2267
|
if (info) {
|
|
2035
2268
|
if (!info.title) {
|
|
@@ -2058,7 +2291,7 @@ class StableBrowser {
|
|
|
2058
2291
|
const uuidStr = "id_" + randomUUID();
|
|
2059
2292
|
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
2060
2293
|
try {
|
|
2061
|
-
await this.takeScreenshot(screenshotPath);
|
|
2294
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
2062
2295
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
2063
2296
|
// // save the buffer to the screenshot path asynchrously
|
|
2064
2297
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -2079,7 +2312,7 @@ class StableBrowser {
|
|
|
2079
2312
|
else if (options && options.screenshot) {
|
|
2080
2313
|
result.screenshotPath = options.screenshotPath;
|
|
2081
2314
|
try {
|
|
2082
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2315
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
2083
2316
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
2084
2317
|
// // save the buffer to the screenshot path asynchrously
|
|
2085
2318
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -2097,7 +2330,7 @@ class StableBrowser {
|
|
|
2097
2330
|
}
|
|
2098
2331
|
return result;
|
|
2099
2332
|
}
|
|
2100
|
-
async takeScreenshot(screenshotPath) {
|
|
2333
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
2101
2334
|
const playContext = this.context.playContext;
|
|
2102
2335
|
// Using CDP to capture the screenshot
|
|
2103
2336
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -2122,13 +2355,7 @@ class StableBrowser {
|
|
|
2122
2355
|
const client = await playContext.newCDPSession(this.page);
|
|
2123
2356
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
2124
2357
|
format: "png",
|
|
2125
|
-
|
|
2126
|
-
// x: 0,
|
|
2127
|
-
// y: 0,
|
|
2128
|
-
// width: viewportWidth,
|
|
2129
|
-
// height: viewportHeight,
|
|
2130
|
-
// scale: 1,
|
|
2131
|
-
// },
|
|
2358
|
+
captureBeyondViewport: fullPage,
|
|
2132
2359
|
});
|
|
2133
2360
|
await client.detach();
|
|
2134
2361
|
if (!screenshotPath) {
|
|
@@ -2137,7 +2364,7 @@ class StableBrowser {
|
|
|
2137
2364
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
2138
2365
|
}
|
|
2139
2366
|
else {
|
|
2140
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2367
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
2141
2368
|
}
|
|
2142
2369
|
// if (focusedElement) {
|
|
2143
2370
|
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
@@ -2237,6 +2464,12 @@ class StableBrowser {
|
|
|
2237
2464
|
state.info.value = state.value;
|
|
2238
2465
|
this.setTestData({ [variable]: state.value }, world);
|
|
2239
2466
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2467
|
+
if (process.env.MODE === "executions") {
|
|
2468
|
+
const globalDataFile = "global_test_data.json";
|
|
2469
|
+
if (existsSync(globalDataFile)) {
|
|
2470
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2240
2473
|
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2241
2474
|
return state.info;
|
|
2242
2475
|
}
|
|
@@ -2308,6 +2541,12 @@ class StableBrowser {
|
|
|
2308
2541
|
state.info.value = state.value;
|
|
2309
2542
|
this.setTestData({ [variable]: state.value }, world);
|
|
2310
2543
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2544
|
+
if (process.env.MODE === "executions") {
|
|
2545
|
+
const globalDataFile = "global_test_data.json";
|
|
2546
|
+
if (existsSync(globalDataFile)) {
|
|
2547
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2311
2550
|
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2312
2551
|
return state.info;
|
|
2313
2552
|
}
|
|
@@ -2438,7 +2677,7 @@ class StableBrowser {
|
|
|
2438
2677
|
let expectedValue;
|
|
2439
2678
|
try {
|
|
2440
2679
|
await _preCommand(state, this);
|
|
2441
|
-
expectedValue = await
|
|
2680
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2442
2681
|
state.info.expectedValue = expectedValue;
|
|
2443
2682
|
switch (property) {
|
|
2444
2683
|
case "innerText":
|
|
@@ -2486,47 +2725,54 @@ class StableBrowser {
|
|
|
2486
2725
|
}
|
|
2487
2726
|
state.info.value = val;
|
|
2488
2727
|
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
|
-
}
|
|
2728
|
+
state.info.value = val;
|
|
2729
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2730
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2731
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2732
|
+
let matchPassed = false;
|
|
2733
|
+
if (isRegex) {
|
|
2734
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2735
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2736
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2737
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2738
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2739
|
+
const regex = new RegExp(patternBody, flags);
|
|
2740
|
+
state.info.regex = true;
|
|
2741
|
+
matchPassed = regex.test(val);
|
|
2507
2742
|
}
|
|
2508
2743
|
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
|
-
}
|
|
2744
|
+
// Fallback: treat as literal
|
|
2745
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2746
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2747
|
+
matchPassed = regex.test(val);
|
|
2521
2748
|
}
|
|
2522
2749
|
}
|
|
2750
|
+
else if (isContains) {
|
|
2751
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2752
|
+
matchPassed = val.includes(containsValue);
|
|
2753
|
+
}
|
|
2754
|
+
else if (isExact) {
|
|
2755
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2756
|
+
matchPassed = val === exactValue;
|
|
2757
|
+
}
|
|
2758
|
+
else if (property === "innerText") {
|
|
2759
|
+
// Default innerText logic
|
|
2760
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2761
|
+
const valLines = val.split("\n");
|
|
2762
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2763
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2764
|
+
}
|
|
2523
2765
|
else {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2766
|
+
// Fallback exact or loose match
|
|
2767
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2768
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2769
|
+
matchPassed = regex.test(val);
|
|
2770
|
+
}
|
|
2771
|
+
if (!matchPassed) {
|
|
2772
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2773
|
+
state.info.failCause.assertionFailed = true;
|
|
2774
|
+
state.info.failCause.lastError = errorMessage;
|
|
2775
|
+
throw new Error(errorMessage);
|
|
2530
2776
|
}
|
|
2531
2777
|
return state.info;
|
|
2532
2778
|
}
|
|
@@ -2557,6 +2803,7 @@ class StableBrowser {
|
|
|
2557
2803
|
allowDisabled: true,
|
|
2558
2804
|
info: {},
|
|
2559
2805
|
};
|
|
2806
|
+
state.options ??= { timeout: timeoutMs };
|
|
2560
2807
|
// Initialize startTime outside try block to ensure it's always accessible
|
|
2561
2808
|
const startTime = Date.now();
|
|
2562
2809
|
let conditionMet = false;
|
|
@@ -2726,6 +2973,12 @@ class StableBrowser {
|
|
|
2726
2973
|
emailUrl = url;
|
|
2727
2974
|
codeOrUrlFound = true;
|
|
2728
2975
|
}
|
|
2976
|
+
if (process.env.MODE === "executions") {
|
|
2977
|
+
const globalDataFile = "global_test_data.json";
|
|
2978
|
+
if (existsSync(globalDataFile)) {
|
|
2979
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2729
2982
|
if (codeOrUrlFound) {
|
|
2730
2983
|
return { emailUrl, emailCode };
|
|
2731
2984
|
}
|
|
@@ -3132,7 +3385,16 @@ class StableBrowser {
|
|
|
3132
3385
|
text = text.replace(/\\"/g, '"');
|
|
3133
3386
|
}
|
|
3134
3387
|
const timeout = this._getFindElementTimeout(options);
|
|
3135
|
-
|
|
3388
|
+
//if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
3389
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
3390
|
+
if (!stepFastMode) {
|
|
3391
|
+
if (!this.fastMode) {
|
|
3392
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3393
|
+
}
|
|
3394
|
+
else {
|
|
3395
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3136
3398
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
3137
3399
|
if (newValue !== text) {
|
|
3138
3400
|
this.logger.info(text + "=" + newValue);
|
|
@@ -3140,6 +3402,11 @@ class StableBrowser {
|
|
|
3140
3402
|
}
|
|
3141
3403
|
let dateAlternatives = findDateAlternatives(text);
|
|
3142
3404
|
let numberAlternatives = findNumberAlternatives(text);
|
|
3405
|
+
if (stepFastMode) {
|
|
3406
|
+
state.onlyFailuresScreenshot = true;
|
|
3407
|
+
state.scroll = false;
|
|
3408
|
+
state.highlight = false;
|
|
3409
|
+
}
|
|
3143
3410
|
try {
|
|
3144
3411
|
await _preCommand(state, this);
|
|
3145
3412
|
state.info.text = text;
|
|
@@ -3259,6 +3526,8 @@ class StableBrowser {
|
|
|
3259
3526
|
operation: "verify_text_with_relation",
|
|
3260
3527
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3261
3528
|
};
|
|
3529
|
+
const cmdStartTime = Date.now();
|
|
3530
|
+
let cmdEndTime = null;
|
|
3262
3531
|
const timeout = this._getFindElementTimeout(options);
|
|
3263
3532
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3264
3533
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3294,6 +3563,17 @@ class StableBrowser {
|
|
|
3294
3563
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3295
3564
|
continue;
|
|
3296
3565
|
}
|
|
3566
|
+
else {
|
|
3567
|
+
cmdEndTime = Date.now();
|
|
3568
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3569
|
+
if (foundAncore) {
|
|
3570
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3571
|
+
}
|
|
3572
|
+
else {
|
|
3573
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3297
3577
|
try {
|
|
3298
3578
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3299
3579
|
foundAncore = true;
|
|
@@ -3432,7 +3712,7 @@ class StableBrowser {
|
|
|
3432
3712
|
Object.assign(e, { info: info });
|
|
3433
3713
|
error = e;
|
|
3434
3714
|
// throw e;
|
|
3435
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3715
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3436
3716
|
}
|
|
3437
3717
|
finally {
|
|
3438
3718
|
const endTime = Date.now();
|
|
@@ -3781,6 +4061,22 @@ class StableBrowser {
|
|
|
3781
4061
|
}
|
|
3782
4062
|
}
|
|
3783
4063
|
async waitForPageLoad(options = {}, world = null) {
|
|
4064
|
+
// try {
|
|
4065
|
+
// let currentPagePath = null;
|
|
4066
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4067
|
+
// if (this.latestPagePath) {
|
|
4068
|
+
// // get the currect page path and compare with the latest page path
|
|
4069
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4070
|
+
// // if the page path is the same, do not wait for page load
|
|
4071
|
+
// console.log("No page change: " + currentPagePath);
|
|
4072
|
+
// return;
|
|
4073
|
+
// }
|
|
4074
|
+
// }
|
|
4075
|
+
// this.latestPagePath = currentPagePath;
|
|
4076
|
+
// } catch (e) {
|
|
4077
|
+
// console.debug("Error getting current page path: ", e);
|
|
4078
|
+
// }
|
|
4079
|
+
//console.log("Waiting for page load");
|
|
3784
4080
|
let timeout = this._getLoadTimeout(options);
|
|
3785
4081
|
const promiseArray = [];
|
|
3786
4082
|
// let waitForNetworkIdle = true;
|
|
@@ -3815,7 +4111,10 @@ class StableBrowser {
|
|
|
3815
4111
|
}
|
|
3816
4112
|
}
|
|
3817
4113
|
finally {
|
|
3818
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4114
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4115
|
+
if (options && !options.noSleep) {
|
|
4116
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4117
|
+
}
|
|
3819
4118
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3820
4119
|
const endTime = Date.now();
|
|
3821
4120
|
_reportToWorld(world, {
|
|
@@ -3869,7 +4168,7 @@ class StableBrowser {
|
|
|
3869
4168
|
}
|
|
3870
4169
|
operation = options.operation;
|
|
3871
4170
|
// validate operation is one of the supported operations
|
|
3872
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4171
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3873
4172
|
throw new Error("operation is not supported");
|
|
3874
4173
|
}
|
|
3875
4174
|
const state = {
|
|
@@ -3938,6 +4237,17 @@ class StableBrowser {
|
|
|
3938
4237
|
state.element = results[0];
|
|
3939
4238
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3940
4239
|
break;
|
|
4240
|
+
case "hover":
|
|
4241
|
+
if (!options.css) {
|
|
4242
|
+
throw new Error("css is not defined");
|
|
4243
|
+
}
|
|
4244
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4245
|
+
if (result1.length === 0) {
|
|
4246
|
+
throw new Error(`Element not found in cell area`);
|
|
4247
|
+
}
|
|
4248
|
+
state.element = result1[0];
|
|
4249
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4250
|
+
break;
|
|
3941
4251
|
default:
|
|
3942
4252
|
throw new Error("operation is not supported");
|
|
3943
4253
|
}
|
|
@@ -3951,6 +4261,12 @@ class StableBrowser {
|
|
|
3951
4261
|
}
|
|
3952
4262
|
saveTestDataAsGlobal(options, world) {
|
|
3953
4263
|
const dataFile = _getDataFile(world, this.context, this);
|
|
4264
|
+
if (process.env.MODE === "executions") {
|
|
4265
|
+
const globalDataFile = path.join(this.project_path, "global_test_data.json");
|
|
4266
|
+
fs.copyFileSync(dataFile, globalDataFile);
|
|
4267
|
+
this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
3954
4270
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
3955
4271
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
3956
4272
|
}
|
|
@@ -4053,6 +4369,7 @@ class StableBrowser {
|
|
|
4053
4369
|
if (world && world.attach) {
|
|
4054
4370
|
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4055
4371
|
}
|
|
4372
|
+
this.context.loadedRoutes = null;
|
|
4056
4373
|
this.beforeScenarioCalled = true;
|
|
4057
4374
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4058
4375
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -4079,11 +4396,32 @@ class StableBrowser {
|
|
|
4079
4396
|
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4080
4397
|
}
|
|
4081
4398
|
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4399
|
+
if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
|
|
4400
|
+
this.trace = true;
|
|
4401
|
+
const traceFolder = path.join(this.context.reportFolder, "trace");
|
|
4402
|
+
if (!fs.existsSync(traceFolder)) {
|
|
4403
|
+
fs.mkdirSync(traceFolder, { recursive: true });
|
|
4404
|
+
}
|
|
4405
|
+
this.traceFolder = traceFolder;
|
|
4406
|
+
await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
async afterScenario(world, scenario) {
|
|
4410
|
+
const id = scenario.testCaseStartedId;
|
|
4411
|
+
if (this.trace) {
|
|
4412
|
+
await this.context.playContext.tracing.stop({
|
|
4413
|
+
path: path.join(this.traceFolder, `trace-${id}.zip`),
|
|
4414
|
+
});
|
|
4415
|
+
}
|
|
4082
4416
|
}
|
|
4083
|
-
async afterScenario(world, scenario) { }
|
|
4084
4417
|
async beforeStep(world, step) {
|
|
4418
|
+
if (step?.pickleStep && this.trace) {
|
|
4419
|
+
await this.context.playContext.tracing.group(`Step: ${step.pickleStep.text}`);
|
|
4420
|
+
}
|
|
4421
|
+
this.stepTags = [];
|
|
4085
4422
|
if (!this.beforeScenarioCalled) {
|
|
4086
4423
|
this.beforeScenario(world, step);
|
|
4424
|
+
this.context.loadedRoutes = null;
|
|
4087
4425
|
}
|
|
4088
4426
|
if (this.stepIndex === undefined) {
|
|
4089
4427
|
this.stepIndex = 0;
|
|
@@ -4093,7 +4431,12 @@ class StableBrowser {
|
|
|
4093
4431
|
}
|
|
4094
4432
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4095
4433
|
this.stepName = step.pickleStep.text;
|
|
4096
|
-
|
|
4434
|
+
let printableStepName = this.stepName;
|
|
4435
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4436
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4437
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4438
|
+
});
|
|
4439
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4097
4440
|
}
|
|
4098
4441
|
else if (step && step.text) {
|
|
4099
4442
|
this.stepName = step.text;
|
|
@@ -4108,7 +4451,10 @@ class StableBrowser {
|
|
|
4108
4451
|
}
|
|
4109
4452
|
if (this.initSnapshotTaken === false) {
|
|
4110
4453
|
this.initSnapshotTaken = true;
|
|
4111
|
-
if (world &&
|
|
4454
|
+
if (world &&
|
|
4455
|
+
world.attach &&
|
|
4456
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4457
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4112
4458
|
const snapshot = await this.getAriaSnapshot();
|
|
4113
4459
|
if (snapshot) {
|
|
4114
4460
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -4118,7 +4464,11 @@ class StableBrowser {
|
|
|
4118
4464
|
this.context.routeResults = null;
|
|
4119
4465
|
this.context.loadedRoutes = null;
|
|
4120
4466
|
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4121
|
-
networkBeforeStep(this.stepName);
|
|
4467
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4468
|
+
this.inStepReport = false;
|
|
4469
|
+
}
|
|
4470
|
+
setStepTags(tags) {
|
|
4471
|
+
this.stepTags = tags;
|
|
4122
4472
|
}
|
|
4123
4473
|
async getAriaSnapshot() {
|
|
4124
4474
|
try {
|
|
@@ -4192,7 +4542,7 @@ class StableBrowser {
|
|
|
4192
4542
|
state.payload = payload;
|
|
4193
4543
|
if (commandStatus === "FAILED") {
|
|
4194
4544
|
state.throwError = true;
|
|
4195
|
-
throw new Error(
|
|
4545
|
+
throw new Error(commandText);
|
|
4196
4546
|
}
|
|
4197
4547
|
}
|
|
4198
4548
|
catch (e) {
|
|
@@ -4202,26 +4552,22 @@ class StableBrowser {
|
|
|
4202
4552
|
await _commandFinally(state, this);
|
|
4203
4553
|
}
|
|
4204
4554
|
}
|
|
4205
|
-
async afterStep(world, step) {
|
|
4555
|
+
async afterStep(world, step, result) {
|
|
4206
4556
|
this.stepName = null;
|
|
4207
|
-
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4208
|
-
if (this.context.browserObject.context) {
|
|
4209
|
-
await this.context.browserObject.context.tracing.stopChunk({
|
|
4210
|
-
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4211
|
-
});
|
|
4212
|
-
if (world && world.attach) {
|
|
4213
|
-
await world.attach(JSON.stringify({
|
|
4214
|
-
type: "trace",
|
|
4215
|
-
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4216
|
-
}), "application/json+trace");
|
|
4217
|
-
}
|
|
4218
|
-
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4219
|
-
}
|
|
4220
|
-
}
|
|
4221
4557
|
if (this.context) {
|
|
4222
4558
|
this.context.examplesRow = null;
|
|
4223
4559
|
}
|
|
4224
|
-
if (
|
|
4560
|
+
if (!this.inStepReport) {
|
|
4561
|
+
// check the step result
|
|
4562
|
+
if (result && result.status === "FAILED" && world && world.attach) {
|
|
4563
|
+
await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
if (world &&
|
|
4567
|
+
world.attach &&
|
|
4568
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4569
|
+
!this.fastMode &&
|
|
4570
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4225
4571
|
const snapshot = await this.getAriaSnapshot();
|
|
4226
4572
|
if (snapshot) {
|
|
4227
4573
|
const obj = {};
|
|
@@ -4229,6 +4575,11 @@ class StableBrowser {
|
|
|
4229
4575
|
}
|
|
4230
4576
|
}
|
|
4231
4577
|
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4578
|
+
if (this.context.routeResults) {
|
|
4579
|
+
if (world && world.attach) {
|
|
4580
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4232
4583
|
if (!process.env.TEMP_RUN) {
|
|
4233
4584
|
const state = {
|
|
4234
4585
|
world,
|
|
@@ -4252,7 +4603,16 @@ class StableBrowser {
|
|
|
4252
4603
|
await _commandFinally(state, this);
|
|
4253
4604
|
}
|
|
4254
4605
|
}
|
|
4255
|
-
networkAfterStep(this.stepName);
|
|
4606
|
+
networkAfterStep(this.stepName, this.context);
|
|
4607
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4608
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4609
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4610
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
if (this.trace) {
|
|
4614
|
+
await this.context.playContext.tracing.groupEnd();
|
|
4615
|
+
}
|
|
4256
4616
|
}
|
|
4257
4617
|
}
|
|
4258
4618
|
function createTimedPromise(promise, label) {
|