automation_model 1.0.742-dev → 1.0.742-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 +570 -89
- 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
|
}
|
|
@@ -755,7 +991,6 @@ class StableBrowser {
|
|
|
755
991
|
let locatorsCount = 0;
|
|
756
992
|
let lazy_scroll = false;
|
|
757
993
|
//let arrayMode = Array.isArray(selectors);
|
|
758
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
759
994
|
let selectorsLocators = null;
|
|
760
995
|
selectorsLocators = selectors.locators;
|
|
761
996
|
// group selectors by priority
|
|
@@ -783,6 +1018,7 @@ class StableBrowser {
|
|
|
783
1018
|
let highPriorityOnly = true;
|
|
784
1019
|
let visibleOnly = true;
|
|
785
1020
|
while (true) {
|
|
1021
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
786
1022
|
locatorsCount = 0;
|
|
787
1023
|
let result = [];
|
|
788
1024
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -898,9 +1134,13 @@ class StableBrowser {
|
|
|
898
1134
|
}
|
|
899
1135
|
}
|
|
900
1136
|
if (foundLocators.length === 1) {
|
|
1137
|
+
let box = null;
|
|
1138
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1139
|
+
box = await foundLocators[0].boundingBox();
|
|
1140
|
+
}
|
|
901
1141
|
result.foundElements.push({
|
|
902
1142
|
locator: foundLocators[0],
|
|
903
|
-
box:
|
|
1143
|
+
box: box,
|
|
904
1144
|
unique: true,
|
|
905
1145
|
});
|
|
906
1146
|
result.locatorIndex = i;
|
|
@@ -1055,11 +1295,16 @@ class StableBrowser {
|
|
|
1055
1295
|
operation: "click",
|
|
1056
1296
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1057
1297
|
};
|
|
1298
|
+
check_performance("click_all ***", this.context, true);
|
|
1058
1299
|
try {
|
|
1300
|
+
check_performance("click_preCommand", this.context, true);
|
|
1059
1301
|
await _preCommand(state, this);
|
|
1302
|
+
check_performance("click_preCommand", this.context, false);
|
|
1060
1303
|
await performAction("click", state.element, options, this, state, _params);
|
|
1061
|
-
if (!this.fastMode) {
|
|
1062
|
-
|
|
1304
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1305
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1306
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1307
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
1063
1308
|
}
|
|
1064
1309
|
return state.info;
|
|
1065
1310
|
}
|
|
@@ -1067,7 +1312,13 @@ class StableBrowser {
|
|
|
1067
1312
|
await _commandError(state, e, this);
|
|
1068
1313
|
}
|
|
1069
1314
|
finally {
|
|
1315
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1070
1316
|
await _commandFinally(state, this);
|
|
1317
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1318
|
+
check_performance("click_all ***", this.context, false);
|
|
1319
|
+
if (this.context.profile) {
|
|
1320
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1321
|
+
}
|
|
1071
1322
|
}
|
|
1072
1323
|
}
|
|
1073
1324
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1159,7 +1410,7 @@ class StableBrowser {
|
|
|
1159
1410
|
}
|
|
1160
1411
|
}
|
|
1161
1412
|
}
|
|
1162
|
-
await this.waitForPageLoad();
|
|
1413
|
+
//await this.waitForPageLoad();
|
|
1163
1414
|
return state.info;
|
|
1164
1415
|
}
|
|
1165
1416
|
catch (e) {
|
|
@@ -1185,7 +1436,7 @@ class StableBrowser {
|
|
|
1185
1436
|
await _preCommand(state, this);
|
|
1186
1437
|
await performAction("hover", state.element, options, this, state, _params);
|
|
1187
1438
|
await _screenshot(state, this);
|
|
1188
|
-
await this.waitForPageLoad();
|
|
1439
|
+
//await this.waitForPageLoad();
|
|
1189
1440
|
return state.info;
|
|
1190
1441
|
}
|
|
1191
1442
|
catch (e) {
|
|
@@ -1221,7 +1472,7 @@ class StableBrowser {
|
|
|
1221
1472
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1222
1473
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1223
1474
|
}
|
|
1224
|
-
await this.waitForPageLoad();
|
|
1475
|
+
//await this.waitForPageLoad();
|
|
1225
1476
|
return state.info;
|
|
1226
1477
|
}
|
|
1227
1478
|
catch (e) {
|
|
@@ -1503,8 +1754,8 @@ class StableBrowser {
|
|
|
1503
1754
|
if (enter) {
|
|
1504
1755
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1505
1756
|
await this.page.keyboard.press("Enter");
|
|
1757
|
+
await this.waitForPageLoad();
|
|
1506
1758
|
}
|
|
1507
|
-
await this.waitForPageLoad();
|
|
1508
1759
|
return state.info;
|
|
1509
1760
|
}
|
|
1510
1761
|
catch (e) {
|
|
@@ -1959,12 +2210,7 @@ class StableBrowser {
|
|
|
1959
2210
|
}
|
|
1960
2211
|
}
|
|
1961
2212
|
getTestData(world = null) {
|
|
1962
|
-
|
|
1963
|
-
let data = {};
|
|
1964
|
-
if (fs.existsSync(dataFile)) {
|
|
1965
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1966
|
-
}
|
|
1967
|
-
return data;
|
|
2213
|
+
return _getTestData(world, this.context, this);
|
|
1968
2214
|
}
|
|
1969
2215
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1970
2216
|
// collect url/path/title
|
|
@@ -2397,6 +2643,12 @@ class StableBrowser {
|
|
|
2397
2643
|
const isEditable = await state.element.isEditable();
|
|
2398
2644
|
val = String(!isEditable);
|
|
2399
2645
|
break;
|
|
2646
|
+
case "innerHTML":
|
|
2647
|
+
val = String(await state.element.innerHTML());
|
|
2648
|
+
break;
|
|
2649
|
+
case "outerHTML":
|
|
2650
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2651
|
+
break;
|
|
2400
2652
|
default:
|
|
2401
2653
|
if (property.startsWith("dataset.")) {
|
|
2402
2654
|
const dataAttribute = property.substring(8);
|
|
@@ -2406,46 +2658,65 @@ class StableBrowser {
|
|
|
2406
2658
|
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2407
2659
|
}
|
|
2408
2660
|
}
|
|
2661
|
+
// Helper function to remove all style="" attributes
|
|
2662
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2663
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2664
|
+
};
|
|
2665
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2666
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2667
|
+
val = removeStyleAttributes(val);
|
|
2668
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2669
|
+
}
|
|
2409
2670
|
state.info.value = val;
|
|
2410
2671
|
let regex;
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
state.info.failCause.assertionFailed = true;
|
|
2426
|
-
state.info.failCause.lastError = errorMessage;
|
|
2427
|
-
throw new Error(errorMessage);
|
|
2428
|
-
}
|
|
2672
|
+
state.info.value = val;
|
|
2673
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2674
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2675
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2676
|
+
let matchPassed = false;
|
|
2677
|
+
if (isRegex) {
|
|
2678
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2679
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2680
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2681
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2682
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2683
|
+
const regex = new RegExp(patternBody, flags);
|
|
2684
|
+
state.info.regex = true;
|
|
2685
|
+
matchPassed = regex.test(val);
|
|
2429
2686
|
}
|
|
2430
2687
|
else {
|
|
2431
|
-
|
|
2432
|
-
const
|
|
2433
|
-
const
|
|
2434
|
-
|
|
2435
|
-
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2436
|
-
state.info.failCause.assertionFailed = true;
|
|
2437
|
-
state.info.failCause.lastError = errorMessage;
|
|
2438
|
-
throw new Error(errorMessage);
|
|
2439
|
-
}
|
|
2688
|
+
// Fallback: treat as literal
|
|
2689
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2690
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2691
|
+
matchPassed = regex.test(val);
|
|
2440
2692
|
}
|
|
2441
2693
|
}
|
|
2694
|
+
else if (isContains) {
|
|
2695
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2696
|
+
matchPassed = val.includes(containsValue);
|
|
2697
|
+
}
|
|
2698
|
+
else if (isExact) {
|
|
2699
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2700
|
+
matchPassed = val === exactValue;
|
|
2701
|
+
}
|
|
2702
|
+
else if (property === "innerText") {
|
|
2703
|
+
// Default innerText logic
|
|
2704
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2705
|
+
const valLines = val.split("\n");
|
|
2706
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2707
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2708
|
+
}
|
|
2442
2709
|
else {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2710
|
+
// Fallback exact or loose match
|
|
2711
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2712
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2713
|
+
matchPassed = regex.test(val);
|
|
2714
|
+
}
|
|
2715
|
+
if (!matchPassed) {
|
|
2716
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2717
|
+
state.info.failCause.assertionFailed = true;
|
|
2718
|
+
state.info.failCause.lastError = errorMessage;
|
|
2719
|
+
throw new Error(errorMessage);
|
|
2449
2720
|
}
|
|
2450
2721
|
return state.info;
|
|
2451
2722
|
}
|
|
@@ -2456,6 +2727,133 @@ class StableBrowser {
|
|
|
2456
2727
|
await _commandFinally(state, this);
|
|
2457
2728
|
}
|
|
2458
2729
|
}
|
|
2730
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2731
|
+
// Convert timeout from seconds to milliseconds
|
|
2732
|
+
const timeoutMs = timeout * 1000;
|
|
2733
|
+
const state = {
|
|
2734
|
+
selectors,
|
|
2735
|
+
_params,
|
|
2736
|
+
condition,
|
|
2737
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2738
|
+
options,
|
|
2739
|
+
world,
|
|
2740
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2741
|
+
highlight: true,
|
|
2742
|
+
screenshot: true,
|
|
2743
|
+
text: `Conditional wait for element`,
|
|
2744
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2745
|
+
operation: "conditionalWait",
|
|
2746
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2747
|
+
allowDisabled: true,
|
|
2748
|
+
info: {},
|
|
2749
|
+
};
|
|
2750
|
+
state.options ??= { timeout: timeoutMs };
|
|
2751
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2752
|
+
const startTime = Date.now();
|
|
2753
|
+
let conditionMet = false;
|
|
2754
|
+
let currentValue = null;
|
|
2755
|
+
let lastError = null;
|
|
2756
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2757
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2758
|
+
const elapsedTime = Date.now() - startTime;
|
|
2759
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2760
|
+
try {
|
|
2761
|
+
// Try to execute _preCommand (element location)
|
|
2762
|
+
await _preCommand(state, this);
|
|
2763
|
+
// If _preCommand succeeds, start condition checking
|
|
2764
|
+
const checkCondition = async () => {
|
|
2765
|
+
try {
|
|
2766
|
+
switch (condition.toLowerCase()) {
|
|
2767
|
+
case "checked":
|
|
2768
|
+
currentValue = await state.element.isChecked();
|
|
2769
|
+
return currentValue === true;
|
|
2770
|
+
case "unchecked":
|
|
2771
|
+
currentValue = await state.element.isChecked();
|
|
2772
|
+
return currentValue === false;
|
|
2773
|
+
case "visible":
|
|
2774
|
+
currentValue = await state.element.isVisible();
|
|
2775
|
+
return currentValue === true;
|
|
2776
|
+
case "hidden":
|
|
2777
|
+
currentValue = await state.element.isVisible();
|
|
2778
|
+
return currentValue === false;
|
|
2779
|
+
case "enabled":
|
|
2780
|
+
currentValue = await state.element.isDisabled();
|
|
2781
|
+
return currentValue === false;
|
|
2782
|
+
case "disabled":
|
|
2783
|
+
currentValue = await state.element.isDisabled();
|
|
2784
|
+
return currentValue === true;
|
|
2785
|
+
case "editable":
|
|
2786
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2787
|
+
currentValue = await state.element.isContentEditable();
|
|
2788
|
+
return currentValue === true;
|
|
2789
|
+
default:
|
|
2790
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2791
|
+
state.info.success = false;
|
|
2792
|
+
return false;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
catch (error) {
|
|
2796
|
+
// Don't throw here, just return false to continue retrying
|
|
2797
|
+
return false;
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
// Inner loop for condition checking (once element is located)
|
|
2801
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2802
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2803
|
+
conditionMet = await checkCondition();
|
|
2804
|
+
if (conditionMet) {
|
|
2805
|
+
break;
|
|
2806
|
+
}
|
|
2807
|
+
// Check if we still have time for another attempt
|
|
2808
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2809
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2810
|
+
}
|
|
2811
|
+
else {
|
|
2812
|
+
break;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
// If we got here and condition is met, break out of main loop
|
|
2816
|
+
if (conditionMet) {
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
// If condition not met but no exception, we've timed out
|
|
2820
|
+
break;
|
|
2821
|
+
}
|
|
2822
|
+
catch (e) {
|
|
2823
|
+
lastError = e;
|
|
2824
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2825
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2826
|
+
// Check if we have enough time left to retry
|
|
2827
|
+
if (timeLeft > 100) {
|
|
2828
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2829
|
+
}
|
|
2830
|
+
else {
|
|
2831
|
+
break;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2836
|
+
state.info = {
|
|
2837
|
+
success: conditionMet,
|
|
2838
|
+
conditionMet,
|
|
2839
|
+
actualWaitTime,
|
|
2840
|
+
currentValue,
|
|
2841
|
+
lastError: lastError?.message || null,
|
|
2842
|
+
message: conditionMet
|
|
2843
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2844
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2845
|
+
};
|
|
2846
|
+
if (lastError) {
|
|
2847
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2848
|
+
}
|
|
2849
|
+
try {
|
|
2850
|
+
await _commandFinally(state, this);
|
|
2851
|
+
}
|
|
2852
|
+
catch (finallyError) {
|
|
2853
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2854
|
+
}
|
|
2855
|
+
return state.info;
|
|
2856
|
+
}
|
|
2459
2857
|
async extractEmailData(emailAddress, options, world) {
|
|
2460
2858
|
if (!emailAddress) {
|
|
2461
2859
|
throw new Error("email address is null");
|
|
@@ -3052,6 +3450,8 @@ class StableBrowser {
|
|
|
3052
3450
|
operation: "verify_text_with_relation",
|
|
3053
3451
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3054
3452
|
};
|
|
3453
|
+
const cmdStartTime = Date.now();
|
|
3454
|
+
let cmdEndTime = null;
|
|
3055
3455
|
const timeout = this._getFindElementTimeout(options);
|
|
3056
3456
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3057
3457
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -3087,6 +3487,17 @@ class StableBrowser {
|
|
|
3087
3487
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3088
3488
|
continue;
|
|
3089
3489
|
}
|
|
3490
|
+
else {
|
|
3491
|
+
cmdEndTime = Date.now();
|
|
3492
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3493
|
+
if (foundAncore) {
|
|
3494
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3495
|
+
}
|
|
3496
|
+
else {
|
|
3497
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3090
3501
|
try {
|
|
3091
3502
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3092
3503
|
foundAncore = true;
|
|
@@ -3225,7 +3636,7 @@ class StableBrowser {
|
|
|
3225
3636
|
Object.assign(e, { info: info });
|
|
3226
3637
|
error = e;
|
|
3227
3638
|
// throw e;
|
|
3228
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3639
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
3229
3640
|
}
|
|
3230
3641
|
finally {
|
|
3231
3642
|
const endTime = Date.now();
|
|
@@ -3574,6 +3985,22 @@ class StableBrowser {
|
|
|
3574
3985
|
}
|
|
3575
3986
|
}
|
|
3576
3987
|
async waitForPageLoad(options = {}, world = null) {
|
|
3988
|
+
// try {
|
|
3989
|
+
// let currentPagePath = null;
|
|
3990
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
3991
|
+
// if (this.latestPagePath) {
|
|
3992
|
+
// // get the currect page path and compare with the latest page path
|
|
3993
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
3994
|
+
// // if the page path is the same, do not wait for page load
|
|
3995
|
+
// console.log("No page change: " + currentPagePath);
|
|
3996
|
+
// return;
|
|
3997
|
+
// }
|
|
3998
|
+
// }
|
|
3999
|
+
// this.latestPagePath = currentPagePath;
|
|
4000
|
+
// } catch (e) {
|
|
4001
|
+
// console.debug("Error getting current page path: ", e);
|
|
4002
|
+
// }
|
|
4003
|
+
//console.log("Waiting for page load");
|
|
3577
4004
|
let timeout = this._getLoadTimeout(options);
|
|
3578
4005
|
const promiseArray = [];
|
|
3579
4006
|
// let waitForNetworkIdle = true;
|
|
@@ -3606,10 +4033,12 @@ class StableBrowser {
|
|
|
3606
4033
|
else if (e.label === "domcontentloaded") {
|
|
3607
4034
|
console.log("waited for the domcontent loaded timeout");
|
|
3608
4035
|
}
|
|
3609
|
-
console.log(".");
|
|
3610
4036
|
}
|
|
3611
4037
|
finally {
|
|
3612
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4038
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4039
|
+
if (options && !options.noSleep) {
|
|
4040
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4041
|
+
}
|
|
3613
4042
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
3614
4043
|
const endTime = Date.now();
|
|
3615
4044
|
_reportToWorld(world, {
|
|
@@ -3650,7 +4079,6 @@ class StableBrowser {
|
|
|
3650
4079
|
await this.page.close();
|
|
3651
4080
|
}
|
|
3652
4081
|
catch (e) {
|
|
3653
|
-
console.log(".");
|
|
3654
4082
|
await _commandError(state, e, this);
|
|
3655
4083
|
}
|
|
3656
4084
|
finally {
|
|
@@ -3664,7 +4092,7 @@ class StableBrowser {
|
|
|
3664
4092
|
}
|
|
3665
4093
|
operation = options.operation;
|
|
3666
4094
|
// validate operation is one of the supported operations
|
|
3667
|
-
if (operation != "click" && operation != "hover+click") {
|
|
4095
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
3668
4096
|
throw new Error("operation is not supported");
|
|
3669
4097
|
}
|
|
3670
4098
|
const state = {
|
|
@@ -3733,6 +4161,17 @@ class StableBrowser {
|
|
|
3733
4161
|
state.element = results[0];
|
|
3734
4162
|
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3735
4163
|
break;
|
|
4164
|
+
case "hover":
|
|
4165
|
+
if (!options.css) {
|
|
4166
|
+
throw new Error("css is not defined");
|
|
4167
|
+
}
|
|
4168
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4169
|
+
if (result1.length === 0) {
|
|
4170
|
+
throw new Error(`Element not found in cell area`);
|
|
4171
|
+
}
|
|
4172
|
+
state.element = result1[0];
|
|
4173
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4174
|
+
break;
|
|
3736
4175
|
default:
|
|
3737
4176
|
throw new Error("operation is not supported");
|
|
3738
4177
|
}
|
|
@@ -3765,7 +4204,6 @@ class StableBrowser {
|
|
|
3765
4204
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3766
4205
|
}
|
|
3767
4206
|
catch (e) {
|
|
3768
|
-
console.log(".");
|
|
3769
4207
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3770
4208
|
}
|
|
3771
4209
|
finally {
|
|
@@ -3803,7 +4241,6 @@ class StableBrowser {
|
|
|
3803
4241
|
await this.page.reload();
|
|
3804
4242
|
}
|
|
3805
4243
|
catch (e) {
|
|
3806
|
-
console.log(".");
|
|
3807
4244
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3808
4245
|
}
|
|
3809
4246
|
finally {
|
|
@@ -3847,6 +4284,10 @@ class StableBrowser {
|
|
|
3847
4284
|
}
|
|
3848
4285
|
}
|
|
3849
4286
|
async beforeScenario(world, scenario) {
|
|
4287
|
+
if (world && world.attach) {
|
|
4288
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4289
|
+
}
|
|
4290
|
+
this.context.loadedRoutes = null;
|
|
3850
4291
|
this.beforeScenarioCalled = true;
|
|
3851
4292
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3852
4293
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3876,8 +4317,10 @@ class StableBrowser {
|
|
|
3876
4317
|
}
|
|
3877
4318
|
async afterScenario(world, scenario) { }
|
|
3878
4319
|
async beforeStep(world, step) {
|
|
4320
|
+
this.stepTags = [];
|
|
3879
4321
|
if (!this.beforeScenarioCalled) {
|
|
3880
4322
|
this.beforeScenario(world, step);
|
|
4323
|
+
this.context.loadedRoutes = null;
|
|
3881
4324
|
}
|
|
3882
4325
|
if (this.stepIndex === undefined) {
|
|
3883
4326
|
this.stepIndex = 0;
|
|
@@ -3887,7 +4330,12 @@ class StableBrowser {
|
|
|
3887
4330
|
}
|
|
3888
4331
|
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3889
4332
|
this.stepName = step.pickleStep.text;
|
|
3890
|
-
|
|
4333
|
+
let printableStepName = this.stepName;
|
|
4334
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4335
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4336
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4337
|
+
});
|
|
4338
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
3891
4339
|
}
|
|
3892
4340
|
else if (step && step.text) {
|
|
3893
4341
|
this.stepName = step.text;
|
|
@@ -3902,13 +4350,23 @@ class StableBrowser {
|
|
|
3902
4350
|
}
|
|
3903
4351
|
if (this.initSnapshotTaken === false) {
|
|
3904
4352
|
this.initSnapshotTaken = true;
|
|
3905
|
-
if (world &&
|
|
4353
|
+
if (world &&
|
|
4354
|
+
world.attach &&
|
|
4355
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4356
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
3906
4357
|
const snapshot = await this.getAriaSnapshot();
|
|
3907
4358
|
if (snapshot) {
|
|
3908
4359
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3909
4360
|
}
|
|
3910
4361
|
}
|
|
3911
4362
|
}
|
|
4363
|
+
this.context.routeResults = null;
|
|
4364
|
+
this.context.loadedRoutes = null;
|
|
4365
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4366
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4367
|
+
}
|
|
4368
|
+
setStepTags(tags) {
|
|
4369
|
+
this.stepTags = tags;
|
|
3912
4370
|
}
|
|
3913
4371
|
async getAriaSnapshot() {
|
|
3914
4372
|
try {
|
|
@@ -3928,12 +4386,18 @@ class StableBrowser {
|
|
|
3928
4386
|
try {
|
|
3929
4387
|
// Ensure frame is attached and has body
|
|
3930
4388
|
const body = frame.locator("body");
|
|
3931
|
-
await body.waitFor({ timeout:
|
|
4389
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
3932
4390
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4391
|
+
if (!snapshot) {
|
|
4392
|
+
continue;
|
|
4393
|
+
}
|
|
3933
4394
|
content.push(`- frame: ${i}`);
|
|
3934
4395
|
content.push(snapshot);
|
|
3935
4396
|
}
|
|
3936
|
-
catch (innerErr) {
|
|
4397
|
+
catch (innerErr) {
|
|
4398
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4399
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4400
|
+
}
|
|
3937
4401
|
}
|
|
3938
4402
|
return content.join("\n");
|
|
3939
4403
|
}
|
|
@@ -4005,13 +4469,23 @@ class StableBrowser {
|
|
|
4005
4469
|
if (this.context) {
|
|
4006
4470
|
this.context.examplesRow = null;
|
|
4007
4471
|
}
|
|
4008
|
-
if (world &&
|
|
4472
|
+
if (world &&
|
|
4473
|
+
world.attach &&
|
|
4474
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4475
|
+
!this.fastMode &&
|
|
4476
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4009
4477
|
const snapshot = await this.getAriaSnapshot();
|
|
4010
4478
|
if (snapshot) {
|
|
4011
4479
|
const obj = {};
|
|
4012
4480
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4013
4481
|
}
|
|
4014
4482
|
}
|
|
4483
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4484
|
+
if (this.context.routeResults) {
|
|
4485
|
+
if (world && world.attach) {
|
|
4486
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4015
4489
|
if (!process.env.TEMP_RUN) {
|
|
4016
4490
|
const state = {
|
|
4017
4491
|
world,
|
|
@@ -4035,6 +4509,13 @@ class StableBrowser {
|
|
|
4035
4509
|
await _commandFinally(state, this);
|
|
4036
4510
|
}
|
|
4037
4511
|
}
|
|
4512
|
+
networkAfterStep(this.stepName, this.context);
|
|
4513
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4514
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4515
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4516
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4038
4519
|
}
|
|
4039
4520
|
}
|
|
4040
4521
|
function createTimedPromise(promise, label) {
|