automation_model 1.0.802-dev → 1.0.802-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 +40 -12
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +103 -69
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +2 -7
- package/lib/browser_manager.js +105 -102
- 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 +2 -2
- package/lib/command_common.js +42 -24
- package/lib/command_common.js.map +1 -1
- package/lib/constants.d.ts +4 -0
- package/lib/constants.js +2 -0
- package/lib/constants.js.map +1 -0
- 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 +137 -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 +183 -120
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +64 -2
- package/lib/route.js +496 -251
- 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 +14 -8
- package/lib/stable_browser.js +464 -91
- 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 +7 -3
- package/lib/utils.js +162 -25
- package/lib/utils.js.map +1 -1
- package/package.json +21 -12
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,8 @@ 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";
|
|
32
|
+
import _ from "lodash";
|
|
29
33
|
export const Types = {
|
|
30
34
|
CLICK: "click_element",
|
|
31
35
|
WAIT_ELEMENT: "wait_element",
|
|
@@ -44,6 +48,7 @@ export const Types = {
|
|
|
44
48
|
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
45
49
|
ANALYZE_TABLE: "analyze_table",
|
|
46
50
|
SELECT: "select_combobox", //
|
|
51
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
47
52
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
48
53
|
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
49
54
|
TYPE_PRESS: "type_press",
|
|
@@ -62,12 +67,11 @@ export const Types = {
|
|
|
62
67
|
SET_INPUT: "set_input",
|
|
63
68
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
64
69
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
65
|
-
VERIFY_PROPERTY: "verify_element_property",
|
|
66
70
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
67
71
|
BRUNO: "bruno",
|
|
68
|
-
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
69
72
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
70
73
|
SET_INPUT_FILES: "set_input_files",
|
|
74
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
71
75
|
REPORT_COMMAND: "report_command",
|
|
72
76
|
STEP_COMPLETE: "step_complete",
|
|
73
77
|
SLEEP: "sleep",
|
|
@@ -84,6 +88,7 @@ class StableBrowser {
|
|
|
84
88
|
context;
|
|
85
89
|
world;
|
|
86
90
|
fastMode;
|
|
91
|
+
stepTags;
|
|
87
92
|
project_path = null;
|
|
88
93
|
webLogFile = null;
|
|
89
94
|
networkLogger = null;
|
|
@@ -92,14 +97,17 @@ class StableBrowser {
|
|
|
92
97
|
tags = null;
|
|
93
98
|
isRecording = false;
|
|
94
99
|
initSnapshotTaken = false;
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
101
|
+
// set to true if the step issue a report
|
|
102
|
+
inStepReport = false;
|
|
103
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
97
104
|
this.browser = browser;
|
|
98
105
|
this.page = page;
|
|
99
106
|
this.logger = logger;
|
|
100
107
|
this.context = context;
|
|
101
108
|
this.world = world;
|
|
102
109
|
this.fastMode = fastMode;
|
|
110
|
+
this.stepTags = stepTags;
|
|
103
111
|
if (!this.logger) {
|
|
104
112
|
this.logger = console;
|
|
105
113
|
}
|
|
@@ -132,7 +140,7 @@ class StableBrowser {
|
|
|
132
140
|
this.fastMode = true;
|
|
133
141
|
}
|
|
134
142
|
if (process.env.FAST_MODE === "true") {
|
|
135
|
-
console.log("Fast mode enabled from environment variable");
|
|
143
|
+
// console.log("Fast mode enabled from environment variable");
|
|
136
144
|
this.fastMode = true;
|
|
137
145
|
}
|
|
138
146
|
if (process.env.FAST_MODE === "false") {
|
|
@@ -175,6 +183,7 @@ class StableBrowser {
|
|
|
175
183
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
176
184
|
registerDownloadEvent(this.page, this.world, context);
|
|
177
185
|
page.on("close", async () => {
|
|
186
|
+
// return if browser context is already closed
|
|
178
187
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
179
188
|
this.context.pages.pop();
|
|
180
189
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -184,7 +193,12 @@ class StableBrowser {
|
|
|
184
193
|
console.log("Switched to page " + title);
|
|
185
194
|
}
|
|
186
195
|
catch (error) {
|
|
187
|
-
|
|
196
|
+
if (error?.message?.includes("Target page, context or browser has been closed")) {
|
|
197
|
+
// Ignore this error
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.error("Error on page close", error);
|
|
201
|
+
}
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
204
|
});
|
|
@@ -193,7 +207,12 @@ class StableBrowser {
|
|
|
193
207
|
console.log("Switch page: " + (await page.title()));
|
|
194
208
|
}
|
|
195
209
|
catch (e) {
|
|
196
|
-
|
|
210
|
+
if (e?.message?.includes("Target page, context or browser has been closed")) {
|
|
211
|
+
// Ignore this error
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
this.logger.error("error on page load " + e);
|
|
215
|
+
}
|
|
197
216
|
}
|
|
198
217
|
context.pageLoading.status = false;
|
|
199
218
|
}.bind(this));
|
|
@@ -205,7 +224,7 @@ class StableBrowser {
|
|
|
205
224
|
}
|
|
206
225
|
let newContextCreated = false;
|
|
207
226
|
if (!apps[appName]) {
|
|
208
|
-
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
227
|
+
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);
|
|
209
228
|
newContextCreated = true;
|
|
210
229
|
apps[appName] = {
|
|
211
230
|
context: newContext,
|
|
@@ -221,7 +240,7 @@ class StableBrowser {
|
|
|
221
240
|
if (newContextCreated) {
|
|
222
241
|
this.registerEventListeners(this.context);
|
|
223
242
|
await this.goto(this.context.environment.baseUrl);
|
|
224
|
-
if (!this.fastMode) {
|
|
243
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
225
244
|
await this.waitForPageLoad();
|
|
226
245
|
}
|
|
227
246
|
}
|
|
@@ -313,7 +332,7 @@ class StableBrowser {
|
|
|
313
332
|
// async closeUnexpectedPopups() {
|
|
314
333
|
// await closeUnexpectedPopups(this.page);
|
|
315
334
|
// }
|
|
316
|
-
async goto(url, world = null) {
|
|
335
|
+
async goto(url, world = null, options = {}) {
|
|
317
336
|
if (!url) {
|
|
318
337
|
throw new Error("url is null, verify that the environment file is correct");
|
|
319
338
|
}
|
|
@@ -334,16 +353,23 @@ class StableBrowser {
|
|
|
334
353
|
screenshot: false,
|
|
335
354
|
highlight: false,
|
|
336
355
|
};
|
|
356
|
+
let timeout = 60000;
|
|
357
|
+
if (this.configuration && this.configuration.page_timeout) {
|
|
358
|
+
timeout = this.configuration.page_timeout;
|
|
359
|
+
}
|
|
360
|
+
if (options && options["timeout"]) {
|
|
361
|
+
timeout = options["timeout"];
|
|
362
|
+
}
|
|
337
363
|
try {
|
|
338
364
|
await _preCommand(state, this);
|
|
339
365
|
await this.page.goto(url, {
|
|
340
|
-
timeout:
|
|
366
|
+
timeout: timeout,
|
|
341
367
|
});
|
|
342
368
|
await _screenshot(state, this);
|
|
343
369
|
}
|
|
344
370
|
catch (error) {
|
|
345
371
|
console.error("Error on goto", error);
|
|
346
|
-
_commandError(state, error, this);
|
|
372
|
+
await _commandError(state, error, this);
|
|
347
373
|
}
|
|
348
374
|
finally {
|
|
349
375
|
await _commandFinally(state, this);
|
|
@@ -372,7 +398,7 @@ class StableBrowser {
|
|
|
372
398
|
}
|
|
373
399
|
catch (error) {
|
|
374
400
|
console.error("Error on goBack", error);
|
|
375
|
-
_commandError(state, error, this);
|
|
401
|
+
await _commandError(state, error, this);
|
|
376
402
|
}
|
|
377
403
|
finally {
|
|
378
404
|
await _commandFinally(state, this);
|
|
@@ -401,7 +427,7 @@ class StableBrowser {
|
|
|
401
427
|
}
|
|
402
428
|
catch (error) {
|
|
403
429
|
console.error("Error on goForward", error);
|
|
404
|
-
_commandError(state, error, this);
|
|
430
|
+
await _commandError(state, error, this);
|
|
405
431
|
}
|
|
406
432
|
finally {
|
|
407
433
|
await _commandFinally(state, this);
|
|
@@ -505,12 +531,6 @@ class StableBrowser {
|
|
|
505
531
|
if (!el.setAttribute) {
|
|
506
532
|
el = el.parentElement;
|
|
507
533
|
}
|
|
508
|
-
// remove any attributes start with data-blinq-id
|
|
509
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
510
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
511
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
512
|
-
// }
|
|
513
|
-
// }
|
|
514
534
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
515
535
|
return true;
|
|
516
536
|
}, [tag1, randomToken]))) {
|
|
@@ -680,40 +700,186 @@ class StableBrowser {
|
|
|
680
700
|
}
|
|
681
701
|
return { rerun: false };
|
|
682
702
|
}
|
|
703
|
+
getFilePath() {
|
|
704
|
+
const stackFrames = errorStackParser.parse(new Error());
|
|
705
|
+
const mjsFrames = stackFrames.filter((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
|
|
706
|
+
const stackFrame = mjsFrames[mjsFrames.length - 2];
|
|
707
|
+
const filepath = stackFrame?.fileName;
|
|
708
|
+
if (filepath) {
|
|
709
|
+
let jsonFilePath = filepath.replace(".mjs", ".json");
|
|
710
|
+
if (existsSync(jsonFilePath)) {
|
|
711
|
+
return jsonFilePath;
|
|
712
|
+
}
|
|
713
|
+
const config = this.configuration ?? {};
|
|
714
|
+
if (!config?.locatorsMetadataDir) {
|
|
715
|
+
config.locatorsMetadataDir = "features/step_definitions/locators";
|
|
716
|
+
}
|
|
717
|
+
if (config && config.locatorsMetadataDir) {
|
|
718
|
+
jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
|
|
719
|
+
}
|
|
720
|
+
if (existsSync(jsonFilePath)) {
|
|
721
|
+
return jsonFilePath;
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
getFullElementLocators(selectors, filePath) {
|
|
728
|
+
if (!filePath || !existsSync(filePath)) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
732
|
+
try {
|
|
733
|
+
const allElements = JSON.parse(content);
|
|
734
|
+
const element_key = selectors?.element_key;
|
|
735
|
+
if (element_key && allElements[element_key]) {
|
|
736
|
+
return allElements[element_key];
|
|
737
|
+
}
|
|
738
|
+
for (const elementKey in allElements) {
|
|
739
|
+
const element = allElements[elementKey];
|
|
740
|
+
let foundStrategy = null;
|
|
741
|
+
for (const key in element) {
|
|
742
|
+
if (key === "strategy") {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const locators = element[key];
|
|
746
|
+
if (!locators || !locators.length) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
for (const locator of locators) {
|
|
750
|
+
delete locator.score;
|
|
751
|
+
}
|
|
752
|
+
if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
|
|
753
|
+
foundStrategy = key;
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (foundStrategy) {
|
|
758
|
+
return element;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
console.error("Error parsing locators from file: " + filePath, error);
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
683
767
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
684
768
|
if (!timeout) {
|
|
685
769
|
timeout = 30000;
|
|
686
770
|
}
|
|
771
|
+
let element = null;
|
|
772
|
+
let allStrategyLocators = null;
|
|
773
|
+
let selectedStrategy = null;
|
|
774
|
+
if (this.tryAllStrategies) {
|
|
775
|
+
allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
|
|
776
|
+
selectedStrategy = allStrategyLocators?.strategy;
|
|
777
|
+
}
|
|
687
778
|
for (let i = 0; i < 3; i++) {
|
|
688
779
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
689
780
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
690
781
|
let selector = selectors.locators[j];
|
|
691
782
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
692
783
|
}
|
|
693
|
-
|
|
784
|
+
if (this.tryAllStrategies && selectedStrategy) {
|
|
785
|
+
const strategyLocators = allStrategyLocators[selectedStrategy];
|
|
786
|
+
let err;
|
|
787
|
+
if (strategyLocators && strategyLocators.length) {
|
|
788
|
+
try {
|
|
789
|
+
selectors.locators = strategyLocators;
|
|
790
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
791
|
+
info.selectedStrategy = selectedStrategy;
|
|
792
|
+
info.log += "element found using strategy " + selectedStrategy + "\n";
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
err = error;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (!element) {
|
|
799
|
+
for (const key in allStrategyLocators) {
|
|
800
|
+
if (key === "strategy" || key === selectedStrategy) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const strategyLocators = allStrategyLocators[key];
|
|
804
|
+
if (strategyLocators && strategyLocators.length) {
|
|
805
|
+
try {
|
|
806
|
+
info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
|
|
807
|
+
selectors.locators = strategyLocators;
|
|
808
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
809
|
+
err = null;
|
|
810
|
+
info.selectedStrategy = key;
|
|
811
|
+
info.log += "element found using strategy " + key + "\n";
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
err = error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (err) {
|
|
821
|
+
throw err;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
826
|
+
}
|
|
694
827
|
if (!element.rerun) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
828
|
+
let newElementSelector = "";
|
|
829
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
|
|
830
|
+
const cssSelector = await element.evaluate((el) => {
|
|
831
|
+
function getCssSelector(el) {
|
|
832
|
+
if (!el || el.nodeType !== 1 || el === document.body)
|
|
833
|
+
return el.tagName.toLowerCase();
|
|
834
|
+
const parent = el.parentElement;
|
|
835
|
+
const tag = el.tagName.toLowerCase();
|
|
836
|
+
// Find the index of the element among its siblings of the same tag
|
|
837
|
+
let index = 1;
|
|
838
|
+
for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
839
|
+
if (sibling.tagName === el.tagName) {
|
|
840
|
+
index++;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Use nth-child if necessary (i.e., if there's more than one of the same tag)
|
|
844
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
845
|
+
const needsNthChild = siblings.length > 1;
|
|
846
|
+
const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
|
|
847
|
+
return getCssSelector(parent) + " > " + selector;
|
|
848
|
+
}
|
|
849
|
+
const cssSelector = getCssSelector(el);
|
|
850
|
+
return cssSelector;
|
|
851
|
+
});
|
|
852
|
+
newElementSelector = cssSelector;
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
856
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
|
|
857
|
+
const dataAttribute = "data-blinq-id";
|
|
858
|
+
await element.evaluate((el, [dataAttribute, randomToken]) => {
|
|
859
|
+
el.setAttribute(dataAttribute, randomToken);
|
|
860
|
+
}, [dataAttribute, randomToken]);
|
|
861
|
+
newElementSelector = `[${dataAttribute}="${randomToken}"]`;
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
// the default case just return the located element
|
|
865
|
+
// will not work for click and type if the locator is placeholder and the placeholder change due to the click event
|
|
866
|
+
return element;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
702
869
|
const scope = element._frame ?? element.page();
|
|
703
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
704
870
|
let prefixSelector = "";
|
|
705
871
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
706
872
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
707
873
|
if (frameSelectorIndex !== -1) {
|
|
708
874
|
// remove everything after the >> internal:control=enter-frame
|
|
709
875
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
710
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
876
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
711
877
|
}
|
|
712
878
|
// if (element?._frame?._selector) {
|
|
713
879
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
714
880
|
// }
|
|
715
881
|
const newSelector = prefixSelector + newElementSelector;
|
|
716
|
-
return scope.locator(newSelector);
|
|
882
|
+
return scope.locator(newSelector).first();
|
|
717
883
|
}
|
|
718
884
|
}
|
|
719
885
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -734,7 +900,7 @@ class StableBrowser {
|
|
|
734
900
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
735
901
|
let frameLocator = frame.selectors[i];
|
|
736
902
|
if (frameLocator.css) {
|
|
737
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
903
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
738
904
|
if (frameLocator.index) {
|
|
739
905
|
testframescope = framescope.nth(frameLocator.index);
|
|
740
906
|
}
|
|
@@ -812,6 +978,15 @@ class StableBrowser {
|
|
|
812
978
|
});
|
|
813
979
|
}
|
|
814
980
|
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
981
|
+
if (selectors.locators && Array.isArray(selectors.locators)) {
|
|
982
|
+
selectors.locators.forEach((locator) => {
|
|
983
|
+
locator.index = locator.index ?? 0;
|
|
984
|
+
locator.visible = locator.visible ?? true;
|
|
985
|
+
if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
986
|
+
locator.css = locator.css + " >> visible=true";
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
}
|
|
815
990
|
if (!info) {
|
|
816
991
|
info = {};
|
|
817
992
|
info.failCause = {};
|
|
@@ -967,9 +1142,13 @@ class StableBrowser {
|
|
|
967
1142
|
}
|
|
968
1143
|
}
|
|
969
1144
|
if (foundLocators.length === 1) {
|
|
1145
|
+
let box = null;
|
|
1146
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1147
|
+
box = await foundLocators[0].boundingBox();
|
|
1148
|
+
}
|
|
970
1149
|
result.foundElements.push({
|
|
971
1150
|
locator: foundLocators[0],
|
|
972
|
-
box:
|
|
1151
|
+
box: box,
|
|
973
1152
|
unique: true,
|
|
974
1153
|
});
|
|
975
1154
|
result.locatorIndex = i;
|
|
@@ -1027,7 +1206,7 @@ class StableBrowser {
|
|
|
1027
1206
|
operation: "simpleClick",
|
|
1028
1207
|
log: "***** click on " + elementDescription + " *****\n",
|
|
1029
1208
|
};
|
|
1030
|
-
_preCommand(state, this);
|
|
1209
|
+
await _preCommand(state, this);
|
|
1031
1210
|
const startTime = Date.now();
|
|
1032
1211
|
let timeout = 30000;
|
|
1033
1212
|
if (options && options.timeout) {
|
|
@@ -1076,7 +1255,7 @@ class StableBrowser {
|
|
|
1076
1255
|
operation: "simpleClickType",
|
|
1077
1256
|
log: "***** click type on " + elementDescription + " *****\n",
|
|
1078
1257
|
};
|
|
1079
|
-
_preCommand(state, this);
|
|
1258
|
+
await _preCommand(state, this);
|
|
1080
1259
|
const startTime = Date.now();
|
|
1081
1260
|
let timeout = 30000;
|
|
1082
1261
|
if (options && options.timeout) {
|
|
@@ -1124,11 +1303,22 @@ class StableBrowser {
|
|
|
1124
1303
|
operation: "click",
|
|
1125
1304
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1126
1305
|
};
|
|
1306
|
+
check_performance("click_all ***", this.context, true);
|
|
1307
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
1308
|
+
if (stepFastMode) {
|
|
1309
|
+
state.onlyFailuresScreenshot = true;
|
|
1310
|
+
state.scroll = false;
|
|
1311
|
+
state.highlight = false;
|
|
1312
|
+
}
|
|
1127
1313
|
try {
|
|
1314
|
+
check_performance("click_preCommand", this.context, true);
|
|
1128
1315
|
await _preCommand(state, this);
|
|
1316
|
+
check_performance("click_preCommand", this.context, false);
|
|
1129
1317
|
await performAction("click", state.element, options, this, state, _params);
|
|
1130
|
-
if (!this.fastMode) {
|
|
1131
|
-
|
|
1318
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1319
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1320
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1321
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1132
1322
|
}
|
|
1133
1323
|
return state.info;
|
|
1134
1324
|
}
|
|
@@ -1136,7 +1326,13 @@ class StableBrowser {
|
|
|
1136
1326
|
await _commandError(state, e, this);
|
|
1137
1327
|
}
|
|
1138
1328
|
finally {
|
|
1329
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1139
1330
|
await _commandFinally(state, this);
|
|
1331
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1332
|
+
check_performance("click_all ***", this.context, false);
|
|
1333
|
+
if (this.context.profile) {
|
|
1334
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1335
|
+
}
|
|
1140
1336
|
}
|
|
1141
1337
|
}
|
|
1142
1338
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1228,7 +1424,7 @@ class StableBrowser {
|
|
|
1228
1424
|
}
|
|
1229
1425
|
}
|
|
1230
1426
|
}
|
|
1231
|
-
await this.waitForPageLoad();
|
|
1427
|
+
//await this.waitForPageLoad();
|
|
1232
1428
|
return state.info;
|
|
1233
1429
|
}
|
|
1234
1430
|
catch (e) {
|
|
@@ -1254,7 +1450,7 @@ class StableBrowser {
|
|
|
1254
1450
|
await _preCommand(state, this);
|
|
1255
1451
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1256
1452
|
await _screenshot(state, this);
|
|
1257
|
-
await this.waitForPageLoad();
|
|
1453
|
+
//await this.waitForPageLoad();
|
|
1258
1454
|
return state.info;
|
|
1259
1455
|
}
|
|
1260
1456
|
catch (e) {
|
|
@@ -1290,7 +1486,7 @@ class StableBrowser {
|
|
|
1290
1486
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1291
1487
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1292
1488
|
}
|
|
1293
|
-
await this.waitForPageLoad();
|
|
1489
|
+
//await this.waitForPageLoad();
|
|
1294
1490
|
return state.info;
|
|
1295
1491
|
}
|
|
1296
1492
|
catch (e) {
|
|
@@ -1394,7 +1590,7 @@ class StableBrowser {
|
|
|
1394
1590
|
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1395
1591
|
operation: "setDateTime",
|
|
1396
1592
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1397
|
-
throwError: false,
|
|
1593
|
+
// throwError: false,
|
|
1398
1594
|
};
|
|
1399
1595
|
try {
|
|
1400
1596
|
await _preCommand(state, this);
|
|
@@ -1476,6 +1672,14 @@ class StableBrowser {
|
|
|
1476
1672
|
}
|
|
1477
1673
|
try {
|
|
1478
1674
|
await _preCommand(state, this);
|
|
1675
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1676
|
+
// tag the element
|
|
1677
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1678
|
+
// use attribute and not id
|
|
1679
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1680
|
+
el.setAttribute(attrName, "");
|
|
1681
|
+
return `[${attrName}]`;
|
|
1682
|
+
}, randomToken);
|
|
1479
1683
|
state.info.value = _value;
|
|
1480
1684
|
if (!options.press) {
|
|
1481
1685
|
try {
|
|
@@ -1501,6 +1705,25 @@ class StableBrowser {
|
|
|
1501
1705
|
}
|
|
1502
1706
|
}
|
|
1503
1707
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1708
|
+
// check if the element exist after the click (no wait)
|
|
1709
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1710
|
+
if (count === 0) {
|
|
1711
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1712
|
+
const scope = state.element._frame ?? element.page();
|
|
1713
|
+
let prefixSelector = "";
|
|
1714
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1715
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1716
|
+
if (frameSelectorIndex !== -1) {
|
|
1717
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1718
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1719
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1720
|
+
}
|
|
1721
|
+
// if (element?._frame?._selector) {
|
|
1722
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1723
|
+
// }
|
|
1724
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1725
|
+
state.element = scope.locator(newSelector).first();
|
|
1726
|
+
}
|
|
1504
1727
|
const valueSegment = state.value.split("&&");
|
|
1505
1728
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1506
1729
|
if (i > 0) {
|
|
@@ -1572,8 +1795,8 @@ class StableBrowser {
|
|
|
1572
1795
|
if (enter) {
|
|
1573
1796
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1574
1797
|
await this.page.keyboard.press("Enter");
|
|
1798
|
+
await this.waitForPageLoad();
|
|
1575
1799
|
}
|
|
1576
|
-
await this.waitForPageLoad();
|
|
1577
1800
|
return state.info;
|
|
1578
1801
|
}
|
|
1579
1802
|
catch (e) {
|
|
@@ -1832,11 +2055,12 @@ class StableBrowser {
|
|
|
1832
2055
|
throw new Error("referanceSnapshot is null");
|
|
1833
2056
|
}
|
|
1834
2057
|
let text = null;
|
|
1835
|
-
|
|
1836
|
-
|
|
2058
|
+
const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
|
|
2059
|
+
if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
|
|
2060
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
|
|
1837
2061
|
}
|
|
1838
|
-
else if (fs.existsSync(path.join(
|
|
1839
|
-
text = fs.readFileSync(path.join(
|
|
2062
|
+
else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
|
|
2063
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
|
|
1840
2064
|
}
|
|
1841
2065
|
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1842
2066
|
text = referanceSnapshot.substring(5);
|
|
@@ -1860,7 +2084,13 @@ class StableBrowser {
|
|
|
1860
2084
|
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1861
2085
|
}
|
|
1862
2086
|
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
2087
|
+
if (snapshot && snapshot.length <= 10) {
|
|
2088
|
+
console.log("Page snapshot length is suspiciously small:", snapshot);
|
|
2089
|
+
}
|
|
1863
2090
|
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
2091
|
+
if (matchResult === undefined) {
|
|
2092
|
+
console.log("snapshotValidation returned undefined");
|
|
2093
|
+
}
|
|
1864
2094
|
if (matchResult.errorLine !== -1) {
|
|
1865
2095
|
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1866
2096
|
}
|
|
@@ -2031,6 +2261,9 @@ class StableBrowser {
|
|
|
2031
2261
|
return _getTestData(world, this.context, this);
|
|
2032
2262
|
}
|
|
2033
2263
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2264
|
+
if (!options) {
|
|
2265
|
+
options = {};
|
|
2266
|
+
}
|
|
2034
2267
|
// collect url/path/title
|
|
2035
2268
|
if (info) {
|
|
2036
2269
|
if (!info.title) {
|
|
@@ -2059,7 +2292,7 @@ class StableBrowser {
|
|
|
2059
2292
|
const uuidStr = "id_" + randomUUID();
|
|
2060
2293
|
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
2061
2294
|
try {
|
|
2062
|
-
await this.takeScreenshot(screenshotPath);
|
|
2295
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
2063
2296
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
2064
2297
|
// // save the buffer to the screenshot path asynchrously
|
|
2065
2298
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -2080,7 +2313,7 @@ class StableBrowser {
|
|
|
2080
2313
|
else if (options && options.screenshot) {
|
|
2081
2314
|
result.screenshotPath = options.screenshotPath;
|
|
2082
2315
|
try {
|
|
2083
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2316
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
2084
2317
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
2085
2318
|
// // save the buffer to the screenshot path asynchrously
|
|
2086
2319
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -2098,7 +2331,7 @@ class StableBrowser {
|
|
|
2098
2331
|
}
|
|
2099
2332
|
return result;
|
|
2100
2333
|
}
|
|
2101
|
-
async takeScreenshot(screenshotPath) {
|
|
2334
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
2102
2335
|
const playContext = this.context.playContext;
|
|
2103
2336
|
// Using CDP to capture the screenshot
|
|
2104
2337
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -2123,13 +2356,7 @@ class StableBrowser {
|
|
|
2123
2356
|
const client = await playContext.newCDPSession(this.page);
|
|
2124
2357
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
2125
2358
|
format: "png",
|
|
2126
|
-
|
|
2127
|
-
// x: 0,
|
|
2128
|
-
// y: 0,
|
|
2129
|
-
// width: viewportWidth,
|
|
2130
|
-
// height: viewportHeight,
|
|
2131
|
-
// scale: 1,
|
|
2132
|
-
// },
|
|
2359
|
+
captureBeyondViewport: fullPage,
|
|
2133
2360
|
});
|
|
2134
2361
|
await client.detach();
|
|
2135
2362
|
if (!screenshotPath) {
|
|
@@ -2138,7 +2365,7 @@ class StableBrowser {
|
|
|
2138
2365
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
2139
2366
|
}
|
|
2140
2367
|
else {
|
|
2141
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2368
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
2142
2369
|
}
|
|
2143
2370
|
// if (focusedElement) {
|
|
2144
2371
|
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
@@ -2238,6 +2465,12 @@ class StableBrowser {
|
|
|
2238
2465
|
state.info.value = state.value;
|
|
2239
2466
|
this.setTestData({ [variable]: state.value }, world);
|
|
2240
2467
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2468
|
+
if (process.env.MODE === "executions") {
|
|
2469
|
+
const globalDataFile = "global_test_data.json";
|
|
2470
|
+
if (existsSync(globalDataFile)) {
|
|
2471
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2241
2474
|
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2242
2475
|
return state.info;
|
|
2243
2476
|
}
|
|
@@ -2309,6 +2542,12 @@ class StableBrowser {
|
|
|
2309
2542
|
state.info.value = state.value;
|
|
2310
2543
|
this.setTestData({ [variable]: state.value }, world);
|
|
2311
2544
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2545
|
+
if (process.env.MODE === "executions") {
|
|
2546
|
+
const globalDataFile = "global_test_data.json";
|
|
2547
|
+
if (existsSync(globalDataFile)) {
|
|
2548
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2312
2551
|
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2313
2552
|
return state.info;
|
|
2314
2553
|
}
|
|
@@ -2439,7 +2678,7 @@ class StableBrowser {
|
|
|
2439
2678
|
let expectedValue;
|
|
2440
2679
|
try {
|
|
2441
2680
|
await _preCommand(state, this);
|
|
2442
|
-
expectedValue = await
|
|
2681
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2443
2682
|
state.info.expectedValue = expectedValue;
|
|
2444
2683
|
switch (property) {
|
|
2445
2684
|
case "innerText":
|
|
@@ -2565,6 +2804,7 @@ class StableBrowser {
|
|
|
2565
2804
|
allowDisabled: true,
|
|
2566
2805
|
info: {},
|
|
2567
2806
|
};
|
|
2807
|
+
state.options ??= { timeout: timeoutMs };
|
|
2568
2808
|
// Initialize startTime outside try block to ensure it's always accessible
|
|
2569
2809
|
const startTime = Date.now();
|
|
2570
2810
|
let conditionMet = false;
|
|
@@ -2734,6 +2974,12 @@ class StableBrowser {
|
|
|
2734
2974
|
emailUrl = url;
|
|
2735
2975
|
codeOrUrlFound = true;
|
|
2736
2976
|
}
|
|
2977
|
+
if (process.env.MODE === "executions") {
|
|
2978
|
+
const globalDataFile = "global_test_data.json";
|
|
2979
|
+
if (existsSync(globalDataFile)) {
|
|
2980
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2737
2983
|
if (codeOrUrlFound) {
|
|
2738
2984
|
return { emailUrl, emailCode };
|
|
2739
2985
|
}
|
|
@@ -3140,7 +3386,16 @@ class StableBrowser {
|
|
|
3140
3386
|
text = text.replace(/\\"/g, '"');
|
|
3141
3387
|
}
|
|
3142
3388
|
const timeout = this._getFindElementTimeout(options);
|
|
3143
|
-
|
|
3389
|
+
//if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
3390
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
3391
|
+
if (!stepFastMode) {
|
|
3392
|
+
if (!this.fastMode) {
|
|
3393
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3394
|
+
}
|
|
3395
|
+
else {
|
|
3396
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3144
3399
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
3145
3400
|
if (newValue !== text) {
|
|
3146
3401
|
this.logger.info(text + "=" + newValue);
|
|
@@ -3148,6 +3403,11 @@ class StableBrowser {
|
|
|
3148
3403
|
}
|
|
3149
3404
|
let dateAlternatives = findDateAlternatives(text);
|
|
3150
3405
|
let numberAlternatives = findNumberAlternatives(text);
|
|
3406
|
+
if (stepFastMode) {
|
|
3407
|
+
state.onlyFailuresScreenshot = true;
|
|
3408
|
+
state.scroll = false;
|
|
3409
|
+
state.highlight = false;
|
|
3410
|
+
}
|
|
3151
3411
|
try {
|
|
3152
3412
|
await _preCommand(state, this);
|
|
3153
3413
|
state.info.text = text;
|
|
@@ -3267,6 +3527,8 @@ class StableBrowser {
|
|
|
3267
3527
|
operation: "verify_text_with_relation",
|
|
3268
3528
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3269
3529
|
};
|
|
3530
|
+
const cmdStartTime = Date.now();
|
|
3531
|
+
let cmdEndTime = null;
|
|
3270
3532
|
const timeout = this._getFindElementTimeout(options);
|
|
3271
3533
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3272
3534
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3302,6 +3564,17 @@ class StableBrowser {
|
|
|
3302
3564
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3303
3565
|
continue;
|
|
3304
3566
|
}
|
|
3567
|
+
else {
|
|
3568
|
+
cmdEndTime = Date.now();
|
|
3569
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3570
|
+
if (foundAncore) {
|
|
3571
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3572
|
+
}
|
|
3573
|
+
else {
|
|
3574
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3305
3578
|
try {
|
|
3306
3579
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3307
3580
|
foundAncore = true;
|
|
@@ -3314,7 +3587,9 @@ class StableBrowser {
|
|
|
3314
3587
|
climbArray1.push("..");
|
|
3315
3588
|
}
|
|
3316
3589
|
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
3317
|
-
|
|
3590
|
+
if (Number(climb) > 0) {
|
|
3591
|
+
css = css + " >> " + climbXpath;
|
|
3592
|
+
}
|
|
3318
3593
|
const count = await frame.locator(css).count();
|
|
3319
3594
|
for (let j = 0; j < count; j++) {
|
|
3320
3595
|
const continer = await frame.locator(css).nth(j);
|
|
@@ -3440,7 +3715,7 @@ class StableBrowser {
|
|
|
3440
3715
|
Object.assign(e, { info: info });
|
|
3441
3716
|
error = e;
|
|
3442
3717
|
// throw e;
|
|
3443
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3718
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3444
3719
|
}
|
|
3445
3720
|
finally {
|
|
3446
3721
|
const endTime = Date.now();
|
|
@@ -3785,10 +4060,26 @@ class StableBrowser {
|
|
|
3785
4060
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
3786
4061
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3787
4062
|
if (this.onRestoreSaveState) {
|
|
3788
|
-
this.onRestoreSaveState(path);
|
|
4063
|
+
await this.onRestoreSaveState(path);
|
|
3789
4064
|
}
|
|
3790
4065
|
}
|
|
3791
4066
|
async waitForPageLoad(options = {}, world = null) {
|
|
4067
|
+
// try {
|
|
4068
|
+
// let currentPagePath = null;
|
|
4069
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4070
|
+
// if (this.latestPagePath) {
|
|
4071
|
+
// // get the currect page path and compare with the latest page path
|
|
4072
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4073
|
+
// // if the page path is the same, do not wait for page load
|
|
4074
|
+
// console.log("No page change: " + currentPagePath);
|
|
4075
|
+
// return;
|
|
4076
|
+
// }
|
|
4077
|
+
// }
|
|
4078
|
+
// this.latestPagePath = currentPagePath;
|
|
4079
|
+
// } catch (e) {
|
|
4080
|
+
// console.debug("Error getting current page path: ", e);
|
|
4081
|
+
// }
|
|
4082
|
+
//console.log("Waiting for page load");
|
|
3792
4083
|
let timeout = this._getLoadTimeout(options);
|
|
3793
4084
|
const promiseArray = [];
|
|
3794
4085
|
// let waitForNetworkIdle = true;
|
|
@@ -3823,7 +4114,10 @@ class StableBrowser {
|
|
|
3823
4114
|
}
|
|
3824
4115
|
}
|
|
3825
4116
|
finally {
|
|
3826
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4117
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4118
|
+
if (options && !options.noSleep) {
|
|
4119
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4120
|
+
}
|
|
3827
4121
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3828
4122
|
const endTime = Date.now();
|
|
3829
4123
|
_reportToWorld(world, {
|
|
@@ -3857,7 +4151,7 @@ class StableBrowser {
|
|
|
3857
4151
|
_text: `Close the page`,
|
|
3858
4152
|
operation: "closePage",
|
|
3859
4153
|
log: "***** close page *****\n",
|
|
3860
|
-
throwError: false,
|
|
4154
|
+
// throwError: false,
|
|
3861
4155
|
};
|
|
3862
4156
|
try {
|
|
3863
4157
|
await _preCommand(state, this);
|
|
@@ -3877,7 +4171,7 @@ class StableBrowser {
|
|
|
3877
4171
|
}
|
|
3878
4172
|
operation = options.operation;
|
|
3879
4173
|
// validate operation is one of the supported operations
|
|
3880
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4174
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3881
4175
|
throw new Error("operation is not supported");
|
|
3882
4176
|
}
|
|
3883
4177
|
const state = {
|
|
@@ -3946,6 +4240,17 @@ class StableBrowser {
|
|
|
3946
4240
|
state.element = results[0];
|
|
3947
4241
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3948
4242
|
break;
|
|
4243
|
+
case "hover":
|
|
4244
|
+
if (!options.css) {
|
|
4245
|
+
throw new Error("css is not defined");
|
|
4246
|
+
}
|
|
4247
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4248
|
+
if (result1.length === 0) {
|
|
4249
|
+
throw new Error(`Element not found in cell area`);
|
|
4250
|
+
}
|
|
4251
|
+
state.element = result1[0];
|
|
4252
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4253
|
+
break;
|
|
3949
4254
|
default:
|
|
3950
4255
|
throw new Error("operation is not supported");
|
|
3951
4256
|
}
|
|
@@ -3959,6 +4264,15 @@ class StableBrowser {
|
|
|
3959
4264
|
}
|
|
3960
4265
|
saveTestDataAsGlobal(options, world) {
|
|
3961
4266
|
const dataFile = _getDataFile(world, this.context, this);
|
|
4267
|
+
if (process.env.MODE === "executions") {
|
|
4268
|
+
const globalDataFile = path.join(this.project_path, "global_test_data.json");
|
|
4269
|
+
const dataFileContents = fs.existsSync(dataFile) ? JSON.parse(fs.readFileSync(dataFile)) : {};
|
|
4270
|
+
const globalDataFileContents = fs.existsSync(globalDataFile) ? JSON.parse(fs.readFileSync(globalDataFile)) : {};
|
|
4271
|
+
const mergedData = JSON.stringify(_.merge({}, dataFileContents, globalDataFileContents), null, 2);
|
|
4272
|
+
fs.writeFileSync(dataFile, mergedData);
|
|
4273
|
+
this.logger.info("Save the scenario test data to " + dataFile + " as global for the following scenarios.");
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
3962
4276
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
3963
4277
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
3964
4278
|
}
|
|
@@ -4061,6 +4375,7 @@ class StableBrowser {
|
|
|
4061
4375
|
if (world && world.attach) {
|
|
4062
4376
|
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4063
4377
|
}
|
|
4378
|
+
this.context.loadedRoutes = null;
|
|
4064
4379
|
this.beforeScenarioCalled = true;
|
|
4065
4380
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4066
4381
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -4074,9 +4389,12 @@ class StableBrowser {
|
|
|
4074
4389
|
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4075
4390
|
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4076
4391
|
// check if @global_test_data tag is present
|
|
4077
|
-
if (this.tags.includes("@global_test_data")) {
|
|
4392
|
+
if (this.tags.includes("@global_test_data" /* TAG_CONSTANTS.GLOBAL_TEST_DATA */)) {
|
|
4078
4393
|
this.saveTestDataAsGlobal({}, world);
|
|
4079
4394
|
}
|
|
4395
|
+
if (this.tags.includes("@fast_mode" /* TAG_CONSTANTS.FAST_MODE */)) {
|
|
4396
|
+
this.fastMode = true;
|
|
4397
|
+
}
|
|
4080
4398
|
}
|
|
4081
4399
|
// update test data based on feature/scenario
|
|
4082
4400
|
let envName = null;
|
|
@@ -4087,14 +4405,51 @@ class StableBrowser {
|
|
|
4087
4405
|
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4088
4406
|
}
|
|
4089
4407
|
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4408
|
+
if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
|
|
4409
|
+
this.trace = true;
|
|
4410
|
+
const traceFolder = path.join(this.context.reportFolder, "trace");
|
|
4411
|
+
if (!fs.existsSync(traceFolder)) {
|
|
4412
|
+
fs.mkdirSync(traceFolder, { recursive: true });
|
|
4413
|
+
}
|
|
4414
|
+
this.traceFolder = traceFolder;
|
|
4415
|
+
await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
async afterScenario(world, scenario) {
|
|
4419
|
+
const id = scenario.testCaseStartedId;
|
|
4420
|
+
if (this.trace) {
|
|
4421
|
+
await this.context.playContext.tracing.stop({
|
|
4422
|
+
path: path.join(this.traceFolder, `trace-${id}.zip`),
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
getGherkinKeyword(step) {
|
|
4427
|
+
if (!step?.type) {
|
|
4428
|
+
return "";
|
|
4429
|
+
}
|
|
4430
|
+
switch (step.type) {
|
|
4431
|
+
case "Context":
|
|
4432
|
+
return "Given";
|
|
4433
|
+
case "Action":
|
|
4434
|
+
return "When";
|
|
4435
|
+
case "Outcome":
|
|
4436
|
+
return "Then";
|
|
4437
|
+
case "Conjunction":
|
|
4438
|
+
return "And";
|
|
4439
|
+
default:
|
|
4440
|
+
return "";
|
|
4441
|
+
}
|
|
4090
4442
|
}
|
|
4091
|
-
async afterScenario(world, scenario) { }
|
|
4092
4443
|
async beforeStep(world, step) {
|
|
4093
|
-
if (this.
|
|
4094
|
-
|
|
4444
|
+
if (step?.pickleStep && this.trace) {
|
|
4445
|
+
const keyword = this.getGherkinKeyword(step.pickleStep);
|
|
4446
|
+
this.traceGroupName = `${keyword} ${step.pickleStep.text}`;
|
|
4447
|
+
await this.context.playContext.tracing.group(this.traceGroupName);
|
|
4095
4448
|
}
|
|
4449
|
+
this.stepTags = [];
|
|
4096
4450
|
if (!this.beforeScenarioCalled) {
|
|
4097
4451
|
this.beforeScenario(world, step);
|
|
4452
|
+
this.context.loadedRoutes = null;
|
|
4098
4453
|
}
|
|
4099
4454
|
if (this.stepIndex === undefined) {
|
|
4100
4455
|
this.stepIndex = 0;
|
|
@@ -4104,7 +4459,12 @@ class StableBrowser {
|
|
|
4104
4459
|
}
|
|
4105
4460
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4106
4461
|
this.stepName = step.pickleStep.text;
|
|
4107
|
-
|
|
4462
|
+
let printableStepName = this.stepName;
|
|
4463
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4464
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4465
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4466
|
+
});
|
|
4467
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4108
4468
|
}
|
|
4109
4469
|
else if (step && step.text) {
|
|
4110
4470
|
this.stepName = step.text;
|
|
@@ -4119,7 +4479,10 @@ class StableBrowser {
|
|
|
4119
4479
|
}
|
|
4120
4480
|
if (this.initSnapshotTaken === false) {
|
|
4121
4481
|
this.initSnapshotTaken = true;
|
|
4122
|
-
if (world &&
|
|
4482
|
+
if (world &&
|
|
4483
|
+
world.attach &&
|
|
4484
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4485
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4123
4486
|
const snapshot = await this.getAriaSnapshot();
|
|
4124
4487
|
if (snapshot) {
|
|
4125
4488
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -4129,7 +4492,11 @@ class StableBrowser {
|
|
|
4129
4492
|
this.context.routeResults = null;
|
|
4130
4493
|
this.context.loadedRoutes = null;
|
|
4131
4494
|
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4132
|
-
networkBeforeStep(this.stepName);
|
|
4495
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4496
|
+
this.inStepReport = false;
|
|
4497
|
+
}
|
|
4498
|
+
setStepTags(tags) {
|
|
4499
|
+
this.stepTags = tags;
|
|
4133
4500
|
}
|
|
4134
4501
|
async getAriaSnapshot() {
|
|
4135
4502
|
try {
|
|
@@ -4203,7 +4570,7 @@ class StableBrowser {
|
|
|
4203
4570
|
state.payload = payload;
|
|
4204
4571
|
if (commandStatus === "FAILED") {
|
|
4205
4572
|
state.throwError = true;
|
|
4206
|
-
throw new Error(
|
|
4573
|
+
throw new Error(commandText);
|
|
4207
4574
|
}
|
|
4208
4575
|
}
|
|
4209
4576
|
catch (e) {
|
|
@@ -4213,26 +4580,22 @@ class StableBrowser {
|
|
|
4213
4580
|
await _commandFinally(state, this);
|
|
4214
4581
|
}
|
|
4215
4582
|
}
|
|
4216
|
-
async afterStep(world, step) {
|
|
4583
|
+
async afterStep(world, step, result) {
|
|
4217
4584
|
this.stepName = null;
|
|
4218
|
-
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4219
|
-
if (this.context.browserObject.context) {
|
|
4220
|
-
await this.context.browserObject.context.tracing.stopChunk({
|
|
4221
|
-
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4222
|
-
});
|
|
4223
|
-
if (world && world.attach) {
|
|
4224
|
-
await world.attach(JSON.stringify({
|
|
4225
|
-
type: "trace",
|
|
4226
|
-
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4227
|
-
}), "application/json+trace");
|
|
4228
|
-
}
|
|
4229
|
-
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4230
|
-
}
|
|
4231
|
-
}
|
|
4232
4585
|
if (this.context) {
|
|
4233
4586
|
this.context.examplesRow = null;
|
|
4234
4587
|
}
|
|
4235
|
-
if (
|
|
4588
|
+
if (!this.inStepReport) {
|
|
4589
|
+
// check the step result
|
|
4590
|
+
if (result && result.status === "FAILED" && world && world.attach) {
|
|
4591
|
+
await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
if (world &&
|
|
4595
|
+
world.attach &&
|
|
4596
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4597
|
+
!this.fastMode &&
|
|
4598
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4236
4599
|
const snapshot = await this.getAriaSnapshot();
|
|
4237
4600
|
if (snapshot) {
|
|
4238
4601
|
const obj = {};
|
|
@@ -4240,6 +4603,11 @@ class StableBrowser {
|
|
|
4240
4603
|
}
|
|
4241
4604
|
}
|
|
4242
4605
|
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4606
|
+
if (this.context.routeResults) {
|
|
4607
|
+
if (world && world.attach) {
|
|
4608
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4243
4611
|
if (!process.env.TEMP_RUN) {
|
|
4244
4612
|
const state = {
|
|
4245
4613
|
world,
|
|
@@ -4263,10 +4631,15 @@ class StableBrowser {
|
|
|
4263
4631
|
await _commandFinally(state, this);
|
|
4264
4632
|
}
|
|
4265
4633
|
}
|
|
4266
|
-
networkAfterStep(this.stepName);
|
|
4634
|
+
networkAfterStep(this.stepName, this.context);
|
|
4267
4635
|
if (process.env.TEMP_RUN === "true") {
|
|
4268
4636
|
// Put a sleep for some time to allow the browser to finish processing
|
|
4269
|
-
|
|
4637
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4638
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
if (this.trace) {
|
|
4642
|
+
await this.context.playContext.tracing.groupEnd();
|
|
4270
4643
|
}
|
|
4271
4644
|
}
|
|
4272
4645
|
}
|