automation_model 1.0.750-dev → 1.0.750-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 +10 -6
- 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 +83 -0
- package/lib/route.js +682 -0
- package/lib/route.js.map +1 -0
- 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 -86
- 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 +30 -8
- package/lib/utils.js.map +1 -1
- package/package.json +19 -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,16 +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";
|
|
30
|
+
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
28
32
|
export const Types = {
|
|
29
33
|
CLICK: "click_element",
|
|
30
34
|
WAIT_ELEMENT: "wait_element",
|
|
31
|
-
NAVIGATE: "navigate",
|
|
35
|
+
NAVIGATE: "navigate",
|
|
36
|
+
GO_BACK: "go_back",
|
|
37
|
+
GO_FORWARD: "go_forward",
|
|
32
38
|
FILL: "fill_element",
|
|
33
39
|
EXECUTE: "execute_page_method", //
|
|
34
40
|
OPEN: "open_environment", //
|
|
@@ -41,6 +47,7 @@ export const Types = {
|
|
|
41
47
|
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
42
48
|
ANALYZE_TABLE: "analyze_table",
|
|
43
49
|
SELECT: "select_combobox", //
|
|
50
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
44
51
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
45
52
|
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
46
53
|
TYPE_PRESS: "type_press",
|
|
@@ -59,15 +66,15 @@ export const Types = {
|
|
|
59
66
|
SET_INPUT: "set_input",
|
|
60
67
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
61
68
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
62
|
-
VERIFY_PROPERTY: "verify_element_property",
|
|
63
69
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
64
70
|
BRUNO: "bruno",
|
|
65
|
-
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
66
71
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
67
72
|
SET_INPUT_FILES: "set_input_files",
|
|
73
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
68
74
|
REPORT_COMMAND: "report_command",
|
|
69
75
|
STEP_COMPLETE: "step_complete",
|
|
70
76
|
SLEEP: "sleep",
|
|
77
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
71
78
|
};
|
|
72
79
|
export const apps = {};
|
|
73
80
|
const formatElementName = (elementName) => {
|
|
@@ -80,6 +87,7 @@ class StableBrowser {
|
|
|
80
87
|
context;
|
|
81
88
|
world;
|
|
82
89
|
fastMode;
|
|
90
|
+
stepTags;
|
|
83
91
|
project_path = null;
|
|
84
92
|
webLogFile = null;
|
|
85
93
|
networkLogger = null;
|
|
@@ -88,13 +96,15 @@ class StableBrowser {
|
|
|
88
96
|
tags = null;
|
|
89
97
|
isRecording = false;
|
|
90
98
|
initSnapshotTaken = false;
|
|
91
|
-
|
|
99
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
100
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
92
101
|
this.browser = browser;
|
|
93
102
|
this.page = page;
|
|
94
103
|
this.logger = logger;
|
|
95
104
|
this.context = context;
|
|
96
105
|
this.world = world;
|
|
97
106
|
this.fastMode = fastMode;
|
|
107
|
+
this.stepTags = stepTags;
|
|
98
108
|
if (!this.logger) {
|
|
99
109
|
this.logger = console;
|
|
100
110
|
}
|
|
@@ -127,6 +137,7 @@ class StableBrowser {
|
|
|
127
137
|
this.fastMode = true;
|
|
128
138
|
}
|
|
129
139
|
if (process.env.FAST_MODE === "true") {
|
|
140
|
+
// console.log("Fast mode enabled from environment variable");
|
|
130
141
|
this.fastMode = true;
|
|
131
142
|
}
|
|
132
143
|
if (process.env.FAST_MODE === "false") {
|
|
@@ -169,6 +180,7 @@ class StableBrowser {
|
|
|
169
180
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
170
181
|
registerDownloadEvent(this.page, this.world, context);
|
|
171
182
|
page.on("close", async () => {
|
|
183
|
+
// return if browser context is already closed
|
|
172
184
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
173
185
|
this.context.pages.pop();
|
|
174
186
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -178,7 +190,12 @@ class StableBrowser {
|
|
|
178
190
|
console.log("Switched to page " + title);
|
|
179
191
|
}
|
|
180
192
|
catch (error) {
|
|
181
|
-
|
|
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
|
+
}
|
|
182
199
|
}
|
|
183
200
|
}
|
|
184
201
|
});
|
|
@@ -187,7 +204,12 @@ class StableBrowser {
|
|
|
187
204
|
console.log("Switch page: " + (await page.title()));
|
|
188
205
|
}
|
|
189
206
|
catch (e) {
|
|
190
|
-
|
|
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
|
+
}
|
|
191
213
|
}
|
|
192
214
|
context.pageLoading.status = false;
|
|
193
215
|
}.bind(this));
|
|
@@ -215,7 +237,7 @@ class StableBrowser {
|
|
|
215
237
|
if (newContextCreated) {
|
|
216
238
|
this.registerEventListeners(this.context);
|
|
217
239
|
await this.goto(this.context.environment.baseUrl);
|
|
218
|
-
if (!this.fastMode) {
|
|
240
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
219
241
|
await this.waitForPageLoad();
|
|
220
242
|
}
|
|
221
243
|
}
|
|
@@ -343,6 +365,64 @@ class StableBrowser {
|
|
|
343
365
|
await _commandFinally(state, this);
|
|
344
366
|
}
|
|
345
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
|
+
}
|
|
346
426
|
async _getLocator(locator, scope, _params) {
|
|
347
427
|
locator = _fixLocatorUsingParams(locator, _params);
|
|
348
428
|
// locator = await this._replaceWithLocalData(locator);
|
|
@@ -441,12 +521,6 @@ class StableBrowser {
|
|
|
441
521
|
if (!el.setAttribute) {
|
|
442
522
|
el = el.parentElement;
|
|
443
523
|
}
|
|
444
|
-
// remove any attributes start with data-blinq-id
|
|
445
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
446
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
447
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
448
|
-
// }
|
|
449
|
-
// }
|
|
450
524
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
451
525
|
return true;
|
|
452
526
|
}, [tag1, randomToken]))) {
|
|
@@ -468,14 +542,13 @@ class StableBrowser {
|
|
|
468
542
|
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
469
543
|
}
|
|
470
544
|
let locatorSearch = selectorHierarchy[index];
|
|
471
|
-
let originalLocatorSearch = "";
|
|
472
545
|
try {
|
|
473
|
-
|
|
474
|
-
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
546
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
475
547
|
}
|
|
476
548
|
catch (e) {
|
|
477
549
|
console.error(e);
|
|
478
550
|
}
|
|
551
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
479
552
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
480
553
|
let locator = null;
|
|
481
554
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
@@ -617,40 +690,186 @@ class StableBrowser {
|
|
|
617
690
|
}
|
|
618
691
|
return { rerun: false };
|
|
619
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
|
+
}
|
|
620
757
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
621
758
|
if (!timeout) {
|
|
622
759
|
timeout = 30000;
|
|
623
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
|
+
}
|
|
624
768
|
for (let i = 0; i < 3; i++) {
|
|
625
769
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
626
770
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
627
771
|
let selector = selectors.locators[j];
|
|
628
772
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
629
773
|
}
|
|
630
|
-
|
|
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
|
+
}
|
|
631
817
|
if (!element.rerun) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
+
}
|
|
639
859
|
const scope = element._frame ?? element.page();
|
|
640
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
641
860
|
let prefixSelector = "";
|
|
642
861
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
643
862
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
644
863
|
if (frameSelectorIndex !== -1) {
|
|
645
864
|
// remove everything after the >> internal:control=enter-frame
|
|
646
865
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
647
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
866
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
648
867
|
}
|
|
649
868
|
// if (element?._frame?._selector) {
|
|
650
869
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
651
870
|
// }
|
|
652
871
|
const newSelector = prefixSelector + newElementSelector;
|
|
653
|
-
return scope.locator(newSelector);
|
|
872
|
+
return scope.locator(newSelector).first();
|
|
654
873
|
}
|
|
655
874
|
}
|
|
656
875
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -671,7 +890,7 @@ class StableBrowser {
|
|
|
671
890
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
672
891
|
let frameLocator = frame.selectors[i];
|
|
673
892
|
if (frameLocator.css) {
|
|
674
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
893
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
675
894
|
if (frameLocator.index) {
|
|
676
895
|
testframescope = framescope.nth(frameLocator.index);
|
|
677
896
|
}
|
|
@@ -683,7 +902,7 @@ class StableBrowser {
|
|
|
683
902
|
break;
|
|
684
903
|
}
|
|
685
904
|
catch (error) {
|
|
686
|
-
console.error("frame not found " + frameLocator.css);
|
|
905
|
+
// console.error("frame not found " + frameLocator.css);
|
|
687
906
|
}
|
|
688
907
|
}
|
|
689
908
|
}
|
|
@@ -749,6 +968,11 @@ class StableBrowser {
|
|
|
749
968
|
});
|
|
750
969
|
}
|
|
751
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
|
+
});
|
|
975
|
+
}
|
|
752
976
|
if (!info) {
|
|
753
977
|
info = {};
|
|
754
978
|
info.failCause = {};
|
|
@@ -761,7 +985,6 @@ class StableBrowser {
|
|
|
761
985
|
let locatorsCount = 0;
|
|
762
986
|
let lazy_scroll = false;
|
|
763
987
|
//let arrayMode = Array.isArray(selectors);
|
|
764
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
765
988
|
let selectorsLocators = null;
|
|
766
989
|
selectorsLocators = selectors.locators;
|
|
767
990
|
// group selectors by priority
|
|
@@ -789,6 +1012,7 @@ class StableBrowser {
|
|
|
789
1012
|
let highPriorityOnly = true;
|
|
790
1013
|
let visibleOnly = true;
|
|
791
1014
|
while (true) {
|
|
1015
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
792
1016
|
locatorsCount = 0;
|
|
793
1017
|
let result = [];
|
|
794
1018
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -904,9 +1128,13 @@ class StableBrowser {
|
|
|
904
1128
|
}
|
|
905
1129
|
}
|
|
906
1130
|
if (foundLocators.length === 1) {
|
|
1131
|
+
let box = null;
|
|
1132
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1133
|
+
box = await foundLocators[0].boundingBox();
|
|
1134
|
+
}
|
|
907
1135
|
result.foundElements.push({
|
|
908
1136
|
locator: foundLocators[0],
|
|
909
|
-
box:
|
|
1137
|
+
box: box,
|
|
910
1138
|
unique: true,
|
|
911
1139
|
});
|
|
912
1140
|
result.locatorIndex = i;
|
|
@@ -1061,11 +1289,16 @@ class StableBrowser {
|
|
|
1061
1289
|
operation: "click",
|
|
1062
1290
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1063
1291
|
};
|
|
1292
|
+
check_performance("click_all ***", this.context, true);
|
|
1064
1293
|
try {
|
|
1294
|
+
check_performance("click_preCommand", this.context, true);
|
|
1065
1295
|
await _preCommand(state, this);
|
|
1296
|
+
check_performance("click_preCommand", this.context, false);
|
|
1066
1297
|
await performAction("click", state.element, options, this, state, _params);
|
|
1067
|
-
if (!this.fastMode) {
|
|
1068
|
-
|
|
1298
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1299
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1300
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1301
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1069
1302
|
}
|
|
1070
1303
|
return state.info;
|
|
1071
1304
|
}
|
|
@@ -1073,7 +1306,13 @@ class StableBrowser {
|
|
|
1073
1306
|
await _commandError(state, e, this);
|
|
1074
1307
|
}
|
|
1075
1308
|
finally {
|
|
1309
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1076
1310
|
await _commandFinally(state, this);
|
|
1311
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1312
|
+
check_performance("click_all ***", this.context, false);
|
|
1313
|
+
if (this.context.profile) {
|
|
1314
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1315
|
+
}
|
|
1077
1316
|
}
|
|
1078
1317
|
}
|
|
1079
1318
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1165,7 +1404,7 @@ class StableBrowser {
|
|
|
1165
1404
|
}
|
|
1166
1405
|
}
|
|
1167
1406
|
}
|
|
1168
|
-
await this.waitForPageLoad();
|
|
1407
|
+
//await this.waitForPageLoad();
|
|
1169
1408
|
return state.info;
|
|
1170
1409
|
}
|
|
1171
1410
|
catch (e) {
|
|
@@ -1191,7 +1430,7 @@ class StableBrowser {
|
|
|
1191
1430
|
await _preCommand(state, this);
|
|
1192
1431
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1193
1432
|
await _screenshot(state, this);
|
|
1194
|
-
await this.waitForPageLoad();
|
|
1433
|
+
//await this.waitForPageLoad();
|
|
1195
1434
|
return state.info;
|
|
1196
1435
|
}
|
|
1197
1436
|
catch (e) {
|
|
@@ -1227,7 +1466,7 @@ class StableBrowser {
|
|
|
1227
1466
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1228
1467
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1229
1468
|
}
|
|
1230
|
-
await this.waitForPageLoad();
|
|
1469
|
+
//await this.waitForPageLoad();
|
|
1231
1470
|
return state.info;
|
|
1232
1471
|
}
|
|
1233
1472
|
catch (e) {
|
|
@@ -1413,6 +1652,14 @@ class StableBrowser {
|
|
|
1413
1652
|
}
|
|
1414
1653
|
try {
|
|
1415
1654
|
await _preCommand(state, this);
|
|
1655
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1656
|
+
// tag the element
|
|
1657
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1658
|
+
// use attribute and not id
|
|
1659
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1660
|
+
el.setAttribute(attrName, "");
|
|
1661
|
+
return `[${attrName}]`;
|
|
1662
|
+
}, randomToken);
|
|
1416
1663
|
state.info.value = _value;
|
|
1417
1664
|
if (!options.press) {
|
|
1418
1665
|
try {
|
|
@@ -1438,6 +1685,25 @@ class StableBrowser {
|
|
|
1438
1685
|
}
|
|
1439
1686
|
}
|
|
1440
1687
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1688
|
+
// check if the element exist after the click (no wait)
|
|
1689
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1690
|
+
if (count === 0) {
|
|
1691
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1692
|
+
const scope = state.element._frame ?? element.page();
|
|
1693
|
+
let prefixSelector = "";
|
|
1694
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1695
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1696
|
+
if (frameSelectorIndex !== -1) {
|
|
1697
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1698
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1699
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1700
|
+
}
|
|
1701
|
+
// if (element?._frame?._selector) {
|
|
1702
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1703
|
+
// }
|
|
1704
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1705
|
+
state.element = scope.locator(newSelector).first();
|
|
1706
|
+
}
|
|
1441
1707
|
const valueSegment = state.value.split("&&");
|
|
1442
1708
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1443
1709
|
if (i > 0) {
|
|
@@ -1509,8 +1775,8 @@ class StableBrowser {
|
|
|
1509
1775
|
if (enter) {
|
|
1510
1776
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1511
1777
|
await this.page.keyboard.press("Enter");
|
|
1778
|
+
await this.waitForPageLoad();
|
|
1512
1779
|
}
|
|
1513
|
-
await this.waitForPageLoad();
|
|
1514
1780
|
return state.info;
|
|
1515
1781
|
}
|
|
1516
1782
|
catch (e) {
|
|
@@ -2424,47 +2690,54 @@ class StableBrowser {
|
|
|
2424
2690
|
}
|
|
2425
2691
|
state.info.value = val;
|
|
2426
2692
|
let regex;
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
const
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
state.info.failCause.assertionFailed = true;
|
|
2442
|
-
state.info.failCause.lastError = errorMessage;
|
|
2443
|
-
throw new Error(errorMessage);
|
|
2444
|
-
}
|
|
2693
|
+
state.info.value = val;
|
|
2694
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2695
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2696
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2697
|
+
let matchPassed = false;
|
|
2698
|
+
if (isRegex) {
|
|
2699
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2700
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2701
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2702
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2703
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2704
|
+
const regex = new RegExp(patternBody, flags);
|
|
2705
|
+
state.info.regex = true;
|
|
2706
|
+
matchPassed = regex.test(val);
|
|
2445
2707
|
}
|
|
2446
2708
|
else {
|
|
2447
|
-
//
|
|
2448
|
-
const
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
// Check if all expected lines are present in the actual lines
|
|
2452
|
-
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2453
|
-
if (!isPart) {
|
|
2454
|
-
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2455
|
-
state.info.failCause.assertionFailed = true;
|
|
2456
|
-
state.info.failCause.lastError = errorMessage;
|
|
2457
|
-
throw new Error(errorMessage);
|
|
2458
|
-
}
|
|
2709
|
+
// Fallback: treat as literal
|
|
2710
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2711
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2712
|
+
matchPassed = regex.test(val);
|
|
2459
2713
|
}
|
|
2460
2714
|
}
|
|
2715
|
+
else if (isContains) {
|
|
2716
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2717
|
+
matchPassed = val.includes(containsValue);
|
|
2718
|
+
}
|
|
2719
|
+
else if (isExact) {
|
|
2720
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2721
|
+
matchPassed = val === exactValue;
|
|
2722
|
+
}
|
|
2723
|
+
else if (property === "innerText") {
|
|
2724
|
+
// Default innerText logic
|
|
2725
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2726
|
+
const valLines = val.split("\n");
|
|
2727
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2728
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2729
|
+
}
|
|
2461
2730
|
else {
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2731
|
+
// Fallback exact or loose match
|
|
2732
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2733
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2734
|
+
matchPassed = regex.test(val);
|
|
2735
|
+
}
|
|
2736
|
+
if (!matchPassed) {
|
|
2737
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2738
|
+
state.info.failCause.assertionFailed = true;
|
|
2739
|
+
state.info.failCause.lastError = errorMessage;
|
|
2740
|
+
throw new Error(errorMessage);
|
|
2468
2741
|
}
|
|
2469
2742
|
return state.info;
|
|
2470
2743
|
}
|
|
@@ -2475,6 +2748,133 @@ class StableBrowser {
|
|
|
2475
2748
|
await _commandFinally(state, this);
|
|
2476
2749
|
}
|
|
2477
2750
|
}
|
|
2751
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2752
|
+
// Convert timeout from seconds to milliseconds
|
|
2753
|
+
const timeoutMs = timeout * 1000;
|
|
2754
|
+
const state = {
|
|
2755
|
+
selectors,
|
|
2756
|
+
_params,
|
|
2757
|
+
condition,
|
|
2758
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2759
|
+
options,
|
|
2760
|
+
world,
|
|
2761
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2762
|
+
highlight: true,
|
|
2763
|
+
screenshot: true,
|
|
2764
|
+
text: `Conditional wait for element`,
|
|
2765
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2766
|
+
operation: "conditionalWait",
|
|
2767
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2768
|
+
allowDisabled: true,
|
|
2769
|
+
info: {},
|
|
2770
|
+
};
|
|
2771
|
+
state.options ??= { timeout: timeoutMs };
|
|
2772
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2773
|
+
const startTime = Date.now();
|
|
2774
|
+
let conditionMet = false;
|
|
2775
|
+
let currentValue = null;
|
|
2776
|
+
let lastError = null;
|
|
2777
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2778
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2779
|
+
const elapsedTime = Date.now() - startTime;
|
|
2780
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2781
|
+
try {
|
|
2782
|
+
// Try to execute _preCommand (element location)
|
|
2783
|
+
await _preCommand(state, this);
|
|
2784
|
+
// If _preCommand succeeds, start condition checking
|
|
2785
|
+
const checkCondition = async () => {
|
|
2786
|
+
try {
|
|
2787
|
+
switch (condition.toLowerCase()) {
|
|
2788
|
+
case "checked":
|
|
2789
|
+
currentValue = await state.element.isChecked();
|
|
2790
|
+
return currentValue === true;
|
|
2791
|
+
case "unchecked":
|
|
2792
|
+
currentValue = await state.element.isChecked();
|
|
2793
|
+
return currentValue === false;
|
|
2794
|
+
case "visible":
|
|
2795
|
+
currentValue = await state.element.isVisible();
|
|
2796
|
+
return currentValue === true;
|
|
2797
|
+
case "hidden":
|
|
2798
|
+
currentValue = await state.element.isVisible();
|
|
2799
|
+
return currentValue === false;
|
|
2800
|
+
case "enabled":
|
|
2801
|
+
currentValue = await state.element.isDisabled();
|
|
2802
|
+
return currentValue === false;
|
|
2803
|
+
case "disabled":
|
|
2804
|
+
currentValue = await state.element.isDisabled();
|
|
2805
|
+
return currentValue === true;
|
|
2806
|
+
case "editable":
|
|
2807
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2808
|
+
currentValue = await state.element.isContentEditable();
|
|
2809
|
+
return currentValue === true;
|
|
2810
|
+
default:
|
|
2811
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2812
|
+
state.info.success = false;
|
|
2813
|
+
return false;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
catch (error) {
|
|
2817
|
+
// Don't throw here, just return false to continue retrying
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
// Inner loop for condition checking (once element is located)
|
|
2822
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2823
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2824
|
+
conditionMet = await checkCondition();
|
|
2825
|
+
if (conditionMet) {
|
|
2826
|
+
break;
|
|
2827
|
+
}
|
|
2828
|
+
// Check if we still have time for another attempt
|
|
2829
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2830
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2831
|
+
}
|
|
2832
|
+
else {
|
|
2833
|
+
break;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
// If we got here and condition is met, break out of main loop
|
|
2837
|
+
if (conditionMet) {
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2840
|
+
// If condition not met but no exception, we've timed out
|
|
2841
|
+
break;
|
|
2842
|
+
}
|
|
2843
|
+
catch (e) {
|
|
2844
|
+
lastError = e;
|
|
2845
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2846
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2847
|
+
// Check if we have enough time left to retry
|
|
2848
|
+
if (timeLeft > 100) {
|
|
2849
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2850
|
+
}
|
|
2851
|
+
else {
|
|
2852
|
+
break;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2857
|
+
state.info = {
|
|
2858
|
+
success: conditionMet,
|
|
2859
|
+
conditionMet,
|
|
2860
|
+
actualWaitTime,
|
|
2861
|
+
currentValue,
|
|
2862
|
+
lastError: lastError?.message || null,
|
|
2863
|
+
message: conditionMet
|
|
2864
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2865
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2866
|
+
};
|
|
2867
|
+
if (lastError) {
|
|
2868
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2869
|
+
}
|
|
2870
|
+
try {
|
|
2871
|
+
await _commandFinally(state, this);
|
|
2872
|
+
}
|
|
2873
|
+
catch (finallyError) {
|
|
2874
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2875
|
+
}
|
|
2876
|
+
return state.info;
|
|
2877
|
+
}
|
|
2478
2878
|
async extractEmailData(emailAddress, options, world) {
|
|
2479
2879
|
if (!emailAddress) {
|
|
2480
2880
|
throw new Error("email address is null");
|
|
@@ -3071,6 +3471,8 @@ class StableBrowser {
|
|
|
3071
3471
|
operation: "verify_text_with_relation",
|
|
3072
3472
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3073
3473
|
};
|
|
3474
|
+
const cmdStartTime = Date.now();
|
|
3475
|
+
let cmdEndTime = null;
|
|
3074
3476
|
const timeout = this._getFindElementTimeout(options);
|
|
3075
3477
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3076
3478
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3106,6 +3508,17 @@ class StableBrowser {
|
|
|
3106
3508
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3107
3509
|
continue;
|
|
3108
3510
|
}
|
|
3511
|
+
else {
|
|
3512
|
+
cmdEndTime = Date.now();
|
|
3513
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3514
|
+
if (foundAncore) {
|
|
3515
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3516
|
+
}
|
|
3517
|
+
else {
|
|
3518
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3109
3522
|
try {
|
|
3110
3523
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3111
3524
|
foundAncore = true;
|
|
@@ -3244,7 +3657,7 @@ class StableBrowser {
|
|
|
3244
3657
|
Object.assign(e, { info: info });
|
|
3245
3658
|
error = e;
|
|
3246
3659
|
// throw e;
|
|
3247
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3660
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3248
3661
|
}
|
|
3249
3662
|
finally {
|
|
3250
3663
|
const endTime = Date.now();
|
|
@@ -3593,6 +4006,22 @@ class StableBrowser {
|
|
|
3593
4006
|
}
|
|
3594
4007
|
}
|
|
3595
4008
|
async waitForPageLoad(options = {}, world = null) {
|
|
4009
|
+
// try {
|
|
4010
|
+
// let currentPagePath = null;
|
|
4011
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4012
|
+
// if (this.latestPagePath) {
|
|
4013
|
+
// // get the currect page path and compare with the latest page path
|
|
4014
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4015
|
+
// // if the page path is the same, do not wait for page load
|
|
4016
|
+
// console.log("No page change: " + currentPagePath);
|
|
4017
|
+
// return;
|
|
4018
|
+
// }
|
|
4019
|
+
// }
|
|
4020
|
+
// this.latestPagePath = currentPagePath;
|
|
4021
|
+
// } catch (e) {
|
|
4022
|
+
// console.debug("Error getting current page path: ", e);
|
|
4023
|
+
// }
|
|
4024
|
+
//console.log("Waiting for page load");
|
|
3596
4025
|
let timeout = this._getLoadTimeout(options);
|
|
3597
4026
|
const promiseArray = [];
|
|
3598
4027
|
// let waitForNetworkIdle = true;
|
|
@@ -3625,10 +4054,12 @@ class StableBrowser {
|
|
|
3625
4054
|
else if (e.label === "domcontentloaded") {
|
|
3626
4055
|
console.log("waited for the domcontent loaded timeout");
|
|
3627
4056
|
}
|
|
3628
|
-
console.log(".");
|
|
3629
4057
|
}
|
|
3630
4058
|
finally {
|
|
3631
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4059
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4060
|
+
if (options && !options.noSleep) {
|
|
4061
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4062
|
+
}
|
|
3632
4063
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3633
4064
|
const endTime = Date.now();
|
|
3634
4065
|
_reportToWorld(world, {
|
|
@@ -3669,7 +4100,6 @@ class StableBrowser {
|
|
|
3669
4100
|
await this.page.close();
|
|
3670
4101
|
}
|
|
3671
4102
|
catch (e) {
|
|
3672
|
-
console.log(".");
|
|
3673
4103
|
await _commandError(state, e, this);
|
|
3674
4104
|
}
|
|
3675
4105
|
finally {
|
|
@@ -3683,7 +4113,7 @@ class StableBrowser {
|
|
|
3683
4113
|
}
|
|
3684
4114
|
operation = options.operation;
|
|
3685
4115
|
// validate operation is one of the supported operations
|
|
3686
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4116
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3687
4117
|
throw new Error("operation is not supported");
|
|
3688
4118
|
}
|
|
3689
4119
|
const state = {
|
|
@@ -3752,6 +4182,17 @@ class StableBrowser {
|
|
|
3752
4182
|
state.element = results[0];
|
|
3753
4183
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3754
4184
|
break;
|
|
4185
|
+
case "hover":
|
|
4186
|
+
if (!options.css) {
|
|
4187
|
+
throw new Error("css is not defined");
|
|
4188
|
+
}
|
|
4189
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4190
|
+
if (result1.length === 0) {
|
|
4191
|
+
throw new Error(`Element not found in cell area`);
|
|
4192
|
+
}
|
|
4193
|
+
state.element = result1[0];
|
|
4194
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4195
|
+
break;
|
|
3755
4196
|
default:
|
|
3756
4197
|
throw new Error("operation is not supported");
|
|
3757
4198
|
}
|
|
@@ -3784,7 +4225,6 @@ class StableBrowser {
|
|
|
3784
4225
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3785
4226
|
}
|
|
3786
4227
|
catch (e) {
|
|
3787
|
-
console.log(".");
|
|
3788
4228
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3789
4229
|
}
|
|
3790
4230
|
finally {
|
|
@@ -3822,7 +4262,6 @@ class StableBrowser {
|
|
|
3822
4262
|
await this.page.reload();
|
|
3823
4263
|
}
|
|
3824
4264
|
catch (e) {
|
|
3825
|
-
console.log(".");
|
|
3826
4265
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3827
4266
|
}
|
|
3828
4267
|
finally {
|
|
@@ -3866,6 +4305,10 @@ class StableBrowser {
|
|
|
3866
4305
|
}
|
|
3867
4306
|
}
|
|
3868
4307
|
async beforeScenario(world, scenario) {
|
|
4308
|
+
if (world && world.attach) {
|
|
4309
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4310
|
+
}
|
|
4311
|
+
this.context.loadedRoutes = null;
|
|
3869
4312
|
this.beforeScenarioCalled = true;
|
|
3870
4313
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3871
4314
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3895,8 +4338,10 @@ class StableBrowser {
|
|
|
3895
4338
|
}
|
|
3896
4339
|
async afterScenario(world, scenario) { }
|
|
3897
4340
|
async beforeStep(world, step) {
|
|
4341
|
+
this.stepTags = [];
|
|
3898
4342
|
if (!this.beforeScenarioCalled) {
|
|
3899
4343
|
this.beforeScenario(world, step);
|
|
4344
|
+
this.context.loadedRoutes = null;
|
|
3900
4345
|
}
|
|
3901
4346
|
if (this.stepIndex === undefined) {
|
|
3902
4347
|
this.stepIndex = 0;
|
|
@@ -3906,7 +4351,12 @@ class StableBrowser {
|
|
|
3906
4351
|
}
|
|
3907
4352
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3908
4353
|
this.stepName = step.pickleStep.text;
|
|
3909
|
-
|
|
4354
|
+
let printableStepName = this.stepName;
|
|
4355
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4356
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4357
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4358
|
+
});
|
|
4359
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
3910
4360
|
}
|
|
3911
4361
|
else if (step && step.text) {
|
|
3912
4362
|
this.stepName = step.text;
|
|
@@ -3921,13 +4371,23 @@ class StableBrowser {
|
|
|
3921
4371
|
}
|
|
3922
4372
|
if (this.initSnapshotTaken === false) {
|
|
3923
4373
|
this.initSnapshotTaken = true;
|
|
3924
|
-
if (world &&
|
|
4374
|
+
if (world &&
|
|
4375
|
+
world.attach &&
|
|
4376
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4377
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
3925
4378
|
const snapshot = await this.getAriaSnapshot();
|
|
3926
4379
|
if (snapshot) {
|
|
3927
4380
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3928
4381
|
}
|
|
3929
4382
|
}
|
|
3930
4383
|
}
|
|
4384
|
+
this.context.routeResults = null;
|
|
4385
|
+
this.context.loadedRoutes = null;
|
|
4386
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4387
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4388
|
+
}
|
|
4389
|
+
setStepTags(tags) {
|
|
4390
|
+
this.stepTags = tags;
|
|
3931
4391
|
}
|
|
3932
4392
|
async getAriaSnapshot() {
|
|
3933
4393
|
try {
|
|
@@ -3947,12 +4407,18 @@ class StableBrowser {
|
|
|
3947
4407
|
try {
|
|
3948
4408
|
// Ensure frame is attached and has body
|
|
3949
4409
|
const body = frame.locator("body");
|
|
3950
|
-
await body.waitFor({ timeout:
|
|
4410
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
3951
4411
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4412
|
+
if (!snapshot) {
|
|
4413
|
+
continue;
|
|
4414
|
+
}
|
|
3952
4415
|
content.push(`- frame: ${i}`);
|
|
3953
4416
|
content.push(snapshot);
|
|
3954
4417
|
}
|
|
3955
|
-
catch (innerErr) {
|
|
4418
|
+
catch (innerErr) {
|
|
4419
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4420
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4421
|
+
}
|
|
3956
4422
|
}
|
|
3957
4423
|
return content.join("\n");
|
|
3958
4424
|
}
|
|
@@ -4024,13 +4490,23 @@ class StableBrowser {
|
|
|
4024
4490
|
if (this.context) {
|
|
4025
4491
|
this.context.examplesRow = null;
|
|
4026
4492
|
}
|
|
4027
|
-
if (world &&
|
|
4493
|
+
if (world &&
|
|
4494
|
+
world.attach &&
|
|
4495
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4496
|
+
!this.fastMode &&
|
|
4497
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4028
4498
|
const snapshot = await this.getAriaSnapshot();
|
|
4029
4499
|
if (snapshot) {
|
|
4030
4500
|
const obj = {};
|
|
4031
4501
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4032
4502
|
}
|
|
4033
4503
|
}
|
|
4504
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4505
|
+
if (this.context.routeResults) {
|
|
4506
|
+
if (world && world.attach) {
|
|
4507
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4034
4510
|
if (!process.env.TEMP_RUN) {
|
|
4035
4511
|
const state = {
|
|
4036
4512
|
world,
|
|
@@ -4054,6 +4530,13 @@ class StableBrowser {
|
|
|
4054
4530
|
await _commandFinally(state, this);
|
|
4055
4531
|
}
|
|
4056
4532
|
}
|
|
4533
|
+
networkAfterStep(this.stepName, this.context);
|
|
4534
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4535
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4536
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4537
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4057
4540
|
}
|
|
4058
4541
|
}
|
|
4059
4542
|
function createTimedPromise(promise, label) {
|