automation_model 1.0.746-dev → 1.0.746-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 +4 -1
- package/lib/api.js +4 -3
- 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 +19 -25
- 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 +129 -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 +121 -125
- 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 +575 -86
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.js +2 -2
- package/lib/table.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 +33 -8
- package/lib/utils.js.map +1 -1
- package/package.json +16 -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
|
}
|
|
@@ -123,9 +133,16 @@ class StableBrowser {
|
|
|
123
133
|
context.pages = [this.page];
|
|
124
134
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
125
135
|
this.world = world;
|
|
136
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
137
|
+
this.fastMode = true;
|
|
138
|
+
}
|
|
126
139
|
if (process.env.FAST_MODE === "true") {
|
|
140
|
+
// console.log("Fast mode enabled from environment variable");
|
|
127
141
|
this.fastMode = true;
|
|
128
142
|
}
|
|
143
|
+
if (process.env.FAST_MODE === "false") {
|
|
144
|
+
this.fastMode = false;
|
|
145
|
+
}
|
|
129
146
|
if (this.context) {
|
|
130
147
|
this.context.fastMode = this.fastMode;
|
|
131
148
|
}
|
|
@@ -163,6 +180,7 @@ class StableBrowser {
|
|
|
163
180
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
164
181
|
registerDownloadEvent(this.page, this.world, context);
|
|
165
182
|
page.on("close", async () => {
|
|
183
|
+
// return if browser context is already closed
|
|
166
184
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
167
185
|
this.context.pages.pop();
|
|
168
186
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -172,7 +190,12 @@ class StableBrowser {
|
|
|
172
190
|
console.log("Switched to page " + title);
|
|
173
191
|
}
|
|
174
192
|
catch (error) {
|
|
175
|
-
|
|
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
|
+
}
|
|
176
199
|
}
|
|
177
200
|
}
|
|
178
201
|
});
|
|
@@ -181,7 +204,12 @@ class StableBrowser {
|
|
|
181
204
|
console.log("Switch page: " + (await page.title()));
|
|
182
205
|
}
|
|
183
206
|
catch (e) {
|
|
184
|
-
|
|
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
|
+
}
|
|
185
213
|
}
|
|
186
214
|
context.pageLoading.status = false;
|
|
187
215
|
}.bind(this));
|
|
@@ -209,7 +237,7 @@ class StableBrowser {
|
|
|
209
237
|
if (newContextCreated) {
|
|
210
238
|
this.registerEventListeners(this.context);
|
|
211
239
|
await this.goto(this.context.environment.baseUrl);
|
|
212
|
-
if (!this.fastMode) {
|
|
240
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
213
241
|
await this.waitForPageLoad();
|
|
214
242
|
}
|
|
215
243
|
}
|
|
@@ -337,6 +365,64 @@ class StableBrowser {
|
|
|
337
365
|
await _commandFinally(state, this);
|
|
338
366
|
}
|
|
339
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
|
+
}
|
|
340
426
|
async _getLocator(locator, scope, _params) {
|
|
341
427
|
locator = _fixLocatorUsingParams(locator, _params);
|
|
342
428
|
// locator = await this._replaceWithLocalData(locator);
|
|
@@ -435,12 +521,6 @@ class StableBrowser {
|
|
|
435
521
|
if (!el.setAttribute) {
|
|
436
522
|
el = el.parentElement;
|
|
437
523
|
}
|
|
438
|
-
// remove any attributes start with data-blinq-id
|
|
439
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
440
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
441
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
442
|
-
// }
|
|
443
|
-
// }
|
|
444
524
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
445
525
|
return true;
|
|
446
526
|
}, [tag1, randomToken]))) {
|
|
@@ -462,14 +542,13 @@ class StableBrowser {
|
|
|
462
542
|
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
463
543
|
}
|
|
464
544
|
let locatorSearch = selectorHierarchy[index];
|
|
465
|
-
let originalLocatorSearch = "";
|
|
466
545
|
try {
|
|
467
|
-
|
|
468
|
-
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
546
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
469
547
|
}
|
|
470
548
|
catch (e) {
|
|
471
549
|
console.error(e);
|
|
472
550
|
}
|
|
551
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
473
552
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
474
553
|
let locator = null;
|
|
475
554
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
@@ -611,40 +690,186 @@ class StableBrowser {
|
|
|
611
690
|
}
|
|
612
691
|
return { rerun: false };
|
|
613
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
|
+
}
|
|
614
757
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
615
758
|
if (!timeout) {
|
|
616
759
|
timeout = 30000;
|
|
617
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
|
+
}
|
|
618
768
|
for (let i = 0; i < 3; i++) {
|
|
619
769
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
620
770
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
621
771
|
let selector = selectors.locators[j];
|
|
622
772
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
623
773
|
}
|
|
624
|
-
|
|
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
|
+
}
|
|
625
817
|
if (!element.rerun) {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
+
}
|
|
633
859
|
const scope = element._frame ?? element.page();
|
|
634
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
635
860
|
let prefixSelector = "";
|
|
636
861
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
637
862
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
638
863
|
if (frameSelectorIndex !== -1) {
|
|
639
864
|
// remove everything after the >> internal:control=enter-frame
|
|
640
865
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
641
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
866
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
642
867
|
}
|
|
643
868
|
// if (element?._frame?._selector) {
|
|
644
869
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
645
870
|
// }
|
|
646
871
|
const newSelector = prefixSelector + newElementSelector;
|
|
647
|
-
return scope.locator(newSelector);
|
|
872
|
+
return scope.locator(newSelector).first();
|
|
648
873
|
}
|
|
649
874
|
}
|
|
650
875
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -677,7 +902,7 @@ class StableBrowser {
|
|
|
677
902
|
break;
|
|
678
903
|
}
|
|
679
904
|
catch (error) {
|
|
680
|
-
console.error("frame not found " + frameLocator.css);
|
|
905
|
+
// console.error("frame not found " + frameLocator.css);
|
|
681
906
|
}
|
|
682
907
|
}
|
|
683
908
|
}
|
|
@@ -743,6 +968,11 @@ class StableBrowser {
|
|
|
743
968
|
});
|
|
744
969
|
}
|
|
745
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
|
+
}
|
|
746
976
|
if (!info) {
|
|
747
977
|
info = {};
|
|
748
978
|
info.failCause = {};
|
|
@@ -755,7 +985,6 @@ class StableBrowser {
|
|
|
755
985
|
let locatorsCount = 0;
|
|
756
986
|
let lazy_scroll = false;
|
|
757
987
|
//let arrayMode = Array.isArray(selectors);
|
|
758
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
759
988
|
let selectorsLocators = null;
|
|
760
989
|
selectorsLocators = selectors.locators;
|
|
761
990
|
// group selectors by priority
|
|
@@ -783,6 +1012,7 @@ class StableBrowser {
|
|
|
783
1012
|
let highPriorityOnly = true;
|
|
784
1013
|
let visibleOnly = true;
|
|
785
1014
|
while (true) {
|
|
1015
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
786
1016
|
locatorsCount = 0;
|
|
787
1017
|
let result = [];
|
|
788
1018
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -898,9 +1128,13 @@ class StableBrowser {
|
|
|
898
1128
|
}
|
|
899
1129
|
}
|
|
900
1130
|
if (foundLocators.length === 1) {
|
|
1131
|
+
let box = null;
|
|
1132
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1133
|
+
box = await foundLocators[0].boundingBox();
|
|
1134
|
+
}
|
|
901
1135
|
result.foundElements.push({
|
|
902
1136
|
locator: foundLocators[0],
|
|
903
|
-
box:
|
|
1137
|
+
box: box,
|
|
904
1138
|
unique: true,
|
|
905
1139
|
});
|
|
906
1140
|
result.locatorIndex = i;
|
|
@@ -1055,11 +1289,16 @@ class StableBrowser {
|
|
|
1055
1289
|
operation: "click",
|
|
1056
1290
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1057
1291
|
};
|
|
1292
|
+
check_performance("click_all ***", this.context, true);
|
|
1058
1293
|
try {
|
|
1294
|
+
check_performance("click_preCommand", this.context, true);
|
|
1059
1295
|
await _preCommand(state, this);
|
|
1296
|
+
check_performance("click_preCommand", this.context, false);
|
|
1060
1297
|
await performAction("click", state.element, options, this, state, _params);
|
|
1061
|
-
if (!this.fastMode) {
|
|
1062
|
-
|
|
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);
|
|
1063
1302
|
}
|
|
1064
1303
|
return state.info;
|
|
1065
1304
|
}
|
|
@@ -1067,7 +1306,13 @@ class StableBrowser {
|
|
|
1067
1306
|
await _commandError(state, e, this);
|
|
1068
1307
|
}
|
|
1069
1308
|
finally {
|
|
1309
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1070
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
|
+
}
|
|
1071
1316
|
}
|
|
1072
1317
|
}
|
|
1073
1318
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1159,7 +1404,7 @@ class StableBrowser {
|
|
|
1159
1404
|
}
|
|
1160
1405
|
}
|
|
1161
1406
|
}
|
|
1162
|
-
await this.waitForPageLoad();
|
|
1407
|
+
//await this.waitForPageLoad();
|
|
1163
1408
|
return state.info;
|
|
1164
1409
|
}
|
|
1165
1410
|
catch (e) {
|
|
@@ -1185,7 +1430,7 @@ class StableBrowser {
|
|
|
1185
1430
|
await _preCommand(state, this);
|
|
1186
1431
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1187
1432
|
await _screenshot(state, this);
|
|
1188
|
-
await this.waitForPageLoad();
|
|
1433
|
+
//await this.waitForPageLoad();
|
|
1189
1434
|
return state.info;
|
|
1190
1435
|
}
|
|
1191
1436
|
catch (e) {
|
|
@@ -1221,7 +1466,7 @@ class StableBrowser {
|
|
|
1221
1466
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1222
1467
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1223
1468
|
}
|
|
1224
|
-
await this.waitForPageLoad();
|
|
1469
|
+
//await this.waitForPageLoad();
|
|
1225
1470
|
return state.info;
|
|
1226
1471
|
}
|
|
1227
1472
|
catch (e) {
|
|
@@ -1407,6 +1652,14 @@ class StableBrowser {
|
|
|
1407
1652
|
}
|
|
1408
1653
|
try {
|
|
1409
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);
|
|
1410
1663
|
state.info.value = _value;
|
|
1411
1664
|
if (!options.press) {
|
|
1412
1665
|
try {
|
|
@@ -1432,6 +1685,25 @@ class StableBrowser {
|
|
|
1432
1685
|
}
|
|
1433
1686
|
}
|
|
1434
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
|
+
}
|
|
1435
1707
|
const valueSegment = state.value.split("&&");
|
|
1436
1708
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1437
1709
|
if (i > 0) {
|
|
@@ -1503,8 +1775,8 @@ class StableBrowser {
|
|
|
1503
1775
|
if (enter) {
|
|
1504
1776
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1505
1777
|
await this.page.keyboard.press("Enter");
|
|
1778
|
+
await this.waitForPageLoad();
|
|
1506
1779
|
}
|
|
1507
|
-
await this.waitForPageLoad();
|
|
1508
1780
|
return state.info;
|
|
1509
1781
|
}
|
|
1510
1782
|
catch (e) {
|
|
@@ -2409,7 +2681,7 @@ class StableBrowser {
|
|
|
2409
2681
|
}
|
|
2410
2682
|
// Helper function to remove all style="" attributes
|
|
2411
2683
|
const removeStyleAttributes = (htmlString) => {
|
|
2412
|
-
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi,
|
|
2684
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2413
2685
|
};
|
|
2414
2686
|
// Remove style attributes for innerHTML and outerHTML properties
|
|
2415
2687
|
if (property === "innerHTML" || property === "outerHTML") {
|
|
@@ -2418,47 +2690,54 @@ class StableBrowser {
|
|
|
2418
2690
|
}
|
|
2419
2691
|
state.info.value = val;
|
|
2420
2692
|
let regex;
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
state.info.failCause.assertionFailed = true;
|
|
2436
|
-
state.info.failCause.lastError = errorMessage;
|
|
2437
|
-
throw new Error(errorMessage);
|
|
2438
|
-
}
|
|
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);
|
|
2439
2707
|
}
|
|
2440
2708
|
else {
|
|
2441
|
-
//
|
|
2442
|
-
const
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2445
|
-
// Check if all expected lines are present in the actual lines
|
|
2446
|
-
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2447
|
-
if (!isPart) {
|
|
2448
|
-
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2449
|
-
state.info.failCause.assertionFailed = true;
|
|
2450
|
-
state.info.failCause.lastError = errorMessage;
|
|
2451
|
-
throw new Error(errorMessage);
|
|
2452
|
-
}
|
|
2709
|
+
// Fallback: treat as literal
|
|
2710
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2711
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2712
|
+
matchPassed = regex.test(val);
|
|
2453
2713
|
}
|
|
2454
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
|
+
}
|
|
2455
2730
|
else {
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
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);
|
|
2462
2741
|
}
|
|
2463
2742
|
return state.info;
|
|
2464
2743
|
}
|
|
@@ -2469,6 +2748,133 @@ class StableBrowser {
|
|
|
2469
2748
|
await _commandFinally(state, this);
|
|
2470
2749
|
}
|
|
2471
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
|
+
}
|
|
2472
2878
|
async extractEmailData(emailAddress, options, world) {
|
|
2473
2879
|
if (!emailAddress) {
|
|
2474
2880
|
throw new Error("email address is null");
|
|
@@ -3065,6 +3471,8 @@ class StableBrowser {
|
|
|
3065
3471
|
operation: "verify_text_with_relation",
|
|
3066
3472
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3067
3473
|
};
|
|
3474
|
+
const cmdStartTime = Date.now();
|
|
3475
|
+
let cmdEndTime = null;
|
|
3068
3476
|
const timeout = this._getFindElementTimeout(options);
|
|
3069
3477
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3070
3478
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3100,6 +3508,17 @@ class StableBrowser {
|
|
|
3100
3508
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3101
3509
|
continue;
|
|
3102
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
|
+
}
|
|
3103
3522
|
try {
|
|
3104
3523
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3105
3524
|
foundAncore = true;
|
|
@@ -3238,7 +3657,7 @@ class StableBrowser {
|
|
|
3238
3657
|
Object.assign(e, { info: info });
|
|
3239
3658
|
error = e;
|
|
3240
3659
|
// throw e;
|
|
3241
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3660
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3242
3661
|
}
|
|
3243
3662
|
finally {
|
|
3244
3663
|
const endTime = Date.now();
|
|
@@ -3587,6 +4006,22 @@ class StableBrowser {
|
|
|
3587
4006
|
}
|
|
3588
4007
|
}
|
|
3589
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");
|
|
3590
4025
|
let timeout = this._getLoadTimeout(options);
|
|
3591
4026
|
const promiseArray = [];
|
|
3592
4027
|
// let waitForNetworkIdle = true;
|
|
@@ -3619,10 +4054,12 @@ class StableBrowser {
|
|
|
3619
4054
|
else if (e.label === "domcontentloaded") {
|
|
3620
4055
|
console.log("waited for the domcontent loaded timeout");
|
|
3621
4056
|
}
|
|
3622
|
-
console.log(".");
|
|
3623
4057
|
}
|
|
3624
4058
|
finally {
|
|
3625
|
-
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
|
+
}
|
|
3626
4063
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3627
4064
|
const endTime = Date.now();
|
|
3628
4065
|
_reportToWorld(world, {
|
|
@@ -3663,7 +4100,6 @@ class StableBrowser {
|
|
|
3663
4100
|
await this.page.close();
|
|
3664
4101
|
}
|
|
3665
4102
|
catch (e) {
|
|
3666
|
-
console.log(".");
|
|
3667
4103
|
await _commandError(state, e, this);
|
|
3668
4104
|
}
|
|
3669
4105
|
finally {
|
|
@@ -3677,7 +4113,7 @@ class StableBrowser {
|
|
|
3677
4113
|
}
|
|
3678
4114
|
operation = options.operation;
|
|
3679
4115
|
// validate operation is one of the supported operations
|
|
3680
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4116
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3681
4117
|
throw new Error("operation is not supported");
|
|
3682
4118
|
}
|
|
3683
4119
|
const state = {
|
|
@@ -3746,6 +4182,17 @@ class StableBrowser {
|
|
|
3746
4182
|
state.element = results[0];
|
|
3747
4183
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3748
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;
|
|
3749
4196
|
default:
|
|
3750
4197
|
throw new Error("operation is not supported");
|
|
3751
4198
|
}
|
|
@@ -3778,7 +4225,6 @@ class StableBrowser {
|
|
|
3778
4225
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3779
4226
|
}
|
|
3780
4227
|
catch (e) {
|
|
3781
|
-
console.log(".");
|
|
3782
4228
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3783
4229
|
}
|
|
3784
4230
|
finally {
|
|
@@ -3816,7 +4262,6 @@ class StableBrowser {
|
|
|
3816
4262
|
await this.page.reload();
|
|
3817
4263
|
}
|
|
3818
4264
|
catch (e) {
|
|
3819
|
-
console.log(".");
|
|
3820
4265
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3821
4266
|
}
|
|
3822
4267
|
finally {
|
|
@@ -3860,6 +4305,10 @@ class StableBrowser {
|
|
|
3860
4305
|
}
|
|
3861
4306
|
}
|
|
3862
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;
|
|
3863
4312
|
this.beforeScenarioCalled = true;
|
|
3864
4313
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3865
4314
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3889,8 +4338,10 @@ class StableBrowser {
|
|
|
3889
4338
|
}
|
|
3890
4339
|
async afterScenario(world, scenario) { }
|
|
3891
4340
|
async beforeStep(world, step) {
|
|
4341
|
+
this.stepTags = [];
|
|
3892
4342
|
if (!this.beforeScenarioCalled) {
|
|
3893
4343
|
this.beforeScenario(world, step);
|
|
4344
|
+
this.context.loadedRoutes = null;
|
|
3894
4345
|
}
|
|
3895
4346
|
if (this.stepIndex === undefined) {
|
|
3896
4347
|
this.stepIndex = 0;
|
|
@@ -3900,7 +4351,12 @@ class StableBrowser {
|
|
|
3900
4351
|
}
|
|
3901
4352
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3902
4353
|
this.stepName = step.pickleStep.text;
|
|
3903
|
-
|
|
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);
|
|
3904
4360
|
}
|
|
3905
4361
|
else if (step && step.text) {
|
|
3906
4362
|
this.stepName = step.text;
|
|
@@ -3915,13 +4371,23 @@ class StableBrowser {
|
|
|
3915
4371
|
}
|
|
3916
4372
|
if (this.initSnapshotTaken === false) {
|
|
3917
4373
|
this.initSnapshotTaken = true;
|
|
3918
|
-
if (world &&
|
|
4374
|
+
if (world &&
|
|
4375
|
+
world.attach &&
|
|
4376
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4377
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
3919
4378
|
const snapshot = await this.getAriaSnapshot();
|
|
3920
4379
|
if (snapshot) {
|
|
3921
4380
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3922
4381
|
}
|
|
3923
4382
|
}
|
|
3924
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;
|
|
3925
4391
|
}
|
|
3926
4392
|
async getAriaSnapshot() {
|
|
3927
4393
|
try {
|
|
@@ -3941,12 +4407,18 @@ class StableBrowser {
|
|
|
3941
4407
|
try {
|
|
3942
4408
|
// Ensure frame is attached and has body
|
|
3943
4409
|
const body = frame.locator("body");
|
|
3944
|
-
await body.waitFor({ timeout:
|
|
4410
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
3945
4411
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4412
|
+
if (!snapshot) {
|
|
4413
|
+
continue;
|
|
4414
|
+
}
|
|
3946
4415
|
content.push(`- frame: ${i}`);
|
|
3947
4416
|
content.push(snapshot);
|
|
3948
4417
|
}
|
|
3949
|
-
catch (innerErr) {
|
|
4418
|
+
catch (innerErr) {
|
|
4419
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4420
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4421
|
+
}
|
|
3950
4422
|
}
|
|
3951
4423
|
return content.join("\n");
|
|
3952
4424
|
}
|
|
@@ -4018,13 +4490,23 @@ class StableBrowser {
|
|
|
4018
4490
|
if (this.context) {
|
|
4019
4491
|
this.context.examplesRow = null;
|
|
4020
4492
|
}
|
|
4021
|
-
if (world &&
|
|
4493
|
+
if (world &&
|
|
4494
|
+
world.attach &&
|
|
4495
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4496
|
+
!this.fastMode &&
|
|
4497
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4022
4498
|
const snapshot = await this.getAriaSnapshot();
|
|
4023
4499
|
if (snapshot) {
|
|
4024
4500
|
const obj = {};
|
|
4025
4501
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4026
4502
|
}
|
|
4027
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
|
+
}
|
|
4028
4510
|
if (!process.env.TEMP_RUN) {
|
|
4029
4511
|
const state = {
|
|
4030
4512
|
world,
|
|
@@ -4048,6 +4530,13 @@ class StableBrowser {
|
|
|
4048
4530
|
await _commandFinally(state, this);
|
|
4049
4531
|
}
|
|
4050
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
|
+
}
|
|
4051
4540
|
}
|
|
4052
4541
|
}
|
|
4053
4542
|
function createTimedPromise(promise, label) {
|