automation_model 1.0.752-dev → 1.0.752-stage
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/api.js +11 -7
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +3 -1
- package/lib/auto_page.js +66 -16
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +57 -32
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.js.map +1 -1
- package/lib/check_performance.d.ts +1 -0
- package/lib/check_performance.js +57 -0
- package/lib/check_performance.js.map +1 -0
- package/lib/command_common.js +17 -1
- package/lib/command_common.js.map +1 -1
- package/lib/file_checker.js +136 -25
- package/lib/file_checker.js.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +1 -2
- package/lib/init_browser.js +122 -126
- package/lib/init_browser.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +2 -0
- package/lib/network.js +398 -87
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +66 -16
- package/lib/route.js +539 -125
- package/lib/route.js.map +1 -1
- package/lib/scripts/axe.mini.js +23994 -1
- package/lib/snapshot_validation.js.map +1 -1
- package/lib/stable_browser.d.ts +15 -4
- package/lib/stable_browser.js +569 -87
- package/lib/stable_browser.js.map +1 -1
- package/lib/table_helper.js +14 -0
- package/lib/table_helper.js.map +1 -1
- package/lib/test_context.d.ts +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +5 -2
- package/lib/utils.js +52 -9
- package/lib/utils.js.map +1 -1
- package/package.json +20 -11
package/lib/stable_browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
+
import { check_performance } from "./check_performance.js";
|
|
2
3
|
import { expect } from "@playwright/test";
|
|
3
4
|
import dayjs from "dayjs";
|
|
4
5
|
import fs from "fs";
|
|
@@ -10,6 +11,7 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
11
|
import drawRectangle from "./drawRect.js";
|
|
11
12
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
13
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
14
|
+
import errorStackParser from "error-stack-parser";
|
|
13
15
|
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
14
16
|
import csv from "csv-parser";
|
|
15
17
|
import { Readable } from "node:stream";
|
|
@@ -19,17 +21,20 @@ import { getTestData } from "./auto_page.js";
|
|
|
19
21
|
import { locate_element } from "./locate_element.js";
|
|
20
22
|
import { randomUUID } from "crypto";
|
|
21
23
|
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
22
|
-
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
24
|
+
import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
23
25
|
import { LocatorLog } from "./locator_log.js";
|
|
24
26
|
import axios from "axios";
|
|
25
27
|
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
28
|
import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
27
29
|
import { loadBrunoParams } from "./bruno.js";
|
|
28
30
|
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
29
32
|
export const Types = {
|
|
30
33
|
CLICK: "click_element",
|
|
31
34
|
WAIT_ELEMENT: "wait_element",
|
|
32
|
-
NAVIGATE: "navigate",
|
|
35
|
+
NAVIGATE: "navigate",
|
|
36
|
+
GO_BACK: "go_back",
|
|
37
|
+
GO_FORWARD: "go_forward",
|
|
33
38
|
FILL: "fill_element",
|
|
34
39
|
EXECUTE: "execute_page_method", //
|
|
35
40
|
OPEN: "open_environment", //
|
|
@@ -42,6 +47,7 @@ export const Types = {
|
|
|
42
47
|
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
43
48
|
ANALYZE_TABLE: "analyze_table",
|
|
44
49
|
SELECT: "select_combobox", //
|
|
50
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
45
51
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
46
52
|
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
47
53
|
TYPE_PRESS: "type_press",
|
|
@@ -60,15 +66,15 @@ export const Types = {
|
|
|
60
66
|
SET_INPUT: "set_input",
|
|
61
67
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
62
68
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
63
|
-
VERIFY_PROPERTY: "verify_element_property",
|
|
64
69
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
65
70
|
BRUNO: "bruno",
|
|
66
|
-
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
67
71
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
68
72
|
SET_INPUT_FILES: "set_input_files",
|
|
73
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
69
74
|
REPORT_COMMAND: "report_command",
|
|
70
75
|
STEP_COMPLETE: "step_complete",
|
|
71
76
|
SLEEP: "sleep",
|
|
77
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
72
78
|
};
|
|
73
79
|
export const apps = {};
|
|
74
80
|
const formatElementName = (elementName) => {
|
|
@@ -81,6 +87,7 @@ class StableBrowser {
|
|
|
81
87
|
context;
|
|
82
88
|
world;
|
|
83
89
|
fastMode;
|
|
90
|
+
stepTags;
|
|
84
91
|
project_path = null;
|
|
85
92
|
webLogFile = null;
|
|
86
93
|
networkLogger = null;
|
|
@@ -89,13 +96,15 @@ class StableBrowser {
|
|
|
89
96
|
tags = null;
|
|
90
97
|
isRecording = false;
|
|
91
98
|
initSnapshotTaken = false;
|
|
92
|
-
|
|
99
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
100
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
93
101
|
this.browser = browser;
|
|
94
102
|
this.page = page;
|
|
95
103
|
this.logger = logger;
|
|
96
104
|
this.context = context;
|
|
97
105
|
this.world = world;
|
|
98
106
|
this.fastMode = fastMode;
|
|
107
|
+
this.stepTags = stepTags;
|
|
99
108
|
if (!this.logger) {
|
|
100
109
|
this.logger = console;
|
|
101
110
|
}
|
|
@@ -128,6 +137,7 @@ class StableBrowser {
|
|
|
128
137
|
this.fastMode = true;
|
|
129
138
|
}
|
|
130
139
|
if (process.env.FAST_MODE === "true") {
|
|
140
|
+
// console.log("Fast mode enabled from environment variable");
|
|
131
141
|
this.fastMode = true;
|
|
132
142
|
}
|
|
133
143
|
if (process.env.FAST_MODE === "false") {
|
|
@@ -170,6 +180,7 @@ class StableBrowser {
|
|
|
170
180
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
171
181
|
registerDownloadEvent(this.page, this.world, context);
|
|
172
182
|
page.on("close", async () => {
|
|
183
|
+
// return if browser context is already closed
|
|
173
184
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
174
185
|
this.context.pages.pop();
|
|
175
186
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -179,7 +190,12 @@ class StableBrowser {
|
|
|
179
190
|
console.log("Switched to page " + title);
|
|
180
191
|
}
|
|
181
192
|
catch (error) {
|
|
182
|
-
|
|
193
|
+
if (error?.message?.includes("Target page, context or browser has been closed")) {
|
|
194
|
+
// Ignore this error
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
console.error("Error on page close", error);
|
|
198
|
+
}
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
});
|
|
@@ -188,7 +204,12 @@ class StableBrowser {
|
|
|
188
204
|
console.log("Switch page: " + (await page.title()));
|
|
189
205
|
}
|
|
190
206
|
catch (e) {
|
|
191
|
-
|
|
207
|
+
if (e?.message?.includes("Target page, context or browser has been closed")) {
|
|
208
|
+
// Ignore this error
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.logger.error("error on page load " + e);
|
|
212
|
+
}
|
|
192
213
|
}
|
|
193
214
|
context.pageLoading.status = false;
|
|
194
215
|
}.bind(this));
|
|
@@ -216,7 +237,7 @@ class StableBrowser {
|
|
|
216
237
|
if (newContextCreated) {
|
|
217
238
|
this.registerEventListeners(this.context);
|
|
218
239
|
await this.goto(this.context.environment.baseUrl);
|
|
219
|
-
if (!this.fastMode) {
|
|
240
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
220
241
|
await this.waitForPageLoad();
|
|
221
242
|
}
|
|
222
243
|
}
|
|
@@ -344,6 +365,64 @@ class StableBrowser {
|
|
|
344
365
|
await _commandFinally(state, this);
|
|
345
366
|
}
|
|
346
367
|
}
|
|
368
|
+
async goBack(options, world = null) {
|
|
369
|
+
const state = {
|
|
370
|
+
value: "",
|
|
371
|
+
world: world,
|
|
372
|
+
type: Types.GO_BACK,
|
|
373
|
+
text: `Browser navigate back`,
|
|
374
|
+
operation: "goBack",
|
|
375
|
+
log: "***** navigate back *****\n",
|
|
376
|
+
info: {},
|
|
377
|
+
locate: false,
|
|
378
|
+
scroll: false,
|
|
379
|
+
screenshot: false,
|
|
380
|
+
highlight: false,
|
|
381
|
+
};
|
|
382
|
+
try {
|
|
383
|
+
await _preCommand(state, this);
|
|
384
|
+
await this.page.goBack({
|
|
385
|
+
waitUntil: "load",
|
|
386
|
+
});
|
|
387
|
+
await _screenshot(state, this);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
console.error("Error on goBack", error);
|
|
391
|
+
_commandError(state, error, this);
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
await _commandFinally(state, this);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async goForward(options, world = null) {
|
|
398
|
+
const state = {
|
|
399
|
+
value: "",
|
|
400
|
+
world: world,
|
|
401
|
+
type: Types.GO_FORWARD,
|
|
402
|
+
text: `Browser navigate forward`,
|
|
403
|
+
operation: "goForward",
|
|
404
|
+
log: "***** navigate forward *****\n",
|
|
405
|
+
info: {},
|
|
406
|
+
locate: false,
|
|
407
|
+
scroll: false,
|
|
408
|
+
screenshot: false,
|
|
409
|
+
highlight: false,
|
|
410
|
+
};
|
|
411
|
+
try {
|
|
412
|
+
await _preCommand(state, this);
|
|
413
|
+
await this.page.goForward({
|
|
414
|
+
waitUntil: "load",
|
|
415
|
+
});
|
|
416
|
+
await _screenshot(state, this);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
console.error("Error on goForward", error);
|
|
420
|
+
_commandError(state, error, this);
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
await _commandFinally(state, this);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
347
426
|
async _getLocator(locator, scope, _params) {
|
|
348
427
|
locator = _fixLocatorUsingParams(locator, _params);
|
|
349
428
|
// locator = await this._replaceWithLocalData(locator);
|
|
@@ -442,12 +521,6 @@ class StableBrowser {
|
|
|
442
521
|
if (!el.setAttribute) {
|
|
443
522
|
el = el.parentElement;
|
|
444
523
|
}
|
|
445
|
-
// remove any attributes start with data-blinq-id
|
|
446
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
447
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
448
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
449
|
-
// }
|
|
450
|
-
// }
|
|
451
524
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
452
525
|
return true;
|
|
453
526
|
}, [tag1, randomToken]))) {
|
|
@@ -469,14 +542,13 @@ class StableBrowser {
|
|
|
469
542
|
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
470
543
|
}
|
|
471
544
|
let locatorSearch = selectorHierarchy[index];
|
|
472
|
-
let originalLocatorSearch = "";
|
|
473
545
|
try {
|
|
474
|
-
|
|
475
|
-
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
546
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
476
547
|
}
|
|
477
548
|
catch (e) {
|
|
478
549
|
console.error(e);
|
|
479
550
|
}
|
|
551
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
480
552
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
481
553
|
let locator = null;
|
|
482
554
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
@@ -618,40 +690,186 @@ class StableBrowser {
|
|
|
618
690
|
}
|
|
619
691
|
return { rerun: false };
|
|
620
692
|
}
|
|
693
|
+
getFilePath() {
|
|
694
|
+
const stackFrames = errorStackParser.parse(new Error());
|
|
695
|
+
const stackFrame = stackFrames.findLast((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
|
|
696
|
+
// return stackFrame?.fileName || null;
|
|
697
|
+
const filepath = stackFrame?.fileName;
|
|
698
|
+
if (filepath) {
|
|
699
|
+
let jsonFilePath = filepath.replace(".mjs", ".json");
|
|
700
|
+
if (existsSync(jsonFilePath)) {
|
|
701
|
+
return jsonFilePath;
|
|
702
|
+
}
|
|
703
|
+
const config = this.configuration ?? {};
|
|
704
|
+
if (!config?.locatorsMetadataDir) {
|
|
705
|
+
config.locatorsMetadataDir = "features/step_definitions/locators";
|
|
706
|
+
}
|
|
707
|
+
if (config && config.locatorsMetadataDir) {
|
|
708
|
+
jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
|
|
709
|
+
}
|
|
710
|
+
if (existsSync(jsonFilePath)) {
|
|
711
|
+
return jsonFilePath;
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
getFullElementLocators(selectors, filePath) {
|
|
718
|
+
if (!filePath || !existsSync(filePath)) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
722
|
+
try {
|
|
723
|
+
const allElements = JSON.parse(content);
|
|
724
|
+
const element_key = selectors?.element_key;
|
|
725
|
+
if (element_key && allElements[element_key]) {
|
|
726
|
+
return allElements[element_key];
|
|
727
|
+
}
|
|
728
|
+
for (const elementKey in allElements) {
|
|
729
|
+
const element = allElements[elementKey];
|
|
730
|
+
let foundStrategy = null;
|
|
731
|
+
for (const key in element) {
|
|
732
|
+
if (key === "strategy") {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const locators = element[key];
|
|
736
|
+
if (!locators || !locators.length) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
for (const locator of locators) {
|
|
740
|
+
delete locator.score;
|
|
741
|
+
}
|
|
742
|
+
if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
|
|
743
|
+
foundStrategy = key;
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (foundStrategy) {
|
|
748
|
+
return element;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
console.error("Error parsing locators from file: " + filePath, error);
|
|
754
|
+
}
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
621
757
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
622
758
|
if (!timeout) {
|
|
623
759
|
timeout = 30000;
|
|
624
760
|
}
|
|
761
|
+
let element = null;
|
|
762
|
+
let allStrategyLocators = null;
|
|
763
|
+
let selectedStrategy = null;
|
|
764
|
+
if (this.tryAllStrategies) {
|
|
765
|
+
allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
|
|
766
|
+
selectedStrategy = allStrategyLocators?.strategy;
|
|
767
|
+
}
|
|
625
768
|
for (let i = 0; i < 3; i++) {
|
|
626
769
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
627
770
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
628
771
|
let selector = selectors.locators[j];
|
|
629
772
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
630
773
|
}
|
|
631
|
-
|
|
774
|
+
if (this.tryAllStrategies && selectedStrategy) {
|
|
775
|
+
const strategyLocators = allStrategyLocators[selectedStrategy];
|
|
776
|
+
let err;
|
|
777
|
+
if (strategyLocators && strategyLocators.length) {
|
|
778
|
+
try {
|
|
779
|
+
selectors.locators = strategyLocators;
|
|
780
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
781
|
+
info.selectedStrategy = selectedStrategy;
|
|
782
|
+
info.log += "element found using strategy " + selectedStrategy + "\n";
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
err = error;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (!element) {
|
|
789
|
+
for (const key in allStrategyLocators) {
|
|
790
|
+
if (key === "strategy" || key === selectedStrategy) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const strategyLocators = allStrategyLocators[key];
|
|
794
|
+
if (strategyLocators && strategyLocators.length) {
|
|
795
|
+
try {
|
|
796
|
+
info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
|
|
797
|
+
selectors.locators = strategyLocators;
|
|
798
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
799
|
+
err = null;
|
|
800
|
+
info.selectedStrategy = key;
|
|
801
|
+
info.log += "element found using strategy " + key + "\n";
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
err = error;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (err) {
|
|
811
|
+
throw err;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
816
|
+
}
|
|
632
817
|
if (!element.rerun) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
818
|
+
let newElementSelector = "";
|
|
819
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
|
|
820
|
+
const cssSelector = await element.evaluate((el) => {
|
|
821
|
+
function getCssSelector(el) {
|
|
822
|
+
if (!el || el.nodeType !== 1 || el === document.body)
|
|
823
|
+
return el.tagName.toLowerCase();
|
|
824
|
+
const parent = el.parentElement;
|
|
825
|
+
const tag = el.tagName.toLowerCase();
|
|
826
|
+
// Find the index of the element among its siblings of the same tag
|
|
827
|
+
let index = 1;
|
|
828
|
+
for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
829
|
+
if (sibling.tagName === el.tagName) {
|
|
830
|
+
index++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// Use nth-child if necessary (i.e., if there's more than one of the same tag)
|
|
834
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
835
|
+
const needsNthChild = siblings.length > 1;
|
|
836
|
+
const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
|
|
837
|
+
return getCssSelector(parent) + " > " + selector;
|
|
838
|
+
}
|
|
839
|
+
const cssSelector = getCssSelector(el);
|
|
840
|
+
return cssSelector;
|
|
841
|
+
});
|
|
842
|
+
newElementSelector = cssSelector;
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
846
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
|
|
847
|
+
const dataAttribute = "data-blinq-id";
|
|
848
|
+
await element.evaluate((el, [dataAttribute, randomToken]) => {
|
|
849
|
+
el.setAttribute(dataAttribute, randomToken);
|
|
850
|
+
}, [dataAttribute, randomToken]);
|
|
851
|
+
newElementSelector = `[${dataAttribute}="${randomToken}"]`;
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
// the default case just return the located element
|
|
855
|
+
// will not work for click and type if the locator is placeholder and the placeholder change due to the click event
|
|
856
|
+
return element;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
640
859
|
const scope = element._frame ?? element.page();
|
|
641
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
642
860
|
let prefixSelector = "";
|
|
643
861
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
644
862
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
645
863
|
if (frameSelectorIndex !== -1) {
|
|
646
864
|
// remove everything after the >> internal:control=enter-frame
|
|
647
865
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
648
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
866
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
649
867
|
}
|
|
650
868
|
// if (element?._frame?._selector) {
|
|
651
869
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
652
870
|
// }
|
|
653
871
|
const newSelector = prefixSelector + newElementSelector;
|
|
654
|
-
return scope.locator(newSelector);
|
|
872
|
+
return scope.locator(newSelector).first();
|
|
655
873
|
}
|
|
656
874
|
}
|
|
657
875
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -672,7 +890,7 @@ class StableBrowser {
|
|
|
672
890
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
673
891
|
let frameLocator = frame.selectors[i];
|
|
674
892
|
if (frameLocator.css) {
|
|
675
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
893
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
676
894
|
if (frameLocator.index) {
|
|
677
895
|
testframescope = framescope.nth(frameLocator.index);
|
|
678
896
|
}
|
|
@@ -684,7 +902,7 @@ class StableBrowser {
|
|
|
684
902
|
break;
|
|
685
903
|
}
|
|
686
904
|
catch (error) {
|
|
687
|
-
console.error("frame not found " + frameLocator.css);
|
|
905
|
+
// console.error("frame not found " + frameLocator.css);
|
|
688
906
|
}
|
|
689
907
|
}
|
|
690
908
|
}
|
|
@@ -750,6 +968,14 @@ class StableBrowser {
|
|
|
750
968
|
});
|
|
751
969
|
}
|
|
752
970
|
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
971
|
+
if (selectors.locators && Array.isArray(selectors.locators)) {
|
|
972
|
+
selectors.locators.forEach((locator) => {
|
|
973
|
+
locator.index = locator.index ?? 0;
|
|
974
|
+
if (locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
975
|
+
locator.css = locator.css + " >> visible=true";
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
753
979
|
if (!info) {
|
|
754
980
|
info = {};
|
|
755
981
|
info.failCause = {};
|
|
@@ -762,7 +988,6 @@ class StableBrowser {
|
|
|
762
988
|
let locatorsCount = 0;
|
|
763
989
|
let lazy_scroll = false;
|
|
764
990
|
//let arrayMode = Array.isArray(selectors);
|
|
765
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
766
991
|
let selectorsLocators = null;
|
|
767
992
|
selectorsLocators = selectors.locators;
|
|
768
993
|
// group selectors by priority
|
|
@@ -790,6 +1015,7 @@ class StableBrowser {
|
|
|
790
1015
|
let highPriorityOnly = true;
|
|
791
1016
|
let visibleOnly = true;
|
|
792
1017
|
while (true) {
|
|
1018
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
793
1019
|
locatorsCount = 0;
|
|
794
1020
|
let result = [];
|
|
795
1021
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -905,9 +1131,13 @@ class StableBrowser {
|
|
|
905
1131
|
}
|
|
906
1132
|
}
|
|
907
1133
|
if (foundLocators.length === 1) {
|
|
1134
|
+
let box = null;
|
|
1135
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1136
|
+
box = await foundLocators[0].boundingBox();
|
|
1137
|
+
}
|
|
908
1138
|
result.foundElements.push({
|
|
909
1139
|
locator: foundLocators[0],
|
|
910
|
-
box:
|
|
1140
|
+
box: box,
|
|
911
1141
|
unique: true,
|
|
912
1142
|
});
|
|
913
1143
|
result.locatorIndex = i;
|
|
@@ -1062,11 +1292,16 @@ class StableBrowser {
|
|
|
1062
1292
|
operation: "click",
|
|
1063
1293
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1064
1294
|
};
|
|
1295
|
+
check_performance("click_all ***", this.context, true);
|
|
1065
1296
|
try {
|
|
1297
|
+
check_performance("click_preCommand", this.context, true);
|
|
1066
1298
|
await _preCommand(state, this);
|
|
1299
|
+
check_performance("click_preCommand", this.context, false);
|
|
1067
1300
|
await performAction("click", state.element, options, this, state, _params);
|
|
1068
|
-
if (!this.fastMode) {
|
|
1069
|
-
|
|
1301
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1302
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1303
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1304
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1070
1305
|
}
|
|
1071
1306
|
return state.info;
|
|
1072
1307
|
}
|
|
@@ -1074,7 +1309,13 @@ class StableBrowser {
|
|
|
1074
1309
|
await _commandError(state, e, this);
|
|
1075
1310
|
}
|
|
1076
1311
|
finally {
|
|
1312
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1077
1313
|
await _commandFinally(state, this);
|
|
1314
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1315
|
+
check_performance("click_all ***", this.context, false);
|
|
1316
|
+
if (this.context.profile) {
|
|
1317
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1318
|
+
}
|
|
1078
1319
|
}
|
|
1079
1320
|
}
|
|
1080
1321
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1166,7 +1407,7 @@ class StableBrowser {
|
|
|
1166
1407
|
}
|
|
1167
1408
|
}
|
|
1168
1409
|
}
|
|
1169
|
-
await this.waitForPageLoad();
|
|
1410
|
+
//await this.waitForPageLoad();
|
|
1170
1411
|
return state.info;
|
|
1171
1412
|
}
|
|
1172
1413
|
catch (e) {
|
|
@@ -1192,7 +1433,7 @@ class StableBrowser {
|
|
|
1192
1433
|
await _preCommand(state, this);
|
|
1193
1434
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1194
1435
|
await _screenshot(state, this);
|
|
1195
|
-
await this.waitForPageLoad();
|
|
1436
|
+
//await this.waitForPageLoad();
|
|
1196
1437
|
return state.info;
|
|
1197
1438
|
}
|
|
1198
1439
|
catch (e) {
|
|
@@ -1228,7 +1469,7 @@ class StableBrowser {
|
|
|
1228
1469
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1229
1470
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1230
1471
|
}
|
|
1231
|
-
await this.waitForPageLoad();
|
|
1472
|
+
//await this.waitForPageLoad();
|
|
1232
1473
|
return state.info;
|
|
1233
1474
|
}
|
|
1234
1475
|
catch (e) {
|
|
@@ -1414,6 +1655,14 @@ class StableBrowser {
|
|
|
1414
1655
|
}
|
|
1415
1656
|
try {
|
|
1416
1657
|
await _preCommand(state, this);
|
|
1658
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1659
|
+
// tag the element
|
|
1660
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1661
|
+
// use attribute and not id
|
|
1662
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1663
|
+
el.setAttribute(attrName, "");
|
|
1664
|
+
return `[${attrName}]`;
|
|
1665
|
+
}, randomToken);
|
|
1417
1666
|
state.info.value = _value;
|
|
1418
1667
|
if (!options.press) {
|
|
1419
1668
|
try {
|
|
@@ -1439,6 +1688,25 @@ class StableBrowser {
|
|
|
1439
1688
|
}
|
|
1440
1689
|
}
|
|
1441
1690
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1691
|
+
// check if the element exist after the click (no wait)
|
|
1692
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1693
|
+
if (count === 0) {
|
|
1694
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1695
|
+
const scope = state.element._frame ?? element.page();
|
|
1696
|
+
let prefixSelector = "";
|
|
1697
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1698
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1699
|
+
if (frameSelectorIndex !== -1) {
|
|
1700
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1701
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1702
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1703
|
+
}
|
|
1704
|
+
// if (element?._frame?._selector) {
|
|
1705
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1706
|
+
// }
|
|
1707
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1708
|
+
state.element = scope.locator(newSelector).first();
|
|
1709
|
+
}
|
|
1442
1710
|
const valueSegment = state.value.split("&&");
|
|
1443
1711
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1444
1712
|
if (i > 0) {
|
|
@@ -1510,8 +1778,8 @@ class StableBrowser {
|
|
|
1510
1778
|
if (enter) {
|
|
1511
1779
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1512
1780
|
await this.page.keyboard.press("Enter");
|
|
1781
|
+
await this.waitForPageLoad();
|
|
1513
1782
|
}
|
|
1514
|
-
await this.waitForPageLoad();
|
|
1515
1783
|
return state.info;
|
|
1516
1784
|
}
|
|
1517
1785
|
catch (e) {
|
|
@@ -2425,47 +2693,54 @@ class StableBrowser {
|
|
|
2425
2693
|
}
|
|
2426
2694
|
state.info.value = val;
|
|
2427
2695
|
let regex;
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
const
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
state.info.failCause.assertionFailed = true;
|
|
2443
|
-
state.info.failCause.lastError = errorMessage;
|
|
2444
|
-
throw new Error(errorMessage);
|
|
2445
|
-
}
|
|
2696
|
+
state.info.value = val;
|
|
2697
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2698
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2699
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2700
|
+
let matchPassed = false;
|
|
2701
|
+
if (isRegex) {
|
|
2702
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2703
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2704
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2705
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2706
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2707
|
+
const regex = new RegExp(patternBody, flags);
|
|
2708
|
+
state.info.regex = true;
|
|
2709
|
+
matchPassed = regex.test(val);
|
|
2446
2710
|
}
|
|
2447
2711
|
else {
|
|
2448
|
-
//
|
|
2449
|
-
const
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
// Check if all expected lines are present in the actual lines
|
|
2453
|
-
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2454
|
-
if (!isPart) {
|
|
2455
|
-
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2456
|
-
state.info.failCause.assertionFailed = true;
|
|
2457
|
-
state.info.failCause.lastError = errorMessage;
|
|
2458
|
-
throw new Error(errorMessage);
|
|
2459
|
-
}
|
|
2712
|
+
// Fallback: treat as literal
|
|
2713
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2714
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2715
|
+
matchPassed = regex.test(val);
|
|
2460
2716
|
}
|
|
2461
2717
|
}
|
|
2718
|
+
else if (isContains) {
|
|
2719
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2720
|
+
matchPassed = val.includes(containsValue);
|
|
2721
|
+
}
|
|
2722
|
+
else if (isExact) {
|
|
2723
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2724
|
+
matchPassed = val === exactValue;
|
|
2725
|
+
}
|
|
2726
|
+
else if (property === "innerText") {
|
|
2727
|
+
// Default innerText logic
|
|
2728
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2729
|
+
const valLines = val.split("\n");
|
|
2730
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2731
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2732
|
+
}
|
|
2462
2733
|
else {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2734
|
+
// Fallback exact or loose match
|
|
2735
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2736
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2737
|
+
matchPassed = regex.test(val);
|
|
2738
|
+
}
|
|
2739
|
+
if (!matchPassed) {
|
|
2740
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2741
|
+
state.info.failCause.assertionFailed = true;
|
|
2742
|
+
state.info.failCause.lastError = errorMessage;
|
|
2743
|
+
throw new Error(errorMessage);
|
|
2469
2744
|
}
|
|
2470
2745
|
return state.info;
|
|
2471
2746
|
}
|
|
@@ -2476,6 +2751,133 @@ class StableBrowser {
|
|
|
2476
2751
|
await _commandFinally(state, this);
|
|
2477
2752
|
}
|
|
2478
2753
|
}
|
|
2754
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2755
|
+
// Convert timeout from seconds to milliseconds
|
|
2756
|
+
const timeoutMs = timeout * 1000;
|
|
2757
|
+
const state = {
|
|
2758
|
+
selectors,
|
|
2759
|
+
_params,
|
|
2760
|
+
condition,
|
|
2761
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2762
|
+
options,
|
|
2763
|
+
world,
|
|
2764
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2765
|
+
highlight: true,
|
|
2766
|
+
screenshot: true,
|
|
2767
|
+
text: `Conditional wait for element`,
|
|
2768
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2769
|
+
operation: "conditionalWait",
|
|
2770
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2771
|
+
allowDisabled: true,
|
|
2772
|
+
info: {},
|
|
2773
|
+
};
|
|
2774
|
+
state.options ??= { timeout: timeoutMs };
|
|
2775
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2776
|
+
const startTime = Date.now();
|
|
2777
|
+
let conditionMet = false;
|
|
2778
|
+
let currentValue = null;
|
|
2779
|
+
let lastError = null;
|
|
2780
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2781
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2782
|
+
const elapsedTime = Date.now() - startTime;
|
|
2783
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2784
|
+
try {
|
|
2785
|
+
// Try to execute _preCommand (element location)
|
|
2786
|
+
await _preCommand(state, this);
|
|
2787
|
+
// If _preCommand succeeds, start condition checking
|
|
2788
|
+
const checkCondition = async () => {
|
|
2789
|
+
try {
|
|
2790
|
+
switch (condition.toLowerCase()) {
|
|
2791
|
+
case "checked":
|
|
2792
|
+
currentValue = await state.element.isChecked();
|
|
2793
|
+
return currentValue === true;
|
|
2794
|
+
case "unchecked":
|
|
2795
|
+
currentValue = await state.element.isChecked();
|
|
2796
|
+
return currentValue === false;
|
|
2797
|
+
case "visible":
|
|
2798
|
+
currentValue = await state.element.isVisible();
|
|
2799
|
+
return currentValue === true;
|
|
2800
|
+
case "hidden":
|
|
2801
|
+
currentValue = await state.element.isVisible();
|
|
2802
|
+
return currentValue === false;
|
|
2803
|
+
case "enabled":
|
|
2804
|
+
currentValue = await state.element.isDisabled();
|
|
2805
|
+
return currentValue === false;
|
|
2806
|
+
case "disabled":
|
|
2807
|
+
currentValue = await state.element.isDisabled();
|
|
2808
|
+
return currentValue === true;
|
|
2809
|
+
case "editable":
|
|
2810
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2811
|
+
currentValue = await state.element.isContentEditable();
|
|
2812
|
+
return currentValue === true;
|
|
2813
|
+
default:
|
|
2814
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2815
|
+
state.info.success = false;
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
catch (error) {
|
|
2820
|
+
// Don't throw here, just return false to continue retrying
|
|
2821
|
+
return false;
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
// Inner loop for condition checking (once element is located)
|
|
2825
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2826
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2827
|
+
conditionMet = await checkCondition();
|
|
2828
|
+
if (conditionMet) {
|
|
2829
|
+
break;
|
|
2830
|
+
}
|
|
2831
|
+
// Check if we still have time for another attempt
|
|
2832
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2833
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2834
|
+
}
|
|
2835
|
+
else {
|
|
2836
|
+
break;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
// If we got here and condition is met, break out of main loop
|
|
2840
|
+
if (conditionMet) {
|
|
2841
|
+
break;
|
|
2842
|
+
}
|
|
2843
|
+
// If condition not met but no exception, we've timed out
|
|
2844
|
+
break;
|
|
2845
|
+
}
|
|
2846
|
+
catch (e) {
|
|
2847
|
+
lastError = e;
|
|
2848
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2849
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2850
|
+
// Check if we have enough time left to retry
|
|
2851
|
+
if (timeLeft > 100) {
|
|
2852
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2853
|
+
}
|
|
2854
|
+
else {
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2860
|
+
state.info = {
|
|
2861
|
+
success: conditionMet,
|
|
2862
|
+
conditionMet,
|
|
2863
|
+
actualWaitTime,
|
|
2864
|
+
currentValue,
|
|
2865
|
+
lastError: lastError?.message || null,
|
|
2866
|
+
message: conditionMet
|
|
2867
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2868
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2869
|
+
};
|
|
2870
|
+
if (lastError) {
|
|
2871
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2872
|
+
}
|
|
2873
|
+
try {
|
|
2874
|
+
await _commandFinally(state, this);
|
|
2875
|
+
}
|
|
2876
|
+
catch (finallyError) {
|
|
2877
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2878
|
+
}
|
|
2879
|
+
return state.info;
|
|
2880
|
+
}
|
|
2479
2881
|
async extractEmailData(emailAddress, options, world) {
|
|
2480
2882
|
if (!emailAddress) {
|
|
2481
2883
|
throw new Error("email address is null");
|
|
@@ -3072,6 +3474,8 @@ class StableBrowser {
|
|
|
3072
3474
|
operation: "verify_text_with_relation",
|
|
3073
3475
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3074
3476
|
};
|
|
3477
|
+
const cmdStartTime = Date.now();
|
|
3478
|
+
let cmdEndTime = null;
|
|
3075
3479
|
const timeout = this._getFindElementTimeout(options);
|
|
3076
3480
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3077
3481
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3107,6 +3511,17 @@ class StableBrowser {
|
|
|
3107
3511
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3108
3512
|
continue;
|
|
3109
3513
|
}
|
|
3514
|
+
else {
|
|
3515
|
+
cmdEndTime = Date.now();
|
|
3516
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3517
|
+
if (foundAncore) {
|
|
3518
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3519
|
+
}
|
|
3520
|
+
else {
|
|
3521
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3110
3525
|
try {
|
|
3111
3526
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3112
3527
|
foundAncore = true;
|
|
@@ -3245,7 +3660,7 @@ class StableBrowser {
|
|
|
3245
3660
|
Object.assign(e, { info: info });
|
|
3246
3661
|
error = e;
|
|
3247
3662
|
// throw e;
|
|
3248
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3663
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3249
3664
|
}
|
|
3250
3665
|
finally {
|
|
3251
3666
|
const endTime = Date.now();
|
|
@@ -3594,6 +4009,22 @@ class StableBrowser {
|
|
|
3594
4009
|
}
|
|
3595
4010
|
}
|
|
3596
4011
|
async waitForPageLoad(options = {}, world = null) {
|
|
4012
|
+
// try {
|
|
4013
|
+
// let currentPagePath = null;
|
|
4014
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4015
|
+
// if (this.latestPagePath) {
|
|
4016
|
+
// // get the currect page path and compare with the latest page path
|
|
4017
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4018
|
+
// // if the page path is the same, do not wait for page load
|
|
4019
|
+
// console.log("No page change: " + currentPagePath);
|
|
4020
|
+
// return;
|
|
4021
|
+
// }
|
|
4022
|
+
// }
|
|
4023
|
+
// this.latestPagePath = currentPagePath;
|
|
4024
|
+
// } catch (e) {
|
|
4025
|
+
// console.debug("Error getting current page path: ", e);
|
|
4026
|
+
// }
|
|
4027
|
+
//console.log("Waiting for page load");
|
|
3597
4028
|
let timeout = this._getLoadTimeout(options);
|
|
3598
4029
|
const promiseArray = [];
|
|
3599
4030
|
// let waitForNetworkIdle = true;
|
|
@@ -3626,10 +4057,12 @@ class StableBrowser {
|
|
|
3626
4057
|
else if (e.label === "domcontentloaded") {
|
|
3627
4058
|
console.log("waited for the domcontent loaded timeout");
|
|
3628
4059
|
}
|
|
3629
|
-
console.log(".");
|
|
3630
4060
|
}
|
|
3631
4061
|
finally {
|
|
3632
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4062
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4063
|
+
if (options && !options.noSleep) {
|
|
4064
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4065
|
+
}
|
|
3633
4066
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3634
4067
|
const endTime = Date.now();
|
|
3635
4068
|
_reportToWorld(world, {
|
|
@@ -3670,7 +4103,6 @@ class StableBrowser {
|
|
|
3670
4103
|
await this.page.close();
|
|
3671
4104
|
}
|
|
3672
4105
|
catch (e) {
|
|
3673
|
-
console.log(".");
|
|
3674
4106
|
await _commandError(state, e, this);
|
|
3675
4107
|
}
|
|
3676
4108
|
finally {
|
|
@@ -3684,7 +4116,7 @@ class StableBrowser {
|
|
|
3684
4116
|
}
|
|
3685
4117
|
operation = options.operation;
|
|
3686
4118
|
// validate operation is one of the supported operations
|
|
3687
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4119
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3688
4120
|
throw new Error("operation is not supported");
|
|
3689
4121
|
}
|
|
3690
4122
|
const state = {
|
|
@@ -3753,6 +4185,17 @@ class StableBrowser {
|
|
|
3753
4185
|
state.element = results[0];
|
|
3754
4186
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3755
4187
|
break;
|
|
4188
|
+
case "hover":
|
|
4189
|
+
if (!options.css) {
|
|
4190
|
+
throw new Error("css is not defined");
|
|
4191
|
+
}
|
|
4192
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4193
|
+
if (result1.length === 0) {
|
|
4194
|
+
throw new Error(`Element not found in cell area`);
|
|
4195
|
+
}
|
|
4196
|
+
state.element = result1[0];
|
|
4197
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4198
|
+
break;
|
|
3756
4199
|
default:
|
|
3757
4200
|
throw new Error("operation is not supported");
|
|
3758
4201
|
}
|
|
@@ -3785,7 +4228,6 @@ class StableBrowser {
|
|
|
3785
4228
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3786
4229
|
}
|
|
3787
4230
|
catch (e) {
|
|
3788
|
-
console.log(".");
|
|
3789
4231
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3790
4232
|
}
|
|
3791
4233
|
finally {
|
|
@@ -3823,7 +4265,6 @@ class StableBrowser {
|
|
|
3823
4265
|
await this.page.reload();
|
|
3824
4266
|
}
|
|
3825
4267
|
catch (e) {
|
|
3826
|
-
console.log(".");
|
|
3827
4268
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3828
4269
|
}
|
|
3829
4270
|
finally {
|
|
@@ -3867,6 +4308,10 @@ class StableBrowser {
|
|
|
3867
4308
|
}
|
|
3868
4309
|
}
|
|
3869
4310
|
async beforeScenario(world, scenario) {
|
|
4311
|
+
if (world && world.attach) {
|
|
4312
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4313
|
+
}
|
|
4314
|
+
this.context.loadedRoutes = null;
|
|
3870
4315
|
this.beforeScenarioCalled = true;
|
|
3871
4316
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3872
4317
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3896,8 +4341,10 @@ class StableBrowser {
|
|
|
3896
4341
|
}
|
|
3897
4342
|
async afterScenario(world, scenario) { }
|
|
3898
4343
|
async beforeStep(world, step) {
|
|
4344
|
+
this.stepTags = [];
|
|
3899
4345
|
if (!this.beforeScenarioCalled) {
|
|
3900
4346
|
this.beforeScenario(world, step);
|
|
4347
|
+
this.context.loadedRoutes = null;
|
|
3901
4348
|
}
|
|
3902
4349
|
if (this.stepIndex === undefined) {
|
|
3903
4350
|
this.stepIndex = 0;
|
|
@@ -3907,7 +4354,12 @@ class StableBrowser {
|
|
|
3907
4354
|
}
|
|
3908
4355
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3909
4356
|
this.stepName = step.pickleStep.text;
|
|
3910
|
-
|
|
4357
|
+
let printableStepName = this.stepName;
|
|
4358
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4359
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4360
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4361
|
+
});
|
|
4362
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
3911
4363
|
}
|
|
3912
4364
|
else if (step && step.text) {
|
|
3913
4365
|
this.stepName = step.text;
|
|
@@ -3922,7 +4374,10 @@ class StableBrowser {
|
|
|
3922
4374
|
}
|
|
3923
4375
|
if (this.initSnapshotTaken === false) {
|
|
3924
4376
|
this.initSnapshotTaken = true;
|
|
3925
|
-
if (world &&
|
|
4377
|
+
if (world &&
|
|
4378
|
+
world.attach &&
|
|
4379
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4380
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
3926
4381
|
const snapshot = await this.getAriaSnapshot();
|
|
3927
4382
|
if (snapshot) {
|
|
3928
4383
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -3930,7 +4385,12 @@ class StableBrowser {
|
|
|
3930
4385
|
}
|
|
3931
4386
|
}
|
|
3932
4387
|
this.context.routeResults = null;
|
|
3933
|
-
|
|
4388
|
+
this.context.loadedRoutes = null;
|
|
4389
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4390
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4391
|
+
}
|
|
4392
|
+
setStepTags(tags) {
|
|
4393
|
+
this.stepTags = tags;
|
|
3934
4394
|
}
|
|
3935
4395
|
async getAriaSnapshot() {
|
|
3936
4396
|
try {
|
|
@@ -3950,12 +4410,18 @@ class StableBrowser {
|
|
|
3950
4410
|
try {
|
|
3951
4411
|
// Ensure frame is attached and has body
|
|
3952
4412
|
const body = frame.locator("body");
|
|
3953
|
-
await body.waitFor({ timeout:
|
|
4413
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
3954
4414
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4415
|
+
if (!snapshot) {
|
|
4416
|
+
continue;
|
|
4417
|
+
}
|
|
3955
4418
|
content.push(`- frame: ${i}`);
|
|
3956
4419
|
content.push(snapshot);
|
|
3957
4420
|
}
|
|
3958
|
-
catch (innerErr) {
|
|
4421
|
+
catch (innerErr) {
|
|
4422
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4423
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4424
|
+
}
|
|
3959
4425
|
}
|
|
3960
4426
|
return content.join("\n");
|
|
3961
4427
|
}
|
|
@@ -4027,7 +4493,11 @@ class StableBrowser {
|
|
|
4027
4493
|
if (this.context) {
|
|
4028
4494
|
this.context.examplesRow = null;
|
|
4029
4495
|
}
|
|
4030
|
-
if (world &&
|
|
4496
|
+
if (world &&
|
|
4497
|
+
world.attach &&
|
|
4498
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4499
|
+
!this.fastMode &&
|
|
4500
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4031
4501
|
const snapshot = await this.getAriaSnapshot();
|
|
4032
4502
|
if (snapshot) {
|
|
4033
4503
|
const obj = {};
|
|
@@ -4035,6 +4505,11 @@ class StableBrowser {
|
|
|
4035
4505
|
}
|
|
4036
4506
|
}
|
|
4037
4507
|
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4508
|
+
if (this.context.routeResults) {
|
|
4509
|
+
if (world && world.attach) {
|
|
4510
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4038
4513
|
if (!process.env.TEMP_RUN) {
|
|
4039
4514
|
const state = {
|
|
4040
4515
|
world,
|
|
@@ -4058,6 +4533,13 @@ class StableBrowser {
|
|
|
4058
4533
|
await _commandFinally(state, this);
|
|
4059
4534
|
}
|
|
4060
4535
|
}
|
|
4536
|
+
networkAfterStep(this.stepName, this.context);
|
|
4537
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4538
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4539
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4540
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4061
4543
|
}
|
|
4062
4544
|
}
|
|
4063
4545
|
function createTimedPromise(promise, label) {
|