automation_model 1.0.741-dev → 1.0.741-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 +60 -9
- 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.d.ts +1 -0
- package/lib/locator.js +9 -2
- package/lib/locator.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 +18 -3
- package/lib/stable_browser.js +725 -54
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +9 -7
- package/lib/table.js +82 -12
- 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 -1
- package/lib/utils.js +36 -9
- package/lib/utils.js.map +1 -1
- package/package.json +15 -10
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,7 +11,8 @@ 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";
|
|
13
|
-
import
|
|
14
|
+
import errorStackParser from "error-stack-parser";
|
|
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";
|
|
16
18
|
import readline from "readline";
|
|
@@ -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",
|
|
@@ -49,6 +56,7 @@ export const Types = {
|
|
|
49
56
|
CHECK: "check_element",
|
|
50
57
|
UNCHECK: "uncheck_element",
|
|
51
58
|
EXTRACT: "extract_attribute",
|
|
59
|
+
EXTRACT_PROPERTY: "extract_property",
|
|
52
60
|
CLOSE_PAGE: "close_page",
|
|
53
61
|
TABLE_OPERATION: "table_operation",
|
|
54
62
|
SET_DATE_TIME: "set_date_time",
|
|
@@ -60,12 +68,13 @@ export const Types = {
|
|
|
60
68
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
61
69
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
62
70
|
BRUNO: "bruno",
|
|
63
|
-
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
64
71
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
65
72
|
SET_INPUT_FILES: "set_input_files",
|
|
73
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
66
74
|
REPORT_COMMAND: "report_command",
|
|
67
75
|
STEP_COMPLETE: "step_complete",
|
|
68
76
|
SLEEP: "sleep",
|
|
77
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
69
78
|
};
|
|
70
79
|
export const apps = {};
|
|
71
80
|
const formatElementName = (elementName) => {
|
|
@@ -78,6 +87,7 @@ class StableBrowser {
|
|
|
78
87
|
context;
|
|
79
88
|
world;
|
|
80
89
|
fastMode;
|
|
90
|
+
stepTags;
|
|
81
91
|
project_path = null;
|
|
82
92
|
webLogFile = null;
|
|
83
93
|
networkLogger = null;
|
|
@@ -86,13 +96,15 @@ class StableBrowser {
|
|
|
86
96
|
tags = null;
|
|
87
97
|
isRecording = false;
|
|
88
98
|
initSnapshotTaken = false;
|
|
89
|
-
|
|
99
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
100
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
90
101
|
this.browser = browser;
|
|
91
102
|
this.page = page;
|
|
92
103
|
this.logger = logger;
|
|
93
104
|
this.context = context;
|
|
94
105
|
this.world = world;
|
|
95
106
|
this.fastMode = fastMode;
|
|
107
|
+
this.stepTags = stepTags;
|
|
96
108
|
if (!this.logger) {
|
|
97
109
|
this.logger = console;
|
|
98
110
|
}
|
|
@@ -121,9 +133,16 @@ class StableBrowser {
|
|
|
121
133
|
context.pages = [this.page];
|
|
122
134
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
123
135
|
this.world = world;
|
|
136
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
137
|
+
this.fastMode = true;
|
|
138
|
+
}
|
|
124
139
|
if (process.env.FAST_MODE === "true") {
|
|
140
|
+
// console.log("Fast mode enabled from environment variable");
|
|
125
141
|
this.fastMode = true;
|
|
126
142
|
}
|
|
143
|
+
if (process.env.FAST_MODE === "false") {
|
|
144
|
+
this.fastMode = false;
|
|
145
|
+
}
|
|
127
146
|
if (this.context) {
|
|
128
147
|
this.context.fastMode = this.fastMode;
|
|
129
148
|
}
|
|
@@ -161,6 +180,7 @@ class StableBrowser {
|
|
|
161
180
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
162
181
|
registerDownloadEvent(this.page, this.world, context);
|
|
163
182
|
page.on("close", async () => {
|
|
183
|
+
// return if browser context is already closed
|
|
164
184
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
165
185
|
this.context.pages.pop();
|
|
166
186
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -170,7 +190,12 @@ class StableBrowser {
|
|
|
170
190
|
console.log("Switched to page " + title);
|
|
171
191
|
}
|
|
172
192
|
catch (error) {
|
|
173
|
-
|
|
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
|
+
}
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
201
|
});
|
|
@@ -179,7 +204,12 @@ class StableBrowser {
|
|
|
179
204
|
console.log("Switch page: " + (await page.title()));
|
|
180
205
|
}
|
|
181
206
|
catch (e) {
|
|
182
|
-
|
|
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
|
+
}
|
|
183
213
|
}
|
|
184
214
|
context.pageLoading.status = false;
|
|
185
215
|
}.bind(this));
|
|
@@ -207,7 +237,7 @@ class StableBrowser {
|
|
|
207
237
|
if (newContextCreated) {
|
|
208
238
|
this.registerEventListeners(this.context);
|
|
209
239
|
await this.goto(this.context.environment.baseUrl);
|
|
210
|
-
if (!this.fastMode) {
|
|
240
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
211
241
|
await this.waitForPageLoad();
|
|
212
242
|
}
|
|
213
243
|
}
|
|
@@ -335,6 +365,64 @@ class StableBrowser {
|
|
|
335
365
|
await _commandFinally(state, this);
|
|
336
366
|
}
|
|
337
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
|
+
}
|
|
338
426
|
async _getLocator(locator, scope, _params) {
|
|
339
427
|
locator = _fixLocatorUsingParams(locator, _params);
|
|
340
428
|
// locator = await this._replaceWithLocalData(locator);
|
|
@@ -433,12 +521,6 @@ class StableBrowser {
|
|
|
433
521
|
if (!el.setAttribute) {
|
|
434
522
|
el = el.parentElement;
|
|
435
523
|
}
|
|
436
|
-
// remove any attributes start with data-blinq-id
|
|
437
|
-
// for (let i = 0; i < el.attributes.length; i++) {
|
|
438
|
-
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
439
|
-
// el.removeAttribute(el.attributes[i].name);
|
|
440
|
-
// }
|
|
441
|
-
// }
|
|
442
524
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
443
525
|
return true;
|
|
444
526
|
}, [tag1, randomToken]))) {
|
|
@@ -460,14 +542,13 @@ class StableBrowser {
|
|
|
460
542
|
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
461
543
|
}
|
|
462
544
|
let locatorSearch = selectorHierarchy[index];
|
|
463
|
-
let originalLocatorSearch = "";
|
|
464
545
|
try {
|
|
465
|
-
|
|
466
|
-
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
546
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
467
547
|
}
|
|
468
548
|
catch (e) {
|
|
469
549
|
console.error(e);
|
|
470
550
|
}
|
|
551
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
471
552
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
472
553
|
let locator = null;
|
|
473
554
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
@@ -609,34 +690,205 @@ class StableBrowser {
|
|
|
609
690
|
}
|
|
610
691
|
return { rerun: false };
|
|
611
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
|
+
}
|
|
612
757
|
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
613
758
|
if (!timeout) {
|
|
614
759
|
timeout = 30000;
|
|
615
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
|
+
}
|
|
616
768
|
for (let i = 0; i < 3; i++) {
|
|
617
769
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
618
770
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
619
771
|
let selector = selectors.locators[j];
|
|
620
772
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
621
773
|
}
|
|
622
|
-
|
|
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
|
+
}
|
|
623
817
|
if (!element.rerun) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
+
newElementSelector = await element.evaluate((el, token) => {
|
|
855
|
+
const id = el.id || "";
|
|
856
|
+
if (id) {
|
|
857
|
+
try {
|
|
858
|
+
// count elements with this id
|
|
859
|
+
const count = document.querySelectorAll(`#${CSS.escape(id)}`).length;
|
|
860
|
+
if (count === 1) {
|
|
861
|
+
// unique id → use it
|
|
862
|
+
return `#${CSS.escape(id)}`;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
// duplicate id → fallback to custom attribute
|
|
866
|
+
const attrName = `data-blinq-id-${token}`;
|
|
867
|
+
el.setAttribute(attrName, "");
|
|
868
|
+
return `[${attrName}]`;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
// invalid CSS chars in id → fallback to XPath
|
|
873
|
+
return `//*[@id="${id.replace(/"/g, '\\"')}"]`;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
// no id → assign the random token as the element's id
|
|
878
|
+
el.setAttribute("id", token);
|
|
879
|
+
return `#${token}`;
|
|
880
|
+
}
|
|
881
|
+
}, randomToken);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
631
884
|
const scope = element._frame ?? element.page();
|
|
632
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
633
885
|
let prefixSelector = "";
|
|
634
886
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
635
887
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
636
888
|
if (frameSelectorIndex !== -1) {
|
|
637
889
|
// remove everything after the >> internal:control=enter-frame
|
|
638
890
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
639
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
891
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
640
892
|
}
|
|
641
893
|
// if (element?._frame?._selector) {
|
|
642
894
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
@@ -675,7 +927,7 @@ class StableBrowser {
|
|
|
675
927
|
break;
|
|
676
928
|
}
|
|
677
929
|
catch (error) {
|
|
678
|
-
console.error("frame not found " + frameLocator.css);
|
|
930
|
+
// console.error("frame not found " + frameLocator.css);
|
|
679
931
|
}
|
|
680
932
|
}
|
|
681
933
|
}
|
|
@@ -753,7 +1005,6 @@ class StableBrowser {
|
|
|
753
1005
|
let locatorsCount = 0;
|
|
754
1006
|
let lazy_scroll = false;
|
|
755
1007
|
//let arrayMode = Array.isArray(selectors);
|
|
756
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
757
1008
|
let selectorsLocators = null;
|
|
758
1009
|
selectorsLocators = selectors.locators;
|
|
759
1010
|
// group selectors by priority
|
|
@@ -781,6 +1032,7 @@ class StableBrowser {
|
|
|
781
1032
|
let highPriorityOnly = true;
|
|
782
1033
|
let visibleOnly = true;
|
|
783
1034
|
while (true) {
|
|
1035
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
784
1036
|
locatorsCount = 0;
|
|
785
1037
|
let result = [];
|
|
786
1038
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -896,9 +1148,13 @@ class StableBrowser {
|
|
|
896
1148
|
}
|
|
897
1149
|
}
|
|
898
1150
|
if (foundLocators.length === 1) {
|
|
1151
|
+
let box = null;
|
|
1152
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1153
|
+
box = await foundLocators[0].boundingBox();
|
|
1154
|
+
}
|
|
899
1155
|
result.foundElements.push({
|
|
900
1156
|
locator: foundLocators[0],
|
|
901
|
-
box:
|
|
1157
|
+
box: box,
|
|
902
1158
|
unique: true,
|
|
903
1159
|
});
|
|
904
1160
|
result.locatorIndex = i;
|
|
@@ -1053,11 +1309,16 @@ class StableBrowser {
|
|
|
1053
1309
|
operation: "click",
|
|
1054
1310
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1055
1311
|
};
|
|
1312
|
+
check_performance("click_all ***", this.context, true);
|
|
1056
1313
|
try {
|
|
1314
|
+
check_performance("click_preCommand", this.context, true);
|
|
1057
1315
|
await _preCommand(state, this);
|
|
1316
|
+
check_performance("click_preCommand", this.context, false);
|
|
1058
1317
|
await performAction("click", state.element, options, this, state, _params);
|
|
1059
|
-
if (!this.fastMode) {
|
|
1060
|
-
|
|
1318
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1319
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1320
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1321
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1061
1322
|
}
|
|
1062
1323
|
return state.info;
|
|
1063
1324
|
}
|
|
@@ -1065,7 +1326,13 @@ class StableBrowser {
|
|
|
1065
1326
|
await _commandError(state, e, this);
|
|
1066
1327
|
}
|
|
1067
1328
|
finally {
|
|
1329
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1068
1330
|
await _commandFinally(state, this);
|
|
1331
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1332
|
+
check_performance("click_all ***", this.context, false);
|
|
1333
|
+
if (this.context.profile) {
|
|
1334
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1335
|
+
}
|
|
1069
1336
|
}
|
|
1070
1337
|
}
|
|
1071
1338
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1157,7 +1424,7 @@ class StableBrowser {
|
|
|
1157
1424
|
}
|
|
1158
1425
|
}
|
|
1159
1426
|
}
|
|
1160
|
-
await this.waitForPageLoad();
|
|
1427
|
+
//await this.waitForPageLoad();
|
|
1161
1428
|
return state.info;
|
|
1162
1429
|
}
|
|
1163
1430
|
catch (e) {
|
|
@@ -1183,7 +1450,7 @@ class StableBrowser {
|
|
|
1183
1450
|
await _preCommand(state, this);
|
|
1184
1451
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1185
1452
|
await _screenshot(state, this);
|
|
1186
|
-
await this.waitForPageLoad();
|
|
1453
|
+
//await this.waitForPageLoad();
|
|
1187
1454
|
return state.info;
|
|
1188
1455
|
}
|
|
1189
1456
|
catch (e) {
|
|
@@ -1219,7 +1486,7 @@ class StableBrowser {
|
|
|
1219
1486
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1220
1487
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1221
1488
|
}
|
|
1222
|
-
await this.waitForPageLoad();
|
|
1489
|
+
//await this.waitForPageLoad();
|
|
1223
1490
|
return state.info;
|
|
1224
1491
|
}
|
|
1225
1492
|
catch (e) {
|
|
@@ -1501,8 +1768,8 @@ class StableBrowser {
|
|
|
1501
1768
|
if (enter) {
|
|
1502
1769
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1503
1770
|
await this.page.keyboard.press("Enter");
|
|
1771
|
+
await this.waitForPageLoad();
|
|
1504
1772
|
}
|
|
1505
|
-
await this.waitForPageLoad();
|
|
1506
1773
|
return state.info;
|
|
1507
1774
|
}
|
|
1508
1775
|
catch (e) {
|
|
@@ -1957,12 +2224,7 @@ class StableBrowser {
|
|
|
1957
2224
|
}
|
|
1958
2225
|
}
|
|
1959
2226
|
getTestData(world = null) {
|
|
1960
|
-
|
|
1961
|
-
let data = {};
|
|
1962
|
-
if (fs.existsSync(dataFile)) {
|
|
1963
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1964
|
-
}
|
|
1965
|
-
return data;
|
|
2227
|
+
return _getTestData(world, this.context, this);
|
|
1966
2228
|
}
|
|
1967
2229
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1968
2230
|
// collect url/path/title
|
|
@@ -2182,6 +2444,77 @@ class StableBrowser {
|
|
|
2182
2444
|
await _commandFinally(state, this);
|
|
2183
2445
|
}
|
|
2184
2446
|
}
|
|
2447
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2448
|
+
const state = {
|
|
2449
|
+
selectors,
|
|
2450
|
+
_params,
|
|
2451
|
+
property,
|
|
2452
|
+
variable,
|
|
2453
|
+
options,
|
|
2454
|
+
world,
|
|
2455
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2456
|
+
text: `Extract property from element`,
|
|
2457
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2458
|
+
operation: "extractProperty",
|
|
2459
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2460
|
+
allowDisabled: true,
|
|
2461
|
+
};
|
|
2462
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2463
|
+
try {
|
|
2464
|
+
await _preCommand(state, this);
|
|
2465
|
+
switch (property) {
|
|
2466
|
+
case "inner_text":
|
|
2467
|
+
state.value = await state.element.innerText();
|
|
2468
|
+
break;
|
|
2469
|
+
case "href":
|
|
2470
|
+
state.value = await state.element.getAttribute("href");
|
|
2471
|
+
break;
|
|
2472
|
+
case "value":
|
|
2473
|
+
state.value = await state.element.inputValue();
|
|
2474
|
+
break;
|
|
2475
|
+
case "text":
|
|
2476
|
+
state.value = await state.element.textContent();
|
|
2477
|
+
break;
|
|
2478
|
+
default:
|
|
2479
|
+
if (property.startsWith("dataset.")) {
|
|
2480
|
+
const dataAttribute = property.substring(8);
|
|
2481
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2482
|
+
}
|
|
2483
|
+
else {
|
|
2484
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
if (options !== null) {
|
|
2488
|
+
if (options.regex && options.regex !== "") {
|
|
2489
|
+
// Construct a regex pattern from the provided string
|
|
2490
|
+
const regex = options.regex.slice(1, -1);
|
|
2491
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2492
|
+
const matches = state.value.match(regexPattern);
|
|
2493
|
+
if (matches) {
|
|
2494
|
+
let newValue = "";
|
|
2495
|
+
for (const match of matches) {
|
|
2496
|
+
newValue += match;
|
|
2497
|
+
}
|
|
2498
|
+
state.value = newValue;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2502
|
+
state.value = state.value.trim();
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
state.info.value = state.value;
|
|
2506
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2507
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2508
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2509
|
+
return state.info;
|
|
2510
|
+
}
|
|
2511
|
+
catch (e) {
|
|
2512
|
+
await _commandError(state, e, this);
|
|
2513
|
+
}
|
|
2514
|
+
finally {
|
|
2515
|
+
await _commandFinally(state, this);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2185
2518
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2186
2519
|
const state = {
|
|
2187
2520
|
selectors,
|
|
@@ -2280,6 +2613,261 @@ class StableBrowser {
|
|
|
2280
2613
|
await _commandFinally(state, this);
|
|
2281
2614
|
}
|
|
2282
2615
|
}
|
|
2616
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2617
|
+
const state = {
|
|
2618
|
+
selectors,
|
|
2619
|
+
_params,
|
|
2620
|
+
property,
|
|
2621
|
+
value,
|
|
2622
|
+
options,
|
|
2623
|
+
world,
|
|
2624
|
+
type: Types.VERIFY_PROPERTY,
|
|
2625
|
+
highlight: true,
|
|
2626
|
+
screenshot: true,
|
|
2627
|
+
text: `Verify element property`,
|
|
2628
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2629
|
+
operation: "verifyProperty",
|
|
2630
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2631
|
+
allowDisabled: true,
|
|
2632
|
+
};
|
|
2633
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2634
|
+
let val;
|
|
2635
|
+
let expectedValue;
|
|
2636
|
+
try {
|
|
2637
|
+
await _preCommand(state, this);
|
|
2638
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2639
|
+
state.info.expectedValue = expectedValue;
|
|
2640
|
+
switch (property) {
|
|
2641
|
+
case "innerText":
|
|
2642
|
+
val = String(await state.element.innerText());
|
|
2643
|
+
break;
|
|
2644
|
+
case "text":
|
|
2645
|
+
val = String(await state.element.textContent());
|
|
2646
|
+
break;
|
|
2647
|
+
case "value":
|
|
2648
|
+
val = String(await state.element.inputValue());
|
|
2649
|
+
break;
|
|
2650
|
+
case "checked":
|
|
2651
|
+
val = String(await state.element.isChecked());
|
|
2652
|
+
break;
|
|
2653
|
+
case "disabled":
|
|
2654
|
+
val = String(await state.element.isDisabled());
|
|
2655
|
+
break;
|
|
2656
|
+
case "readOnly":
|
|
2657
|
+
const isEditable = await state.element.isEditable();
|
|
2658
|
+
val = String(!isEditable);
|
|
2659
|
+
break;
|
|
2660
|
+
case "innerHTML":
|
|
2661
|
+
val = String(await state.element.innerHTML());
|
|
2662
|
+
break;
|
|
2663
|
+
case "outerHTML":
|
|
2664
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2665
|
+
break;
|
|
2666
|
+
default:
|
|
2667
|
+
if (property.startsWith("dataset.")) {
|
|
2668
|
+
const dataAttribute = property.substring(8);
|
|
2669
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2670
|
+
}
|
|
2671
|
+
else {
|
|
2672
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
// Helper function to remove all style="" attributes
|
|
2676
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2677
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2678
|
+
};
|
|
2679
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2680
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2681
|
+
val = removeStyleAttributes(val);
|
|
2682
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2683
|
+
}
|
|
2684
|
+
state.info.value = val;
|
|
2685
|
+
let regex;
|
|
2686
|
+
state.info.value = val;
|
|
2687
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2688
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2689
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2690
|
+
let matchPassed = false;
|
|
2691
|
+
if (isRegex) {
|
|
2692
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2693
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2694
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2695
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2696
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2697
|
+
const regex = new RegExp(patternBody, flags);
|
|
2698
|
+
state.info.regex = true;
|
|
2699
|
+
matchPassed = regex.test(val);
|
|
2700
|
+
}
|
|
2701
|
+
else {
|
|
2702
|
+
// Fallback: treat as literal
|
|
2703
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2704
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2705
|
+
matchPassed = regex.test(val);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
else if (isContains) {
|
|
2709
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2710
|
+
matchPassed = val.includes(containsValue);
|
|
2711
|
+
}
|
|
2712
|
+
else if (isExact) {
|
|
2713
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2714
|
+
matchPassed = val === exactValue;
|
|
2715
|
+
}
|
|
2716
|
+
else if (property === "innerText") {
|
|
2717
|
+
// Default innerText logic
|
|
2718
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2719
|
+
const valLines = val.split("\n");
|
|
2720
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2721
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2722
|
+
}
|
|
2723
|
+
else {
|
|
2724
|
+
// Fallback exact or loose match
|
|
2725
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2726
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2727
|
+
matchPassed = regex.test(val);
|
|
2728
|
+
}
|
|
2729
|
+
if (!matchPassed) {
|
|
2730
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2731
|
+
state.info.failCause.assertionFailed = true;
|
|
2732
|
+
state.info.failCause.lastError = errorMessage;
|
|
2733
|
+
throw new Error(errorMessage);
|
|
2734
|
+
}
|
|
2735
|
+
return state.info;
|
|
2736
|
+
}
|
|
2737
|
+
catch (e) {
|
|
2738
|
+
await _commandError(state, e, this);
|
|
2739
|
+
}
|
|
2740
|
+
finally {
|
|
2741
|
+
await _commandFinally(state, this);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2745
|
+
// Convert timeout from seconds to milliseconds
|
|
2746
|
+
const timeoutMs = timeout * 1000;
|
|
2747
|
+
const state = {
|
|
2748
|
+
selectors,
|
|
2749
|
+
_params,
|
|
2750
|
+
condition,
|
|
2751
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2752
|
+
options,
|
|
2753
|
+
world,
|
|
2754
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2755
|
+
highlight: true,
|
|
2756
|
+
screenshot: true,
|
|
2757
|
+
text: `Conditional wait for element`,
|
|
2758
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2759
|
+
operation: "conditionalWait",
|
|
2760
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2761
|
+
allowDisabled: true,
|
|
2762
|
+
info: {},
|
|
2763
|
+
};
|
|
2764
|
+
state.options ??= { timeout: timeoutMs };
|
|
2765
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2766
|
+
const startTime = Date.now();
|
|
2767
|
+
let conditionMet = false;
|
|
2768
|
+
let currentValue = null;
|
|
2769
|
+
let lastError = null;
|
|
2770
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2771
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2772
|
+
const elapsedTime = Date.now() - startTime;
|
|
2773
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2774
|
+
try {
|
|
2775
|
+
// Try to execute _preCommand (element location)
|
|
2776
|
+
await _preCommand(state, this);
|
|
2777
|
+
// If _preCommand succeeds, start condition checking
|
|
2778
|
+
const checkCondition = async () => {
|
|
2779
|
+
try {
|
|
2780
|
+
switch (condition.toLowerCase()) {
|
|
2781
|
+
case "checked":
|
|
2782
|
+
currentValue = await state.element.isChecked();
|
|
2783
|
+
return currentValue === true;
|
|
2784
|
+
case "unchecked":
|
|
2785
|
+
currentValue = await state.element.isChecked();
|
|
2786
|
+
return currentValue === false;
|
|
2787
|
+
case "visible":
|
|
2788
|
+
currentValue = await state.element.isVisible();
|
|
2789
|
+
return currentValue === true;
|
|
2790
|
+
case "hidden":
|
|
2791
|
+
currentValue = await state.element.isVisible();
|
|
2792
|
+
return currentValue === false;
|
|
2793
|
+
case "enabled":
|
|
2794
|
+
currentValue = await state.element.isDisabled();
|
|
2795
|
+
return currentValue === false;
|
|
2796
|
+
case "disabled":
|
|
2797
|
+
currentValue = await state.element.isDisabled();
|
|
2798
|
+
return currentValue === true;
|
|
2799
|
+
case "editable":
|
|
2800
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2801
|
+
currentValue = await state.element.isContentEditable();
|
|
2802
|
+
return currentValue === true;
|
|
2803
|
+
default:
|
|
2804
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2805
|
+
state.info.success = false;
|
|
2806
|
+
return false;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
catch (error) {
|
|
2810
|
+
// Don't throw here, just return false to continue retrying
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
// Inner loop for condition checking (once element is located)
|
|
2815
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2816
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2817
|
+
conditionMet = await checkCondition();
|
|
2818
|
+
if (conditionMet) {
|
|
2819
|
+
break;
|
|
2820
|
+
}
|
|
2821
|
+
// Check if we still have time for another attempt
|
|
2822
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2823
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2824
|
+
}
|
|
2825
|
+
else {
|
|
2826
|
+
break;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
// If we got here and condition is met, break out of main loop
|
|
2830
|
+
if (conditionMet) {
|
|
2831
|
+
break;
|
|
2832
|
+
}
|
|
2833
|
+
// If condition not met but no exception, we've timed out
|
|
2834
|
+
break;
|
|
2835
|
+
}
|
|
2836
|
+
catch (e) {
|
|
2837
|
+
lastError = e;
|
|
2838
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2839
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2840
|
+
// Check if we have enough time left to retry
|
|
2841
|
+
if (timeLeft > 100) {
|
|
2842
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2843
|
+
}
|
|
2844
|
+
else {
|
|
2845
|
+
break;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2850
|
+
state.info = {
|
|
2851
|
+
success: conditionMet,
|
|
2852
|
+
conditionMet,
|
|
2853
|
+
actualWaitTime,
|
|
2854
|
+
currentValue,
|
|
2855
|
+
lastError: lastError?.message || null,
|
|
2856
|
+
message: conditionMet
|
|
2857
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2858
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2859
|
+
};
|
|
2860
|
+
if (lastError) {
|
|
2861
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2862
|
+
}
|
|
2863
|
+
try {
|
|
2864
|
+
await _commandFinally(state, this);
|
|
2865
|
+
}
|
|
2866
|
+
catch (finallyError) {
|
|
2867
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2868
|
+
}
|
|
2869
|
+
return state.info;
|
|
2870
|
+
}
|
|
2283
2871
|
async extractEmailData(emailAddress, options, world) {
|
|
2284
2872
|
if (!emailAddress) {
|
|
2285
2873
|
throw new Error("email address is null");
|
|
@@ -2876,6 +3464,8 @@ class StableBrowser {
|
|
|
2876
3464
|
operation: "verify_text_with_relation",
|
|
2877
3465
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2878
3466
|
};
|
|
3467
|
+
const cmdStartTime = Date.now();
|
|
3468
|
+
let cmdEndTime = null;
|
|
2879
3469
|
const timeout = this._getFindElementTimeout(options);
|
|
2880
3470
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2881
3471
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -2911,6 +3501,17 @@ class StableBrowser {
|
|
|
2911
3501
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2912
3502
|
continue;
|
|
2913
3503
|
}
|
|
3504
|
+
else {
|
|
3505
|
+
cmdEndTime = Date.now();
|
|
3506
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3507
|
+
if (foundAncore) {
|
|
3508
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3509
|
+
}
|
|
3510
|
+
else {
|
|
3511
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
2914
3515
|
try {
|
|
2915
3516
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2916
3517
|
foundAncore = true;
|
|
@@ -3049,7 +3650,7 @@ class StableBrowser {
|
|
|
3049
3650
|
Object.assign(e, { info: info });
|
|
3050
3651
|
error = e;
|
|
3051
3652
|
// throw e;
|
|
3052
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3653
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3053
3654
|
}
|
|
3054
3655
|
finally {
|
|
3055
3656
|
const endTime = Date.now();
|
|
@@ -3398,6 +3999,22 @@ class StableBrowser {
|
|
|
3398
3999
|
}
|
|
3399
4000
|
}
|
|
3400
4001
|
async waitForPageLoad(options = {}, world = null) {
|
|
4002
|
+
// try {
|
|
4003
|
+
// let currentPagePath = null;
|
|
4004
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4005
|
+
// if (this.latestPagePath) {
|
|
4006
|
+
// // get the currect page path and compare with the latest page path
|
|
4007
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4008
|
+
// // if the page path is the same, do not wait for page load
|
|
4009
|
+
// console.log("No page change: " + currentPagePath);
|
|
4010
|
+
// return;
|
|
4011
|
+
// }
|
|
4012
|
+
// }
|
|
4013
|
+
// this.latestPagePath = currentPagePath;
|
|
4014
|
+
// } catch (e) {
|
|
4015
|
+
// console.debug("Error getting current page path: ", e);
|
|
4016
|
+
// }
|
|
4017
|
+
//console.log("Waiting for page load");
|
|
3401
4018
|
let timeout = this._getLoadTimeout(options);
|
|
3402
4019
|
const promiseArray = [];
|
|
3403
4020
|
// let waitForNetworkIdle = true;
|
|
@@ -3430,10 +4047,12 @@ class StableBrowser {
|
|
|
3430
4047
|
else if (e.label === "domcontentloaded") {
|
|
3431
4048
|
console.log("waited for the domcontent loaded timeout");
|
|
3432
4049
|
}
|
|
3433
|
-
console.log(".");
|
|
3434
4050
|
}
|
|
3435
4051
|
finally {
|
|
3436
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4052
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4053
|
+
if (options && !options.noSleep) {
|
|
4054
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4055
|
+
}
|
|
3437
4056
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3438
4057
|
const endTime = Date.now();
|
|
3439
4058
|
_reportToWorld(world, {
|
|
@@ -3474,7 +4093,6 @@ class StableBrowser {
|
|
|
3474
4093
|
await this.page.close();
|
|
3475
4094
|
}
|
|
3476
4095
|
catch (e) {
|
|
3477
|
-
console.log(".");
|
|
3478
4096
|
await _commandError(state, e, this);
|
|
3479
4097
|
}
|
|
3480
4098
|
finally {
|
|
@@ -3488,7 +4106,7 @@ class StableBrowser {
|
|
|
3488
4106
|
}
|
|
3489
4107
|
operation = options.operation;
|
|
3490
4108
|
// validate operation is one of the supported operations
|
|
3491
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4109
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3492
4110
|
throw new Error("operation is not supported");
|
|
3493
4111
|
}
|
|
3494
4112
|
const state = {
|
|
@@ -3557,6 +4175,17 @@ class StableBrowser {
|
|
|
3557
4175
|
state.element = results[0];
|
|
3558
4176
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3559
4177
|
break;
|
|
4178
|
+
case "hover":
|
|
4179
|
+
if (!options.css) {
|
|
4180
|
+
throw new Error("css is not defined");
|
|
4181
|
+
}
|
|
4182
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4183
|
+
if (result1.length === 0) {
|
|
4184
|
+
throw new Error(`Element not found in cell area`);
|
|
4185
|
+
}
|
|
4186
|
+
state.element = result1[0];
|
|
4187
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4188
|
+
break;
|
|
3560
4189
|
default:
|
|
3561
4190
|
throw new Error("operation is not supported");
|
|
3562
4191
|
}
|
|
@@ -3589,7 +4218,6 @@ class StableBrowser {
|
|
|
3589
4218
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3590
4219
|
}
|
|
3591
4220
|
catch (e) {
|
|
3592
|
-
console.log(".");
|
|
3593
4221
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3594
4222
|
}
|
|
3595
4223
|
finally {
|
|
@@ -3627,7 +4255,6 @@ class StableBrowser {
|
|
|
3627
4255
|
await this.page.reload();
|
|
3628
4256
|
}
|
|
3629
4257
|
catch (e) {
|
|
3630
|
-
console.log(".");
|
|
3631
4258
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3632
4259
|
}
|
|
3633
4260
|
finally {
|
|
@@ -3671,6 +4298,10 @@ class StableBrowser {
|
|
|
3671
4298
|
}
|
|
3672
4299
|
}
|
|
3673
4300
|
async beforeScenario(world, scenario) {
|
|
4301
|
+
if (world && world.attach) {
|
|
4302
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4303
|
+
}
|
|
4304
|
+
this.context.loadedRoutes = null;
|
|
3674
4305
|
this.beforeScenarioCalled = true;
|
|
3675
4306
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3676
4307
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3700,8 +4331,10 @@ class StableBrowser {
|
|
|
3700
4331
|
}
|
|
3701
4332
|
async afterScenario(world, scenario) { }
|
|
3702
4333
|
async beforeStep(world, step) {
|
|
4334
|
+
this.stepTags = [];
|
|
3703
4335
|
if (!this.beforeScenarioCalled) {
|
|
3704
4336
|
this.beforeScenario(world, step);
|
|
4337
|
+
this.context.loadedRoutes = null;
|
|
3705
4338
|
}
|
|
3706
4339
|
if (this.stepIndex === undefined) {
|
|
3707
4340
|
this.stepIndex = 0;
|
|
@@ -3711,7 +4344,12 @@ class StableBrowser {
|
|
|
3711
4344
|
}
|
|
3712
4345
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3713
4346
|
this.stepName = step.pickleStep.text;
|
|
3714
|
-
|
|
4347
|
+
let printableStepName = this.stepName;
|
|
4348
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4349
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4350
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4351
|
+
});
|
|
4352
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
3715
4353
|
}
|
|
3716
4354
|
else if (step && step.text) {
|
|
3717
4355
|
this.stepName = step.text;
|
|
@@ -3726,13 +4364,23 @@ class StableBrowser {
|
|
|
3726
4364
|
}
|
|
3727
4365
|
if (this.initSnapshotTaken === false) {
|
|
3728
4366
|
this.initSnapshotTaken = true;
|
|
3729
|
-
if (world &&
|
|
4367
|
+
if (world &&
|
|
4368
|
+
world.attach &&
|
|
4369
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4370
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
3730
4371
|
const snapshot = await this.getAriaSnapshot();
|
|
3731
4372
|
if (snapshot) {
|
|
3732
4373
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3733
4374
|
}
|
|
3734
4375
|
}
|
|
3735
4376
|
}
|
|
4377
|
+
this.context.routeResults = null;
|
|
4378
|
+
this.context.loadedRoutes = null;
|
|
4379
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4380
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4381
|
+
}
|
|
4382
|
+
setStepTags(tags) {
|
|
4383
|
+
this.stepTags = tags;
|
|
3736
4384
|
}
|
|
3737
4385
|
async getAriaSnapshot() {
|
|
3738
4386
|
try {
|
|
@@ -3752,12 +4400,18 @@ class StableBrowser {
|
|
|
3752
4400
|
try {
|
|
3753
4401
|
// Ensure frame is attached and has body
|
|
3754
4402
|
const body = frame.locator("body");
|
|
3755
|
-
await body.waitFor({ timeout:
|
|
4403
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
3756
4404
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4405
|
+
if (!snapshot) {
|
|
4406
|
+
continue;
|
|
4407
|
+
}
|
|
3757
4408
|
content.push(`- frame: ${i}`);
|
|
3758
4409
|
content.push(snapshot);
|
|
3759
4410
|
}
|
|
3760
|
-
catch (innerErr) {
|
|
4411
|
+
catch (innerErr) {
|
|
4412
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4413
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4414
|
+
}
|
|
3761
4415
|
}
|
|
3762
4416
|
return content.join("\n");
|
|
3763
4417
|
}
|
|
@@ -3829,13 +4483,23 @@ class StableBrowser {
|
|
|
3829
4483
|
if (this.context) {
|
|
3830
4484
|
this.context.examplesRow = null;
|
|
3831
4485
|
}
|
|
3832
|
-
if (world &&
|
|
4486
|
+
if (world &&
|
|
4487
|
+
world.attach &&
|
|
4488
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4489
|
+
!this.fastMode &&
|
|
4490
|
+
!this.stepTags.includes("fast-mode")) {
|
|
3833
4491
|
const snapshot = await this.getAriaSnapshot();
|
|
3834
4492
|
if (snapshot) {
|
|
3835
4493
|
const obj = {};
|
|
3836
4494
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3837
4495
|
}
|
|
3838
4496
|
}
|
|
4497
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4498
|
+
if (this.context.routeResults) {
|
|
4499
|
+
if (world && world.attach) {
|
|
4500
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
3839
4503
|
if (!process.env.TEMP_RUN) {
|
|
3840
4504
|
const state = {
|
|
3841
4505
|
world,
|
|
@@ -3859,6 +4523,13 @@ class StableBrowser {
|
|
|
3859
4523
|
await _commandFinally(state, this);
|
|
3860
4524
|
}
|
|
3861
4525
|
}
|
|
4526
|
+
networkAfterStep(this.stepName, this.context);
|
|
4527
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4528
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4529
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4530
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
3862
4533
|
}
|
|
3863
4534
|
}
|
|
3864
4535
|
function createTimedPromise(promise, label) {
|