automation_model 1.0.743-dev → 1.0.743-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 +15 -4
- package/lib/stable_browser.js +561 -93
- 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",
|
|
@@ -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,197 @@ 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
|
+
newElementSelector = await element.evaluate((el, token) => {
|
|
855
|
+
const id = el.id || "";
|
|
856
|
+
if (id) {
|
|
857
|
+
// use attribute and not id
|
|
858
|
+
const attrName = `data-blinq-id-${token}`;
|
|
859
|
+
el.setAttribute(attrName, "");
|
|
860
|
+
return `[${attrName}]`;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
// no id → assign the random token as the element's id
|
|
864
|
+
el.setAttribute("id", token);
|
|
865
|
+
return `#${token}`;
|
|
866
|
+
}
|
|
867
|
+
}, randomToken);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
633
870
|
const scope = element._frame ?? element.page();
|
|
634
|
-
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
635
871
|
let prefixSelector = "";
|
|
636
872
|
const frameControlSelector = " >> internal:control=enter-frame";
|
|
637
873
|
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
638
874
|
if (frameSelectorIndex !== -1) {
|
|
639
875
|
// remove everything after the >> internal:control=enter-frame
|
|
640
876
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
641
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
877
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
642
878
|
}
|
|
643
879
|
// if (element?._frame?._selector) {
|
|
644
880
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
645
881
|
// }
|
|
646
882
|
const newSelector = prefixSelector + newElementSelector;
|
|
647
|
-
return scope.locator(newSelector);
|
|
883
|
+
return scope.locator(newSelector).first();
|
|
648
884
|
}
|
|
649
885
|
}
|
|
650
886
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -677,7 +913,7 @@ class StableBrowser {
|
|
|
677
913
|
break;
|
|
678
914
|
}
|
|
679
915
|
catch (error) {
|
|
680
|
-
console.error("frame not found " + frameLocator.css);
|
|
916
|
+
// console.error("frame not found " + frameLocator.css);
|
|
681
917
|
}
|
|
682
918
|
}
|
|
683
919
|
}
|
|
@@ -743,6 +979,11 @@ class StableBrowser {
|
|
|
743
979
|
});
|
|
744
980
|
}
|
|
745
981
|
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
982
|
+
if (selectors.locators && Array.isArray(selectors.locators)) {
|
|
983
|
+
selectors.locators.forEach((locator) => {
|
|
984
|
+
locator.index = locator.index ?? 0;
|
|
985
|
+
});
|
|
986
|
+
}
|
|
746
987
|
if (!info) {
|
|
747
988
|
info = {};
|
|
748
989
|
info.failCause = {};
|
|
@@ -755,7 +996,6 @@ class StableBrowser {
|
|
|
755
996
|
let locatorsCount = 0;
|
|
756
997
|
let lazy_scroll = false;
|
|
757
998
|
//let arrayMode = Array.isArray(selectors);
|
|
758
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
759
999
|
let selectorsLocators = null;
|
|
760
1000
|
selectorsLocators = selectors.locators;
|
|
761
1001
|
// group selectors by priority
|
|
@@ -783,6 +1023,7 @@ class StableBrowser {
|
|
|
783
1023
|
let highPriorityOnly = true;
|
|
784
1024
|
let visibleOnly = true;
|
|
785
1025
|
while (true) {
|
|
1026
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
786
1027
|
locatorsCount = 0;
|
|
787
1028
|
let result = [];
|
|
788
1029
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -898,9 +1139,13 @@ class StableBrowser {
|
|
|
898
1139
|
}
|
|
899
1140
|
}
|
|
900
1141
|
if (foundLocators.length === 1) {
|
|
1142
|
+
let box = null;
|
|
1143
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1144
|
+
box = await foundLocators[0].boundingBox();
|
|
1145
|
+
}
|
|
901
1146
|
result.foundElements.push({
|
|
902
1147
|
locator: foundLocators[0],
|
|
903
|
-
box:
|
|
1148
|
+
box: box,
|
|
904
1149
|
unique: true,
|
|
905
1150
|
});
|
|
906
1151
|
result.locatorIndex = i;
|
|
@@ -1055,11 +1300,16 @@ class StableBrowser {
|
|
|
1055
1300
|
operation: "click",
|
|
1056
1301
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1057
1302
|
};
|
|
1303
|
+
check_performance("click_all ***", this.context, true);
|
|
1058
1304
|
try {
|
|
1305
|
+
check_performance("click_preCommand", this.context, true);
|
|
1059
1306
|
await _preCommand(state, this);
|
|
1307
|
+
check_performance("click_preCommand", this.context, false);
|
|
1060
1308
|
await performAction("click", state.element, options, this, state, _params);
|
|
1061
|
-
if (!this.fastMode) {
|
|
1062
|
-
|
|
1309
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1310
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1311
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1312
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1063
1313
|
}
|
|
1064
1314
|
return state.info;
|
|
1065
1315
|
}
|
|
@@ -1067,7 +1317,13 @@ class StableBrowser {
|
|
|
1067
1317
|
await _commandError(state, e, this);
|
|
1068
1318
|
}
|
|
1069
1319
|
finally {
|
|
1320
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1070
1321
|
await _commandFinally(state, this);
|
|
1322
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1323
|
+
check_performance("click_all ***", this.context, false);
|
|
1324
|
+
if (this.context.profile) {
|
|
1325
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1326
|
+
}
|
|
1071
1327
|
}
|
|
1072
1328
|
}
|
|
1073
1329
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1159,7 +1415,7 @@ class StableBrowser {
|
|
|
1159
1415
|
}
|
|
1160
1416
|
}
|
|
1161
1417
|
}
|
|
1162
|
-
await this.waitForPageLoad();
|
|
1418
|
+
//await this.waitForPageLoad();
|
|
1163
1419
|
return state.info;
|
|
1164
1420
|
}
|
|
1165
1421
|
catch (e) {
|
|
@@ -1185,7 +1441,7 @@ class StableBrowser {
|
|
|
1185
1441
|
await _preCommand(state, this);
|
|
1186
1442
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1187
1443
|
await _screenshot(state, this);
|
|
1188
|
-
await this.waitForPageLoad();
|
|
1444
|
+
//await this.waitForPageLoad();
|
|
1189
1445
|
return state.info;
|
|
1190
1446
|
}
|
|
1191
1447
|
catch (e) {
|
|
@@ -1221,7 +1477,7 @@ class StableBrowser {
|
|
|
1221
1477
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1222
1478
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1223
1479
|
}
|
|
1224
|
-
await this.waitForPageLoad();
|
|
1480
|
+
//await this.waitForPageLoad();
|
|
1225
1481
|
return state.info;
|
|
1226
1482
|
}
|
|
1227
1483
|
catch (e) {
|
|
@@ -1503,8 +1759,8 @@ class StableBrowser {
|
|
|
1503
1759
|
if (enter) {
|
|
1504
1760
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1505
1761
|
await this.page.keyboard.press("Enter");
|
|
1762
|
+
await this.waitForPageLoad();
|
|
1506
1763
|
}
|
|
1507
|
-
await this.waitForPageLoad();
|
|
1508
1764
|
return state.info;
|
|
1509
1765
|
}
|
|
1510
1766
|
catch (e) {
|
|
@@ -1959,12 +2215,7 @@ class StableBrowser {
|
|
|
1959
2215
|
}
|
|
1960
2216
|
}
|
|
1961
2217
|
getTestData(world = null) {
|
|
1962
|
-
|
|
1963
|
-
let data = {};
|
|
1964
|
-
if (fs.existsSync(dataFile)) {
|
|
1965
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1966
|
-
}
|
|
1967
|
-
return data;
|
|
2218
|
+
return _getTestData(world, this.context, this);
|
|
1968
2219
|
}
|
|
1969
2220
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1970
2221
|
// collect url/path/title
|
|
@@ -2414,7 +2665,7 @@ class StableBrowser {
|
|
|
2414
2665
|
}
|
|
2415
2666
|
// Helper function to remove all style="" attributes
|
|
2416
2667
|
const removeStyleAttributes = (htmlString) => {
|
|
2417
|
-
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi,
|
|
2668
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2418
2669
|
};
|
|
2419
2670
|
// Remove style attributes for innerHTML and outerHTML properties
|
|
2420
2671
|
if (property === "innerHTML" || property === "outerHTML") {
|
|
@@ -2423,47 +2674,54 @@ class StableBrowser {
|
|
|
2423
2674
|
}
|
|
2424
2675
|
state.info.value = val;
|
|
2425
2676
|
let regex;
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
const
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
state.info.failCause.assertionFailed = true;
|
|
2441
|
-
state.info.failCause.lastError = errorMessage;
|
|
2442
|
-
throw new Error(errorMessage);
|
|
2443
|
-
}
|
|
2677
|
+
state.info.value = val;
|
|
2678
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2679
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2680
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2681
|
+
let matchPassed = false;
|
|
2682
|
+
if (isRegex) {
|
|
2683
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2684
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2685
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2686
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2687
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2688
|
+
const regex = new RegExp(patternBody, flags);
|
|
2689
|
+
state.info.regex = true;
|
|
2690
|
+
matchPassed = regex.test(val);
|
|
2444
2691
|
}
|
|
2445
2692
|
else {
|
|
2446
|
-
//
|
|
2447
|
-
const
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
// Check if all expected lines are present in the actual lines
|
|
2451
|
-
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2452
|
-
if (!isPart) {
|
|
2453
|
-
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2454
|
-
state.info.failCause.assertionFailed = true;
|
|
2455
|
-
state.info.failCause.lastError = errorMessage;
|
|
2456
|
-
throw new Error(errorMessage);
|
|
2457
|
-
}
|
|
2693
|
+
// Fallback: treat as literal
|
|
2694
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2695
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2696
|
+
matchPassed = regex.test(val);
|
|
2458
2697
|
}
|
|
2459
2698
|
}
|
|
2699
|
+
else if (isContains) {
|
|
2700
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2701
|
+
matchPassed = val.includes(containsValue);
|
|
2702
|
+
}
|
|
2703
|
+
else if (isExact) {
|
|
2704
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2705
|
+
matchPassed = val === exactValue;
|
|
2706
|
+
}
|
|
2707
|
+
else if (property === "innerText") {
|
|
2708
|
+
// Default innerText logic
|
|
2709
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2710
|
+
const valLines = val.split("\n");
|
|
2711
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2712
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2713
|
+
}
|
|
2460
2714
|
else {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2715
|
+
// Fallback exact or loose match
|
|
2716
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2717
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2718
|
+
matchPassed = regex.test(val);
|
|
2719
|
+
}
|
|
2720
|
+
if (!matchPassed) {
|
|
2721
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2722
|
+
state.info.failCause.assertionFailed = true;
|
|
2723
|
+
state.info.failCause.lastError = errorMessage;
|
|
2724
|
+
throw new Error(errorMessage);
|
|
2467
2725
|
}
|
|
2468
2726
|
return state.info;
|
|
2469
2727
|
}
|
|
@@ -2474,6 +2732,133 @@ class StableBrowser {
|
|
|
2474
2732
|
await _commandFinally(state, this);
|
|
2475
2733
|
}
|
|
2476
2734
|
}
|
|
2735
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2736
|
+
// Convert timeout from seconds to milliseconds
|
|
2737
|
+
const timeoutMs = timeout * 1000;
|
|
2738
|
+
const state = {
|
|
2739
|
+
selectors,
|
|
2740
|
+
_params,
|
|
2741
|
+
condition,
|
|
2742
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2743
|
+
options,
|
|
2744
|
+
world,
|
|
2745
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2746
|
+
highlight: true,
|
|
2747
|
+
screenshot: true,
|
|
2748
|
+
text: `Conditional wait for element`,
|
|
2749
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2750
|
+
operation: "conditionalWait",
|
|
2751
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2752
|
+
allowDisabled: true,
|
|
2753
|
+
info: {},
|
|
2754
|
+
};
|
|
2755
|
+
state.options ??= { timeout: timeoutMs };
|
|
2756
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2757
|
+
const startTime = Date.now();
|
|
2758
|
+
let conditionMet = false;
|
|
2759
|
+
let currentValue = null;
|
|
2760
|
+
let lastError = null;
|
|
2761
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2762
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2763
|
+
const elapsedTime = Date.now() - startTime;
|
|
2764
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2765
|
+
try {
|
|
2766
|
+
// Try to execute _preCommand (element location)
|
|
2767
|
+
await _preCommand(state, this);
|
|
2768
|
+
// If _preCommand succeeds, start condition checking
|
|
2769
|
+
const checkCondition = async () => {
|
|
2770
|
+
try {
|
|
2771
|
+
switch (condition.toLowerCase()) {
|
|
2772
|
+
case "checked":
|
|
2773
|
+
currentValue = await state.element.isChecked();
|
|
2774
|
+
return currentValue === true;
|
|
2775
|
+
case "unchecked":
|
|
2776
|
+
currentValue = await state.element.isChecked();
|
|
2777
|
+
return currentValue === false;
|
|
2778
|
+
case "visible":
|
|
2779
|
+
currentValue = await state.element.isVisible();
|
|
2780
|
+
return currentValue === true;
|
|
2781
|
+
case "hidden":
|
|
2782
|
+
currentValue = await state.element.isVisible();
|
|
2783
|
+
return currentValue === false;
|
|
2784
|
+
case "enabled":
|
|
2785
|
+
currentValue = await state.element.isDisabled();
|
|
2786
|
+
return currentValue === false;
|
|
2787
|
+
case "disabled":
|
|
2788
|
+
currentValue = await state.element.isDisabled();
|
|
2789
|
+
return currentValue === true;
|
|
2790
|
+
case "editable":
|
|
2791
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2792
|
+
currentValue = await state.element.isContentEditable();
|
|
2793
|
+
return currentValue === true;
|
|
2794
|
+
default:
|
|
2795
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2796
|
+
state.info.success = false;
|
|
2797
|
+
return false;
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
catch (error) {
|
|
2801
|
+
// Don't throw here, just return false to continue retrying
|
|
2802
|
+
return false;
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
// Inner loop for condition checking (once element is located)
|
|
2806
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2807
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2808
|
+
conditionMet = await checkCondition();
|
|
2809
|
+
if (conditionMet) {
|
|
2810
|
+
break;
|
|
2811
|
+
}
|
|
2812
|
+
// Check if we still have time for another attempt
|
|
2813
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2814
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2815
|
+
}
|
|
2816
|
+
else {
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
// If we got here and condition is met, break out of main loop
|
|
2821
|
+
if (conditionMet) {
|
|
2822
|
+
break;
|
|
2823
|
+
}
|
|
2824
|
+
// If condition not met but no exception, we've timed out
|
|
2825
|
+
break;
|
|
2826
|
+
}
|
|
2827
|
+
catch (e) {
|
|
2828
|
+
lastError = e;
|
|
2829
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2830
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2831
|
+
// Check if we have enough time left to retry
|
|
2832
|
+
if (timeLeft > 100) {
|
|
2833
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2834
|
+
}
|
|
2835
|
+
else {
|
|
2836
|
+
break;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2841
|
+
state.info = {
|
|
2842
|
+
success: conditionMet,
|
|
2843
|
+
conditionMet,
|
|
2844
|
+
actualWaitTime,
|
|
2845
|
+
currentValue,
|
|
2846
|
+
lastError: lastError?.message || null,
|
|
2847
|
+
message: conditionMet
|
|
2848
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2849
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2850
|
+
};
|
|
2851
|
+
if (lastError) {
|
|
2852
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2853
|
+
}
|
|
2854
|
+
try {
|
|
2855
|
+
await _commandFinally(state, this);
|
|
2856
|
+
}
|
|
2857
|
+
catch (finallyError) {
|
|
2858
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2859
|
+
}
|
|
2860
|
+
return state.info;
|
|
2861
|
+
}
|
|
2477
2862
|
async extractEmailData(emailAddress, options, world) {
|
|
2478
2863
|
if (!emailAddress) {
|
|
2479
2864
|
throw new Error("email address is null");
|
|
@@ -3070,6 +3455,8 @@ class StableBrowser {
|
|
|
3070
3455
|
operation: "verify_text_with_relation",
|
|
3071
3456
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3072
3457
|
};
|
|
3458
|
+
const cmdStartTime = Date.now();
|
|
3459
|
+
let cmdEndTime = null;
|
|
3073
3460
|
const timeout = this._getFindElementTimeout(options);
|
|
3074
3461
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3075
3462
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3105,6 +3492,17 @@ class StableBrowser {
|
|
|
3105
3492
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3106
3493
|
continue;
|
|
3107
3494
|
}
|
|
3495
|
+
else {
|
|
3496
|
+
cmdEndTime = Date.now();
|
|
3497
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3498
|
+
if (foundAncore) {
|
|
3499
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3500
|
+
}
|
|
3501
|
+
else {
|
|
3502
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3108
3506
|
try {
|
|
3109
3507
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3110
3508
|
foundAncore = true;
|
|
@@ -3243,7 +3641,7 @@ class StableBrowser {
|
|
|
3243
3641
|
Object.assign(e, { info: info });
|
|
3244
3642
|
error = e;
|
|
3245
3643
|
// throw e;
|
|
3246
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3644
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3247
3645
|
}
|
|
3248
3646
|
finally {
|
|
3249
3647
|
const endTime = Date.now();
|
|
@@ -3592,6 +3990,22 @@ class StableBrowser {
|
|
|
3592
3990
|
}
|
|
3593
3991
|
}
|
|
3594
3992
|
async waitForPageLoad(options = {}, world = null) {
|
|
3993
|
+
// try {
|
|
3994
|
+
// let currentPagePath = null;
|
|
3995
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
3996
|
+
// if (this.latestPagePath) {
|
|
3997
|
+
// // get the currect page path and compare with the latest page path
|
|
3998
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
3999
|
+
// // if the page path is the same, do not wait for page load
|
|
4000
|
+
// console.log("No page change: " + currentPagePath);
|
|
4001
|
+
// return;
|
|
4002
|
+
// }
|
|
4003
|
+
// }
|
|
4004
|
+
// this.latestPagePath = currentPagePath;
|
|
4005
|
+
// } catch (e) {
|
|
4006
|
+
// console.debug("Error getting current page path: ", e);
|
|
4007
|
+
// }
|
|
4008
|
+
//console.log("Waiting for page load");
|
|
3595
4009
|
let timeout = this._getLoadTimeout(options);
|
|
3596
4010
|
const promiseArray = [];
|
|
3597
4011
|
// let waitForNetworkIdle = true;
|
|
@@ -3624,10 +4038,12 @@ class StableBrowser {
|
|
|
3624
4038
|
else if (e.label === "domcontentloaded") {
|
|
3625
4039
|
console.log("waited for the domcontent loaded timeout");
|
|
3626
4040
|
}
|
|
3627
|
-
console.log(".");
|
|
3628
4041
|
}
|
|
3629
4042
|
finally {
|
|
3630
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4043
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4044
|
+
if (options && !options.noSleep) {
|
|
4045
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4046
|
+
}
|
|
3631
4047
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3632
4048
|
const endTime = Date.now();
|
|
3633
4049
|
_reportToWorld(world, {
|
|
@@ -3668,7 +4084,6 @@ class StableBrowser {
|
|
|
3668
4084
|
await this.page.close();
|
|
3669
4085
|
}
|
|
3670
4086
|
catch (e) {
|
|
3671
|
-
console.log(".");
|
|
3672
4087
|
await _commandError(state, e, this);
|
|
3673
4088
|
}
|
|
3674
4089
|
finally {
|
|
@@ -3682,7 +4097,7 @@ class StableBrowser {
|
|
|
3682
4097
|
}
|
|
3683
4098
|
operation = options.operation;
|
|
3684
4099
|
// validate operation is one of the supported operations
|
|
3685
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4100
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3686
4101
|
throw new Error("operation is not supported");
|
|
3687
4102
|
}
|
|
3688
4103
|
const state = {
|
|
@@ -3751,6 +4166,17 @@ class StableBrowser {
|
|
|
3751
4166
|
state.element = results[0];
|
|
3752
4167
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3753
4168
|
break;
|
|
4169
|
+
case "hover":
|
|
4170
|
+
if (!options.css) {
|
|
4171
|
+
throw new Error("css is not defined");
|
|
4172
|
+
}
|
|
4173
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4174
|
+
if (result1.length === 0) {
|
|
4175
|
+
throw new Error(`Element not found in cell area`);
|
|
4176
|
+
}
|
|
4177
|
+
state.element = result1[0];
|
|
4178
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4179
|
+
break;
|
|
3754
4180
|
default:
|
|
3755
4181
|
throw new Error("operation is not supported");
|
|
3756
4182
|
}
|
|
@@ -3783,7 +4209,6 @@ class StableBrowser {
|
|
|
3783
4209
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3784
4210
|
}
|
|
3785
4211
|
catch (e) {
|
|
3786
|
-
console.log(".");
|
|
3787
4212
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3788
4213
|
}
|
|
3789
4214
|
finally {
|
|
@@ -3821,7 +4246,6 @@ class StableBrowser {
|
|
|
3821
4246
|
await this.page.reload();
|
|
3822
4247
|
}
|
|
3823
4248
|
catch (e) {
|
|
3824
|
-
console.log(".");
|
|
3825
4249
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3826
4250
|
}
|
|
3827
4251
|
finally {
|
|
@@ -3865,6 +4289,10 @@ class StableBrowser {
|
|
|
3865
4289
|
}
|
|
3866
4290
|
}
|
|
3867
4291
|
async beforeScenario(world, scenario) {
|
|
4292
|
+
if (world && world.attach) {
|
|
4293
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4294
|
+
}
|
|
4295
|
+
this.context.loadedRoutes = null;
|
|
3868
4296
|
this.beforeScenarioCalled = true;
|
|
3869
4297
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3870
4298
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3894,8 +4322,10 @@ class StableBrowser {
|
|
|
3894
4322
|
}
|
|
3895
4323
|
async afterScenario(world, scenario) { }
|
|
3896
4324
|
async beforeStep(world, step) {
|
|
4325
|
+
this.stepTags = [];
|
|
3897
4326
|
if (!this.beforeScenarioCalled) {
|
|
3898
4327
|
this.beforeScenario(world, step);
|
|
4328
|
+
this.context.loadedRoutes = null;
|
|
3899
4329
|
}
|
|
3900
4330
|
if (this.stepIndex === undefined) {
|
|
3901
4331
|
this.stepIndex = 0;
|
|
@@ -3905,7 +4335,12 @@ class StableBrowser {
|
|
|
3905
4335
|
}
|
|
3906
4336
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3907
4337
|
this.stepName = step.pickleStep.text;
|
|
3908
|
-
|
|
4338
|
+
let printableStepName = this.stepName;
|
|
4339
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4340
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4341
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4342
|
+
});
|
|
4343
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
3909
4344
|
}
|
|
3910
4345
|
else if (step && step.text) {
|
|
3911
4346
|
this.stepName = step.text;
|
|
@@ -3920,13 +4355,23 @@ class StableBrowser {
|
|
|
3920
4355
|
}
|
|
3921
4356
|
if (this.initSnapshotTaken === false) {
|
|
3922
4357
|
this.initSnapshotTaken = true;
|
|
3923
|
-
if (world &&
|
|
4358
|
+
if (world &&
|
|
4359
|
+
world.attach &&
|
|
4360
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4361
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
3924
4362
|
const snapshot = await this.getAriaSnapshot();
|
|
3925
4363
|
if (snapshot) {
|
|
3926
4364
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3927
4365
|
}
|
|
3928
4366
|
}
|
|
3929
4367
|
}
|
|
4368
|
+
this.context.routeResults = null;
|
|
4369
|
+
this.context.loadedRoutes = null;
|
|
4370
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4371
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4372
|
+
}
|
|
4373
|
+
setStepTags(tags) {
|
|
4374
|
+
this.stepTags = tags;
|
|
3930
4375
|
}
|
|
3931
4376
|
async getAriaSnapshot() {
|
|
3932
4377
|
try {
|
|
@@ -3946,12 +4391,18 @@ class StableBrowser {
|
|
|
3946
4391
|
try {
|
|
3947
4392
|
// Ensure frame is attached and has body
|
|
3948
4393
|
const body = frame.locator("body");
|
|
3949
|
-
await body.waitFor({ timeout:
|
|
4394
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
3950
4395
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4396
|
+
if (!snapshot) {
|
|
4397
|
+
continue;
|
|
4398
|
+
}
|
|
3951
4399
|
content.push(`- frame: ${i}`);
|
|
3952
4400
|
content.push(snapshot);
|
|
3953
4401
|
}
|
|
3954
|
-
catch (innerErr) {
|
|
4402
|
+
catch (innerErr) {
|
|
4403
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4404
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4405
|
+
}
|
|
3955
4406
|
}
|
|
3956
4407
|
return content.join("\n");
|
|
3957
4408
|
}
|
|
@@ -4023,13 +4474,23 @@ class StableBrowser {
|
|
|
4023
4474
|
if (this.context) {
|
|
4024
4475
|
this.context.examplesRow = null;
|
|
4025
4476
|
}
|
|
4026
|
-
if (world &&
|
|
4477
|
+
if (world &&
|
|
4478
|
+
world.attach &&
|
|
4479
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4480
|
+
!this.fastMode &&
|
|
4481
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4027
4482
|
const snapshot = await this.getAriaSnapshot();
|
|
4028
4483
|
if (snapshot) {
|
|
4029
4484
|
const obj = {};
|
|
4030
4485
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4031
4486
|
}
|
|
4032
4487
|
}
|
|
4488
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4489
|
+
if (this.context.routeResults) {
|
|
4490
|
+
if (world && world.attach) {
|
|
4491
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4033
4494
|
if (!process.env.TEMP_RUN) {
|
|
4034
4495
|
const state = {
|
|
4035
4496
|
world,
|
|
@@ -4053,6 +4514,13 @@ class StableBrowser {
|
|
|
4053
4514
|
await _commandFinally(state, this);
|
|
4054
4515
|
}
|
|
4055
4516
|
}
|
|
4517
|
+
networkAfterStep(this.stepName, this.context);
|
|
4518
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4519
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4520
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4521
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4056
4524
|
}
|
|
4057
4525
|
}
|
|
4058
4526
|
function createTimedPromise(promise, label) {
|