automation_model 1.0.482-dev → 1.0.482
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 +133 -0
- package/lib/analyze_helper.js.map +1 -1
- package/lib/api.d.ts +2 -2
- package/lib/api.js +141 -120
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +7 -2
- package/lib/auto_page.js +283 -29
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +6 -3
- package/lib/browser_manager.js +188 -46
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.d.ts +2 -0
- package/lib/bruno.js +381 -0
- package/lib/bruno.js.map +1 -0
- 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.d.ts +5 -4
- package/lib/command_common.js +131 -20
- package/lib/command_common.js.map +1 -1
- package/lib/date_time.js.map +1 -1
- package/lib/drawRect.js.map +1 -1
- package/lib/environment.d.ts +1 -0
- package/lib/environment.js +1 -0
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.d.ts +6 -0
- package/lib/error-messages.js +206 -0
- package/lib/error-messages.js.map +1 -0
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +172 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/find_function.js.map +1 -1
- package/lib/generation_scripts.d.ts +4 -0
- package/lib/generation_scripts.js +2 -0
- package/lib/generation_scripts.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +4 -3
- package/lib/init_browser.js +160 -83
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js +16 -14
- package/lib/locate_element.js.map +1 -1
- package/lib/locator.d.ts +37 -0
- package/lib/locator.js +172 -0
- package/lib/locator.js.map +1 -1
- package/lib/locator_log.d.ts +26 -0
- package/lib/locator_log.js +69 -0
- package/lib/locator_log.js.map +1 -0
- package/lib/network.d.ts +5 -0
- package/lib/network.js +494 -0
- package/lib/network.js.map +1 -0
- package/lib/route.d.ts +83 -0
- package/lib/route.js +691 -0
- package/lib/route.js.map +1 -0
- package/lib/scripts/axe.mini.js +24005 -0
- package/lib/snapshot_validation.d.ts +37 -0
- package/lib/snapshot_validation.js +357 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +146 -47
- package/lib/stable_browser.js +2552 -836
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +15 -0
- package/lib/table.js +257 -0
- package/lib/table.js.map +1 -0
- package/lib/table_analyze.js.map +1 -1
- package/lib/table_helper.d.ts +19 -0
- package/lib/table_helper.js +130 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +6 -0
- package/lib/test_context.js +5 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +39 -2
- package/lib/utils.js +792 -13
- package/lib/utils.js.map +1 -1
- package/package.json +31 -13
- package/lib/axe/axe.mini.js +0 -12
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,59 +11,100 @@ 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 {
|
|
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";
|
|
15
16
|
import csv from "csv-parser";
|
|
16
17
|
import { Readable } from "node:stream";
|
|
17
18
|
import readline from "readline";
|
|
18
|
-
import { getContext } from "./init_browser.js";
|
|
19
|
+
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
20
|
+
import { getTestData } from "./auto_page.js";
|
|
19
21
|
import { locate_element } from "./locate_element.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
+
import { randomUUID } from "crypto";
|
|
23
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
24
|
+
import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
25
|
+
import { LocatorLog } from "./locator_log.js";
|
|
26
|
+
import axios from "axios";
|
|
27
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
28
|
+
import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
29
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
30
|
+
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
32
|
+
export const Types = {
|
|
22
33
|
CLICK: "click_element",
|
|
34
|
+
WAIT_ELEMENT: "wait_element",
|
|
23
35
|
NAVIGATE: "navigate",
|
|
36
|
+
GO_BACK: "go_back",
|
|
37
|
+
GO_FORWARD: "go_forward",
|
|
24
38
|
FILL: "fill_element",
|
|
25
|
-
EXECUTE: "execute_page_method",
|
|
26
|
-
OPEN: "open_environment",
|
|
39
|
+
EXECUTE: "execute_page_method", //
|
|
40
|
+
OPEN: "open_environment", //
|
|
27
41
|
COMPLETE: "step_complete",
|
|
28
42
|
ASK: "information_needed",
|
|
29
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
30
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
43
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
44
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
31
45
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
46
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
47
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
32
48
|
ANALYZE_TABLE: "analyze_table",
|
|
33
|
-
SELECT: "select_combobox",
|
|
49
|
+
SELECT: "select_combobox", //
|
|
50
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
34
51
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
52
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
35
53
|
TYPE_PRESS: "type_press",
|
|
36
54
|
PRESS: "press_key",
|
|
37
55
|
HOVER: "hover_element",
|
|
38
56
|
CHECK: "check_element",
|
|
39
57
|
UNCHECK: "uncheck_element",
|
|
40
58
|
EXTRACT: "extract_attribute",
|
|
59
|
+
EXTRACT_PROPERTY: "extract_property",
|
|
41
60
|
CLOSE_PAGE: "close_page",
|
|
61
|
+
TABLE_OPERATION: "table_operation",
|
|
42
62
|
SET_DATE_TIME: "set_date_time",
|
|
43
63
|
SET_VIEWPORT: "set_viewport",
|
|
44
64
|
VERIFY_VISUAL: "verify_visual",
|
|
45
65
|
LOAD_DATA: "load_data",
|
|
46
66
|
SET_INPUT: "set_input",
|
|
67
|
+
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
68
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
69
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
70
|
+
BRUNO: "bruno",
|
|
71
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
72
|
+
SET_INPUT_FILES: "set_input_files",
|
|
73
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
74
|
+
REPORT_COMMAND: "report_command",
|
|
75
|
+
STEP_COMPLETE: "step_complete",
|
|
76
|
+
SLEEP: "sleep",
|
|
77
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
47
78
|
};
|
|
48
79
|
export const apps = {};
|
|
80
|
+
const formatElementName = (elementName) => {
|
|
81
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
82
|
+
};
|
|
49
83
|
class StableBrowser {
|
|
50
84
|
browser;
|
|
51
85
|
page;
|
|
52
86
|
logger;
|
|
53
87
|
context;
|
|
54
88
|
world;
|
|
89
|
+
fastMode;
|
|
90
|
+
stepTags;
|
|
55
91
|
project_path = null;
|
|
56
92
|
webLogFile = null;
|
|
57
93
|
networkLogger = null;
|
|
58
94
|
configuration = null;
|
|
59
95
|
appName = "main";
|
|
60
|
-
|
|
96
|
+
tags = null;
|
|
97
|
+
isRecording = false;
|
|
98
|
+
initSnapshotTaken = false;
|
|
99
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
100
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
61
101
|
this.browser = browser;
|
|
62
102
|
this.page = page;
|
|
63
103
|
this.logger = logger;
|
|
64
104
|
this.context = context;
|
|
65
105
|
this.world = world;
|
|
106
|
+
this.fastMode = fastMode;
|
|
107
|
+
this.stepTags = stepTags;
|
|
66
108
|
if (!this.logger) {
|
|
67
109
|
this.logger = console;
|
|
68
110
|
}
|
|
@@ -91,20 +133,54 @@ class StableBrowser {
|
|
|
91
133
|
context.pages = [this.page];
|
|
92
134
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
93
135
|
this.world = world;
|
|
136
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
137
|
+
this.fastMode = true;
|
|
138
|
+
}
|
|
139
|
+
if (process.env.FAST_MODE === "true") {
|
|
140
|
+
// console.log("Fast mode enabled from environment variable");
|
|
141
|
+
this.fastMode = true;
|
|
142
|
+
}
|
|
143
|
+
if (process.env.FAST_MODE === "false") {
|
|
144
|
+
this.fastMode = false;
|
|
145
|
+
}
|
|
146
|
+
if (this.context) {
|
|
147
|
+
this.context.fastMode = this.fastMode;
|
|
148
|
+
}
|
|
94
149
|
this.registerEventListeners(this.context);
|
|
150
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
151
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
95
152
|
}
|
|
96
153
|
registerEventListeners(context) {
|
|
97
154
|
this.registerConsoleLogListener(this.page, context);
|
|
98
|
-
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
155
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
99
156
|
if (!context.pageLoading) {
|
|
100
157
|
context.pageLoading = { status: false };
|
|
101
158
|
}
|
|
159
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
160
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
161
|
+
}
|
|
102
162
|
context.playContext.on("page", async function (page) {
|
|
163
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
164
|
+
console.log("close unexpected popups");
|
|
165
|
+
await page.close();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
103
168
|
context.pageLoading.status = true;
|
|
104
169
|
this.page = page;
|
|
170
|
+
try {
|
|
171
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
172
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error("Error on dialog accept registration", error);
|
|
177
|
+
}
|
|
105
178
|
context.page = page;
|
|
106
179
|
context.pages.push(page);
|
|
180
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
181
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
107
182
|
page.on("close", async () => {
|
|
183
|
+
// return if browser context is already closed
|
|
108
184
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
109
185
|
this.context.pages.pop();
|
|
110
186
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -114,7 +190,12 @@ class StableBrowser {
|
|
|
114
190
|
console.log("Switched to page " + title);
|
|
115
191
|
}
|
|
116
192
|
catch (error) {
|
|
117
|
-
|
|
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
|
+
}
|
|
118
199
|
}
|
|
119
200
|
}
|
|
120
201
|
});
|
|
@@ -123,7 +204,12 @@ class StableBrowser {
|
|
|
123
204
|
console.log("Switch page: " + (await page.title()));
|
|
124
205
|
}
|
|
125
206
|
catch (e) {
|
|
126
|
-
|
|
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
|
+
}
|
|
127
213
|
}
|
|
128
214
|
context.pageLoading.status = false;
|
|
129
215
|
}.bind(this));
|
|
@@ -135,7 +221,7 @@ class StableBrowser {
|
|
|
135
221
|
}
|
|
136
222
|
let newContextCreated = false;
|
|
137
223
|
if (!apps[appName]) {
|
|
138
|
-
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this);
|
|
224
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
139
225
|
newContextCreated = true;
|
|
140
226
|
apps[appName] = {
|
|
141
227
|
context: newContext,
|
|
@@ -144,31 +230,41 @@ class StableBrowser {
|
|
|
144
230
|
};
|
|
145
231
|
}
|
|
146
232
|
const tempContext = {};
|
|
147
|
-
|
|
148
|
-
|
|
233
|
+
_copyContext(this, tempContext);
|
|
234
|
+
_copyContext(apps[appName], this);
|
|
149
235
|
apps[this.appName] = tempContext;
|
|
150
236
|
this.appName = appName;
|
|
151
237
|
if (newContextCreated) {
|
|
152
238
|
this.registerEventListeners(this.context);
|
|
153
239
|
await this.goto(this.context.environment.baseUrl);
|
|
154
|
-
|
|
240
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
241
|
+
await this.waitForPageLoad();
|
|
242
|
+
}
|
|
155
243
|
}
|
|
156
244
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
245
|
+
async switchTab(tabTitleOrIndex) {
|
|
246
|
+
// first check if the tabNameOrIndex is a number
|
|
247
|
+
let index = parseInt(tabTitleOrIndex);
|
|
248
|
+
if (!isNaN(index)) {
|
|
249
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
250
|
+
this.page = this.context.pages[index];
|
|
251
|
+
this.context.page = this.page;
|
|
252
|
+
await this.page.bringToFront();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
165
255
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
256
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
257
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
258
|
+
let page = this.context.pages[i];
|
|
259
|
+
let title = await page.title();
|
|
260
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
261
|
+
this.page = page;
|
|
262
|
+
this.context.page = this.page;
|
|
263
|
+
await this.page.bringToFront();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
169
266
|
}
|
|
170
|
-
|
|
171
|
-
return path.join(logFolder, fileName);
|
|
267
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
172
268
|
}
|
|
173
269
|
registerConsoleLogListener(page, context) {
|
|
174
270
|
if (!this.context.webLogger) {
|
|
@@ -219,7 +315,7 @@ class StableBrowser {
|
|
|
219
315
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
220
316
|
}
|
|
221
317
|
catch (error) {
|
|
222
|
-
console.error("Error in request listener", error);
|
|
318
|
+
// console.error("Error in request listener", error);
|
|
223
319
|
context.networkLogger.push({
|
|
224
320
|
error: "not able to listen",
|
|
225
321
|
message: error.message,
|
|
@@ -233,55 +329,110 @@ class StableBrowser {
|
|
|
233
329
|
// async closeUnexpectedPopups() {
|
|
234
330
|
// await closeUnexpectedPopups(this.page);
|
|
235
331
|
// }
|
|
236
|
-
async goto(url) {
|
|
332
|
+
async goto(url, world = null) {
|
|
333
|
+
if (!url) {
|
|
334
|
+
throw new Error("url is null, verify that the environment file is correct");
|
|
335
|
+
}
|
|
336
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
237
337
|
if (!url.startsWith("http")) {
|
|
238
338
|
url = "https://" + url;
|
|
239
339
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
340
|
+
const state = {
|
|
341
|
+
value: url,
|
|
342
|
+
world: world,
|
|
343
|
+
type: Types.NAVIGATE,
|
|
344
|
+
text: `Navigate Page to: ${url}`,
|
|
345
|
+
operation: "goto",
|
|
346
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
347
|
+
info: {},
|
|
348
|
+
locate: false,
|
|
349
|
+
scroll: false,
|
|
350
|
+
screenshot: false,
|
|
351
|
+
highlight: false,
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
await _preCommand(state, this);
|
|
355
|
+
await this.page.goto(url, {
|
|
356
|
+
timeout: 60000,
|
|
357
|
+
});
|
|
358
|
+
await _screenshot(state, this);
|
|
247
359
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
360
|
+
catch (error) {
|
|
361
|
+
console.error("Error on goto", error);
|
|
362
|
+
_commandError(state, error, this);
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
await _commandFinally(state, this);
|
|
255
366
|
}
|
|
256
|
-
return text;
|
|
257
367
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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);
|
|
262
395
|
}
|
|
263
|
-
// clone the locator
|
|
264
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
265
|
-
this.scanAndManipulate(locator, _params);
|
|
266
|
-
return locator;
|
|
267
396
|
}
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
425
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
426
|
+
async _getLocator(locator, scope, _params) {
|
|
427
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
428
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
429
|
+
for (let key in locator) {
|
|
430
|
+
if (typeof locator[key] !== "string")
|
|
431
|
+
continue;
|
|
432
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
433
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
280
434
|
}
|
|
281
435
|
}
|
|
282
|
-
}
|
|
283
|
-
_getLocator(locator, scope, _params) {
|
|
284
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
285
436
|
let locatorReturn;
|
|
286
437
|
if (locator.role) {
|
|
287
438
|
if (locator.role[1].nameReg) {
|
|
@@ -289,7 +440,7 @@ class StableBrowser {
|
|
|
289
440
|
delete locator.role[1].nameReg;
|
|
290
441
|
}
|
|
291
442
|
// if (locator.role[1].name) {
|
|
292
|
-
// locator.role[1].name =
|
|
443
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
293
444
|
// }
|
|
294
445
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
295
446
|
}
|
|
@@ -332,213 +483,171 @@ class StableBrowser {
|
|
|
332
483
|
if (css && css.locator) {
|
|
333
484
|
css = css.locator;
|
|
334
485
|
}
|
|
335
|
-
let result = await this._locateElementByText(scope,
|
|
486
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
336
487
|
if (result.elementCount === 0) {
|
|
337
488
|
return;
|
|
338
489
|
}
|
|
339
|
-
let textElementCss = "[data-blinq-id
|
|
490
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
340
491
|
// css climb to parent element
|
|
341
492
|
const climbArray = [];
|
|
342
493
|
for (let i = 0; i < climb; i++) {
|
|
343
494
|
climbArray.push("..");
|
|
344
495
|
}
|
|
345
496
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return new RegExp(pattern, flags);
|
|
369
|
-
}
|
|
370
|
-
document.getRegex = getRegex;
|
|
371
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
372
|
-
// Check and add the element if it has a shadow root
|
|
373
|
-
if (element.shadowRoot) {
|
|
374
|
-
result.push(element);
|
|
375
|
-
// Also search within the shadow root
|
|
376
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
377
|
-
}
|
|
378
|
-
// Iterate over child nodes
|
|
379
|
-
element.childNodes.forEach((child) => {
|
|
380
|
-
// Recursively call the function for each child node
|
|
381
|
-
document.collectAllShadowDomElements(child, result);
|
|
382
|
-
});
|
|
383
|
-
return result;
|
|
384
|
-
}
|
|
385
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
386
|
-
if (!tag) {
|
|
387
|
-
tag = "*";
|
|
388
|
-
}
|
|
389
|
-
let regexpSearch = document.getRegex(text);
|
|
390
|
-
if (regexpSearch) {
|
|
391
|
-
regex = true;
|
|
392
|
-
}
|
|
393
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
394
|
-
let shadowHosts = [];
|
|
395
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
396
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
397
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
398
|
-
if (!shadowElement) {
|
|
399
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
403
|
-
elements = elements.concat(shadowElements);
|
|
404
|
-
}
|
|
405
|
-
let randomToken = null;
|
|
406
|
-
const foundElements = [];
|
|
407
|
-
if (regex) {
|
|
408
|
-
if (!regexpSearch) {
|
|
409
|
-
regexpSearch = new RegExp(text, "im");
|
|
410
|
-
}
|
|
411
|
-
for (let i = 0; i < elements.length; i++) {
|
|
412
|
-
const element = elements[i];
|
|
413
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
414
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
415
|
-
foundElements.push(element);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
text = text.trim();
|
|
421
|
-
for (let i = 0; i < elements.length; i++) {
|
|
422
|
-
const element = elements[i];
|
|
423
|
-
if (partial) {
|
|
424
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
425
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
426
|
-
foundElements.push(element);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
431
|
-
(element.value && element.value === text)) {
|
|
432
|
-
foundElements.push(element);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
let noChildElements = [];
|
|
438
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
439
|
-
let element = foundElements[i];
|
|
440
|
-
let hasChild = false;
|
|
441
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
442
|
-
if (i === j) {
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
if (isParent(element, foundElements[j])) {
|
|
446
|
-
hasChild = true;
|
|
447
|
-
break;
|
|
497
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
498
|
+
if (css) {
|
|
499
|
+
resultCss = resultCss + " >> " + css;
|
|
500
|
+
}
|
|
501
|
+
return resultCss;
|
|
502
|
+
}
|
|
503
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
504
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
505
|
+
const locator = scope.locator(query);
|
|
506
|
+
const count = await locator.count();
|
|
507
|
+
if (!tag1) {
|
|
508
|
+
tag1 = "*";
|
|
509
|
+
}
|
|
510
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
511
|
+
let tagCount = 0;
|
|
512
|
+
for (let i = 0; i < count; i++) {
|
|
513
|
+
const element = locator.nth(i);
|
|
514
|
+
// check if the tag matches
|
|
515
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
516
|
+
if (!tag.startsWith("*")) {
|
|
517
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
518
|
+
return false;
|
|
448
519
|
}
|
|
449
520
|
}
|
|
450
|
-
if (!
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
let elementCount = 0;
|
|
455
|
-
if (noChildElements.length > 0) {
|
|
456
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
457
|
-
if (randomToken === null) {
|
|
458
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
459
|
-
}
|
|
460
|
-
let element = noChildElements[i];
|
|
461
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
462
|
-
elementCount++;
|
|
521
|
+
if (!el.setAttribute) {
|
|
522
|
+
el = el.parentElement;
|
|
463
523
|
}
|
|
524
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
525
|
+
return true;
|
|
526
|
+
}, [tag1, randomToken]))) {
|
|
527
|
+
continue;
|
|
464
528
|
}
|
|
465
|
-
|
|
466
|
-
}
|
|
529
|
+
tagCount++;
|
|
530
|
+
}
|
|
531
|
+
return { elementCount: tagCount, randomToken };
|
|
467
532
|
}
|
|
468
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
533
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
534
|
+
if (!info) {
|
|
535
|
+
info = {};
|
|
536
|
+
}
|
|
537
|
+
if (!info.failCause) {
|
|
538
|
+
info.failCause = {};
|
|
539
|
+
}
|
|
540
|
+
if (!info.log) {
|
|
541
|
+
info.log = "";
|
|
542
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
543
|
+
}
|
|
469
544
|
let locatorSearch = selectorHierarchy[index];
|
|
470
545
|
try {
|
|
471
|
-
locatorSearch =
|
|
546
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
472
547
|
}
|
|
473
548
|
catch (e) {
|
|
474
549
|
console.error(e);
|
|
475
550
|
}
|
|
551
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
476
552
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
477
553
|
let locator = null;
|
|
478
554
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
479
|
-
|
|
555
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
556
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
480
557
|
if (!locatorString) {
|
|
558
|
+
info.failCause.textNotFound = true;
|
|
559
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
481
560
|
return;
|
|
482
561
|
}
|
|
483
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
562
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
484
563
|
}
|
|
485
564
|
else if (locatorSearch.text) {
|
|
486
|
-
let
|
|
565
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
566
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
487
567
|
if (result.elementCount === 0) {
|
|
568
|
+
info.failCause.textNotFound = true;
|
|
569
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
488
570
|
return;
|
|
489
571
|
}
|
|
490
|
-
locatorSearch.css = "[data-blinq-id
|
|
572
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
491
573
|
if (locatorSearch.childCss) {
|
|
492
574
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
493
575
|
}
|
|
494
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
576
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
495
577
|
}
|
|
496
578
|
else {
|
|
497
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
579
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
498
580
|
}
|
|
499
581
|
// let cssHref = false;
|
|
500
582
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
501
583
|
// cssHref = true;
|
|
502
584
|
// }
|
|
503
585
|
let count = await locator.count();
|
|
586
|
+
if (count > 0 && !info.failCause.count) {
|
|
587
|
+
info.failCause.count = count;
|
|
588
|
+
}
|
|
504
589
|
//info.log += "total elements found " + count + "\n";
|
|
505
590
|
//let visibleCount = 0;
|
|
506
591
|
let visibleLocator = null;
|
|
507
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
592
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
508
593
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
594
|
+
if (info.locatorLog) {
|
|
595
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
596
|
+
}
|
|
509
597
|
return;
|
|
510
598
|
}
|
|
599
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
600
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
601
|
+
}
|
|
511
602
|
for (let j = 0; j < count; j++) {
|
|
512
603
|
let visible = await locator.nth(j).isVisible();
|
|
513
604
|
const enabled = await locator.nth(j).isEnabled();
|
|
514
605
|
if (!visibleOnly) {
|
|
515
606
|
visible = true;
|
|
516
607
|
}
|
|
517
|
-
if (visible && enabled) {
|
|
608
|
+
if (visible && (allowDisabled || enabled)) {
|
|
518
609
|
foundLocators.push(locator.nth(j));
|
|
610
|
+
if (info.locatorLog) {
|
|
611
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
612
|
+
}
|
|
519
613
|
}
|
|
520
|
-
else {
|
|
614
|
+
else if (logErrors) {
|
|
615
|
+
info.failCause.visible = visible;
|
|
616
|
+
info.failCause.enabled = enabled;
|
|
521
617
|
if (!info.printMessages) {
|
|
522
618
|
info.printMessages = {};
|
|
523
619
|
}
|
|
620
|
+
if (info.locatorLog && !visible) {
|
|
621
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
622
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
623
|
+
}
|
|
624
|
+
if (info.locatorLog && !enabled) {
|
|
625
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
626
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
627
|
+
}
|
|
524
628
|
if (!info.printMessages[j.toString()]) {
|
|
525
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
629
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
526
630
|
info.printMessages[j.toString()] = true;
|
|
527
631
|
}
|
|
528
632
|
}
|
|
529
633
|
}
|
|
530
634
|
}
|
|
531
635
|
async closeUnexpectedPopups(info, _params) {
|
|
636
|
+
if (!info) {
|
|
637
|
+
info = {};
|
|
638
|
+
info.failCause = {};
|
|
639
|
+
info.log = "";
|
|
640
|
+
}
|
|
532
641
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
533
642
|
if (!info) {
|
|
534
643
|
info = {};
|
|
535
644
|
}
|
|
536
|
-
info.log += "scan for popup handlers" + "\n";
|
|
645
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
537
646
|
const handlerGroup = [];
|
|
538
647
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
539
648
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
540
649
|
}
|
|
541
|
-
const scopes =
|
|
650
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
542
651
|
let result = null;
|
|
543
652
|
let scope = null;
|
|
544
653
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -560,33 +669,218 @@ class StableBrowser {
|
|
|
560
669
|
}
|
|
561
670
|
if (result.foundElements.length > 0) {
|
|
562
671
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
672
|
+
try {
|
|
673
|
+
await scope?.evaluate(() => {
|
|
674
|
+
window.__isClosingPopups = true;
|
|
675
|
+
});
|
|
676
|
+
await dialogCloseLocator.click();
|
|
677
|
+
// wait for the dialog to close
|
|
678
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
}
|
|
682
|
+
finally {
|
|
683
|
+
await scope?.evaluate(() => {
|
|
684
|
+
window.__isClosingPopups = false;
|
|
685
|
+
});
|
|
686
|
+
}
|
|
566
687
|
return { rerun: true };
|
|
567
688
|
}
|
|
568
689
|
}
|
|
569
690
|
}
|
|
570
691
|
return { rerun: false };
|
|
571
692
|
}
|
|
572
|
-
|
|
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
|
+
}
|
|
757
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
573
758
|
if (!timeout) {
|
|
574
759
|
timeout = 30000;
|
|
575
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
|
+
}
|
|
576
768
|
for (let i = 0; i < 3; i++) {
|
|
577
769
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
578
770
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
579
771
|
let selector = selectors.locators[j];
|
|
580
772
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
581
773
|
}
|
|
582
|
-
|
|
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
|
+
}
|
|
583
817
|
if (!element.rerun) {
|
|
584
|
-
|
|
818
|
+
let newElementSelector = "";
|
|
819
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
|
|
820
|
+
const cssSelector = await element.evaluate((el) => {
|
|
821
|
+
function getCssSelector(el) {
|
|
822
|
+
if (!el || el.nodeType !== 1 || el === document.body)
|
|
823
|
+
return el.tagName.toLowerCase();
|
|
824
|
+
const parent = el.parentElement;
|
|
825
|
+
const tag = el.tagName.toLowerCase();
|
|
826
|
+
// Find the index of the element among its siblings of the same tag
|
|
827
|
+
let index = 1;
|
|
828
|
+
for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
829
|
+
if (sibling.tagName === el.tagName) {
|
|
830
|
+
index++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// Use nth-child if necessary (i.e., if there's more than one of the same tag)
|
|
834
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
835
|
+
const needsNthChild = siblings.length > 1;
|
|
836
|
+
const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
|
|
837
|
+
return getCssSelector(parent) + " > " + selector;
|
|
838
|
+
}
|
|
839
|
+
const cssSelector = getCssSelector(el);
|
|
840
|
+
return cssSelector;
|
|
841
|
+
});
|
|
842
|
+
newElementSelector = cssSelector;
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
846
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
|
|
847
|
+
const dataAttribute = "data-blinq-id";
|
|
848
|
+
await element.evaluate((el, [dataAttribute, randomToken]) => {
|
|
849
|
+
el.setAttribute(dataAttribute, randomToken);
|
|
850
|
+
}, [dataAttribute, randomToken]);
|
|
851
|
+
newElementSelector = `[${dataAttribute}="${randomToken}"]`;
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
// the default case just return the located element
|
|
855
|
+
// will not work for click and type if the locator is placeholder and the placeholder change due to the click event
|
|
856
|
+
return element;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const scope = element._frame ?? element.page();
|
|
860
|
+
let prefixSelector = "";
|
|
861
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
862
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
863
|
+
if (frameSelectorIndex !== -1) {
|
|
864
|
+
// remove everything after the >> internal:control=enter-frame
|
|
865
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
866
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
867
|
+
}
|
|
868
|
+
// if (element?._frame?._selector) {
|
|
869
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
870
|
+
// }
|
|
871
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
872
|
+
return scope.locator(newSelector).first();
|
|
585
873
|
}
|
|
586
874
|
}
|
|
587
875
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
588
876
|
}
|
|
589
|
-
async _findFrameScope(selectors, timeout = 30000) {
|
|
877
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
878
|
+
if (!info) {
|
|
879
|
+
info = {};
|
|
880
|
+
info.failCause = {};
|
|
881
|
+
info.log = "";
|
|
882
|
+
}
|
|
883
|
+
let startTime = Date.now();
|
|
590
884
|
let scope = this.page;
|
|
591
885
|
if (selectors.frame) {
|
|
592
886
|
return selectors.frame;
|
|
@@ -596,7 +890,7 @@ class StableBrowser {
|
|
|
596
890
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
597
891
|
let frameLocator = frame.selectors[i];
|
|
598
892
|
if (frameLocator.css) {
|
|
599
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
893
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
600
894
|
if (frameLocator.index) {
|
|
601
895
|
testframescope = framescope.nth(frameLocator.index);
|
|
602
896
|
}
|
|
@@ -608,7 +902,7 @@ class StableBrowser {
|
|
|
608
902
|
break;
|
|
609
903
|
}
|
|
610
904
|
catch (error) {
|
|
611
|
-
console.error("frame not found " + frameLocator.css);
|
|
905
|
+
// console.error("frame not found " + frameLocator.css);
|
|
612
906
|
}
|
|
613
907
|
}
|
|
614
908
|
}
|
|
@@ -617,9 +911,11 @@ class StableBrowser {
|
|
|
617
911
|
}
|
|
618
912
|
return framescope;
|
|
619
913
|
};
|
|
914
|
+
let fLocator = null;
|
|
620
915
|
while (true) {
|
|
621
916
|
let frameFound = false;
|
|
622
917
|
if (selectors.nestFrmLoc) {
|
|
918
|
+
fLocator = selectors.nestFrmLoc;
|
|
623
919
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
624
920
|
frameFound = true;
|
|
625
921
|
break;
|
|
@@ -628,6 +924,7 @@ class StableBrowser {
|
|
|
628
924
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
629
925
|
let frameLocator = selectors.frameLocators[i];
|
|
630
926
|
if (frameLocator.css) {
|
|
927
|
+
fLocator = frameLocator.css;
|
|
631
928
|
scope = scope.frameLocator(frameLocator.css);
|
|
632
929
|
frameFound = true;
|
|
633
930
|
break;
|
|
@@ -635,16 +932,25 @@ class StableBrowser {
|
|
|
635
932
|
}
|
|
636
933
|
}
|
|
637
934
|
if (!frameFound && selectors.iframe_src) {
|
|
935
|
+
fLocator = selectors.iframe_src;
|
|
638
936
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
639
937
|
}
|
|
640
938
|
if (!scope) {
|
|
641
|
-
info
|
|
642
|
-
|
|
939
|
+
if (info && info.locatorLog) {
|
|
940
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
941
|
+
}
|
|
942
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
943
|
+
if (Date.now() - startTime > timeout) {
|
|
944
|
+
info.failCause.iframeNotFound = true;
|
|
945
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
643
946
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
644
947
|
}
|
|
645
948
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
646
949
|
}
|
|
647
950
|
else {
|
|
951
|
+
if (info && info.locatorLog) {
|
|
952
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
953
|
+
}
|
|
648
954
|
break;
|
|
649
955
|
}
|
|
650
956
|
}
|
|
@@ -654,20 +960,35 @@ class StableBrowser {
|
|
|
654
960
|
}
|
|
655
961
|
return scope;
|
|
656
962
|
}
|
|
657
|
-
async _getDocumentBody(selectors, timeout = 30000) {
|
|
658
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
963
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
964
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
659
965
|
return scope.evaluate(() => {
|
|
660
966
|
var bodyContent = document.body.innerHTML;
|
|
661
967
|
return bodyContent;
|
|
662
968
|
});
|
|
663
969
|
}
|
|
664
|
-
async _locate_internal(selectors, info, _params, timeout = 30000) {
|
|
970
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
971
|
+
if (selectors.locators && Array.isArray(selectors.locators)) {
|
|
972
|
+
selectors.locators.forEach((locator) => {
|
|
973
|
+
locator.index = locator.index ?? 0;
|
|
974
|
+
locator.visible = locator.visible ?? true;
|
|
975
|
+
if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
976
|
+
locator.css = locator.css + " >> visible=true";
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
if (!info) {
|
|
981
|
+
info = {};
|
|
982
|
+
info.failCause = {};
|
|
983
|
+
info.log = "";
|
|
984
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
985
|
+
}
|
|
665
986
|
let highPriorityTimeout = 5000;
|
|
666
987
|
let visibleOnlyTimeout = 6000;
|
|
667
|
-
let startTime =
|
|
988
|
+
let startTime = Date.now();
|
|
668
989
|
let locatorsCount = 0;
|
|
990
|
+
let lazy_scroll = false;
|
|
669
991
|
//let arrayMode = Array.isArray(selectors);
|
|
670
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
671
992
|
let selectorsLocators = null;
|
|
672
993
|
selectorsLocators = selectors.locators;
|
|
673
994
|
// group selectors by priority
|
|
@@ -695,6 +1016,7 @@ class StableBrowser {
|
|
|
695
1016
|
let highPriorityOnly = true;
|
|
696
1017
|
let visibleOnly = true;
|
|
697
1018
|
while (true) {
|
|
1019
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
698
1020
|
locatorsCount = 0;
|
|
699
1021
|
let result = [];
|
|
700
1022
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -703,18 +1025,13 @@ class StableBrowser {
|
|
|
703
1025
|
}
|
|
704
1026
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
705
1027
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
706
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
1028
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
707
1029
|
if (result.foundElements.length === 0) {
|
|
708
1030
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
709
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
1031
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
710
1032
|
}
|
|
711
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
712
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
716
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
717
|
-
}
|
|
1033
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1034
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
718
1035
|
}
|
|
719
1036
|
let foundElements = result.foundElements;
|
|
720
1037
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -754,24 +1071,43 @@ class StableBrowser {
|
|
|
754
1071
|
return maxCountElement.locator;
|
|
755
1072
|
}
|
|
756
1073
|
}
|
|
757
|
-
if (
|
|
1074
|
+
if (Date.now() - startTime > timeout) {
|
|
758
1075
|
break;
|
|
759
1076
|
}
|
|
760
|
-
if (
|
|
761
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
1077
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
1078
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
762
1079
|
highPriorityOnly = false;
|
|
1080
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
1081
|
+
lazy_scroll = true;
|
|
1082
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
1083
|
+
}
|
|
763
1084
|
}
|
|
764
|
-
if (
|
|
765
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
1085
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
1086
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
766
1087
|
visibleOnly = false;
|
|
767
1088
|
}
|
|
768
1089
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1090
|
+
// sheck of more of half of the timeout has passed
|
|
1091
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
1092
|
+
highPriorityOnly = false;
|
|
1093
|
+
visibleOnly = false;
|
|
1094
|
+
}
|
|
769
1095
|
}
|
|
770
1096
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
771
|
-
info.
|
|
1097
|
+
// if (info.locatorLog) {
|
|
1098
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
1099
|
+
// for (let line of lines) {
|
|
1100
|
+
// this.logger.debug(line);
|
|
1101
|
+
// }
|
|
1102
|
+
// }
|
|
1103
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
1104
|
+
info.failCause.locatorNotFound = true;
|
|
1105
|
+
if (!info?.failCause?.lastError) {
|
|
1106
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
1107
|
+
}
|
|
772
1108
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
773
1109
|
}
|
|
774
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1110
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
775
1111
|
let foundElements = [];
|
|
776
1112
|
const result = {
|
|
777
1113
|
foundElements: foundElements,
|
|
@@ -779,31 +1115,88 @@ class StableBrowser {
|
|
|
779
1115
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
780
1116
|
let foundLocators = [];
|
|
781
1117
|
try {
|
|
782
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1118
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
783
1119
|
}
|
|
784
1120
|
catch (e) {
|
|
785
|
-
this
|
|
786
|
-
this.logger.debug(
|
|
1121
|
+
// this call can fail it the browser is navigating
|
|
1122
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
1123
|
+
// this.logger.debug(e);
|
|
787
1124
|
foundLocators = [];
|
|
788
1125
|
try {
|
|
789
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
1126
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
790
1127
|
}
|
|
791
1128
|
catch (e) {
|
|
792
|
-
|
|
1129
|
+
if (logErrors) {
|
|
1130
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1131
|
+
}
|
|
793
1132
|
}
|
|
794
1133
|
}
|
|
795
1134
|
if (foundLocators.length === 1) {
|
|
1135
|
+
let box = null;
|
|
1136
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1137
|
+
box = await foundLocators[0].boundingBox();
|
|
1138
|
+
}
|
|
796
1139
|
result.foundElements.push({
|
|
797
1140
|
locator: foundLocators[0],
|
|
798
|
-
box:
|
|
1141
|
+
box: box,
|
|
799
1142
|
unique: true,
|
|
800
1143
|
});
|
|
801
1144
|
result.locatorIndex = i;
|
|
802
1145
|
}
|
|
1146
|
+
if (foundLocators.length > 1) {
|
|
1147
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
1148
|
+
const boxes = [];
|
|
1149
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
1150
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
1151
|
+
}
|
|
1152
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
1153
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
1154
|
+
if (j === k) {
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
1158
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
1159
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
1160
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
1161
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
1162
|
+
// as the element is not unique, will remove it
|
|
1163
|
+
boxes.splice(k, 1);
|
|
1164
|
+
k--;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (boxes.length === 1) {
|
|
1169
|
+
result.foundElements.push({
|
|
1170
|
+
locator: boxes[0].locator.first(),
|
|
1171
|
+
box: boxes[0].box,
|
|
1172
|
+
unique: true,
|
|
1173
|
+
});
|
|
1174
|
+
result.locatorIndex = i;
|
|
1175
|
+
}
|
|
1176
|
+
else if (logErrors) {
|
|
1177
|
+
info.failCause.foundMultiple = true;
|
|
1178
|
+
if (info.locatorLog) {
|
|
1179
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
803
1183
|
}
|
|
804
1184
|
return result;
|
|
805
1185
|
}
|
|
806
1186
|
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
1187
|
+
const state = {
|
|
1188
|
+
locate: false,
|
|
1189
|
+
scroll: false,
|
|
1190
|
+
highlight: false,
|
|
1191
|
+
_params,
|
|
1192
|
+
options,
|
|
1193
|
+
world,
|
|
1194
|
+
type: Types.CLICK,
|
|
1195
|
+
text: "Click element",
|
|
1196
|
+
operation: "simpleClick",
|
|
1197
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
1198
|
+
};
|
|
1199
|
+
_preCommand(state, this);
|
|
807
1200
|
const startTime = Date.now();
|
|
808
1201
|
let timeout = 30000;
|
|
809
1202
|
if (options && options.timeout) {
|
|
@@ -827,13 +1220,32 @@ class StableBrowser {
|
|
|
827
1220
|
}
|
|
828
1221
|
catch (e) {
|
|
829
1222
|
if (performance.now() - startTime > timeout) {
|
|
830
|
-
throw e;
|
|
1223
|
+
// throw e;
|
|
1224
|
+
try {
|
|
1225
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1226
|
+
}
|
|
1227
|
+
finally {
|
|
1228
|
+
await _commandFinally(state, this);
|
|
1229
|
+
}
|
|
831
1230
|
}
|
|
832
1231
|
}
|
|
833
1232
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
834
1233
|
}
|
|
835
1234
|
}
|
|
836
1235
|
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
1236
|
+
const state = {
|
|
1237
|
+
locate: false,
|
|
1238
|
+
scroll: false,
|
|
1239
|
+
highlight: false,
|
|
1240
|
+
_params,
|
|
1241
|
+
options,
|
|
1242
|
+
world,
|
|
1243
|
+
type: Types.FILL,
|
|
1244
|
+
text: "Fill element",
|
|
1245
|
+
operation: "simpleClickType",
|
|
1246
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
1247
|
+
};
|
|
1248
|
+
_preCommand(state, this);
|
|
837
1249
|
const startTime = Date.now();
|
|
838
1250
|
let timeout = 30000;
|
|
839
1251
|
if (options && options.timeout) {
|
|
@@ -857,7 +1269,13 @@ class StableBrowser {
|
|
|
857
1269
|
}
|
|
858
1270
|
catch (e) {
|
|
859
1271
|
if (performance.now() - startTime > timeout) {
|
|
860
|
-
throw e;
|
|
1272
|
+
// throw e;
|
|
1273
|
+
try {
|
|
1274
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
await _commandFinally(state, this);
|
|
1278
|
+
}
|
|
861
1279
|
}
|
|
862
1280
|
}
|
|
863
1281
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -870,34 +1288,74 @@ class StableBrowser {
|
|
|
870
1288
|
options,
|
|
871
1289
|
world,
|
|
872
1290
|
text: "Click element",
|
|
1291
|
+
_text: "Click on " + selectors.element_name,
|
|
873
1292
|
type: Types.CLICK,
|
|
874
1293
|
operation: "click",
|
|
875
1294
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
876
1295
|
};
|
|
1296
|
+
check_performance("click_all ***", this.context, true);
|
|
1297
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
1298
|
+
if (stepFastMode) {
|
|
1299
|
+
state.onlyFailuresScreenshot = true;
|
|
1300
|
+
state.scroll = false;
|
|
1301
|
+
state.highlight = false;
|
|
1302
|
+
}
|
|
877
1303
|
try {
|
|
1304
|
+
check_performance("click_preCommand", this.context, true);
|
|
878
1305
|
await _preCommand(state, this);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
await
|
|
884
|
-
|
|
1306
|
+
check_performance("click_preCommand", this.context, false);
|
|
1307
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1308
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1309
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1310
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1311
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
885
1312
|
}
|
|
886
|
-
catch (e) {
|
|
887
|
-
// await this.closeUnexpectedPopups();
|
|
888
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
889
|
-
await state.element.dispatchEvent("click");
|
|
890
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
891
|
-
}
|
|
892
|
-
await this.waitForPageLoad();
|
|
893
1313
|
return state.info;
|
|
894
1314
|
}
|
|
895
1315
|
catch (e) {
|
|
896
1316
|
await _commandError(state, e, this);
|
|
897
1317
|
}
|
|
898
1318
|
finally {
|
|
899
|
-
|
|
1319
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1320
|
+
await _commandFinally(state, this);
|
|
1321
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1322
|
+
check_performance("click_all ***", this.context, false);
|
|
1323
|
+
if (this.context.profile) {
|
|
1324
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1329
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1330
|
+
const state = {
|
|
1331
|
+
selectors,
|
|
1332
|
+
_params,
|
|
1333
|
+
options,
|
|
1334
|
+
world,
|
|
1335
|
+
text: "Wait for element",
|
|
1336
|
+
_text: "Wait for " + selectors.element_name,
|
|
1337
|
+
type: Types.WAIT_ELEMENT,
|
|
1338
|
+
operation: "waitForElement",
|
|
1339
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1340
|
+
};
|
|
1341
|
+
let found = false;
|
|
1342
|
+
try {
|
|
1343
|
+
await _preCommand(state, this);
|
|
1344
|
+
// if (state.options && state.options.context) {
|
|
1345
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1346
|
+
// }
|
|
1347
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1348
|
+
found = true;
|
|
1349
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1350
|
+
}
|
|
1351
|
+
catch (e) {
|
|
1352
|
+
console.error("Error on waitForElement", e);
|
|
1353
|
+
// await _commandError(state, e, this);
|
|
1354
|
+
}
|
|
1355
|
+
finally {
|
|
1356
|
+
await _commandFinally(state, this);
|
|
900
1357
|
}
|
|
1358
|
+
return found;
|
|
901
1359
|
}
|
|
902
1360
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
903
1361
|
const state = {
|
|
@@ -907,6 +1365,7 @@ class StableBrowser {
|
|
|
907
1365
|
world,
|
|
908
1366
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
909
1367
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1368
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
910
1369
|
operation: "setCheck",
|
|
911
1370
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
912
1371
|
};
|
|
@@ -916,30 +1375,53 @@ class StableBrowser {
|
|
|
916
1375
|
// let element = await this._locate(selectors, info, _params);
|
|
917
1376
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
918
1377
|
try {
|
|
919
|
-
//
|
|
920
|
-
|
|
1378
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1379
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1380
|
+
await this._highlightElements(state.element);
|
|
1381
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
921
1382
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1383
|
+
// await this._unHighlightElements(element);
|
|
1384
|
+
// }
|
|
1385
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1386
|
+
// await this._unHighlightElements(element);
|
|
922
1387
|
}
|
|
923
1388
|
catch (e) {
|
|
924
1389
|
if (e.message && e.message.includes("did not change its state")) {
|
|
925
1390
|
this.logger.info("element did not change its state, ignoring...");
|
|
926
1391
|
}
|
|
927
1392
|
else {
|
|
1393
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
928
1394
|
//await this.closeUnexpectedPopups();
|
|
929
1395
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
930
|
-
state.
|
|
931
|
-
|
|
932
|
-
|
|
1396
|
+
state.element_found = false;
|
|
1397
|
+
try {
|
|
1398
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1399
|
+
state.element_found = true;
|
|
1400
|
+
// check the check state
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
// element dismissed
|
|
1404
|
+
}
|
|
1405
|
+
if (state.element_found) {
|
|
1406
|
+
const isChecked = await state.element.isChecked();
|
|
1407
|
+
if (isChecked !== checked) {
|
|
1408
|
+
// perform click
|
|
1409
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
933
1415
|
}
|
|
934
1416
|
}
|
|
935
|
-
await this.waitForPageLoad();
|
|
1417
|
+
//await this.waitForPageLoad();
|
|
936
1418
|
return state.info;
|
|
937
1419
|
}
|
|
938
1420
|
catch (e) {
|
|
939
1421
|
await _commandError(state, e, this);
|
|
940
1422
|
}
|
|
941
1423
|
finally {
|
|
942
|
-
_commandFinally(state, this);
|
|
1424
|
+
await _commandFinally(state, this);
|
|
943
1425
|
}
|
|
944
1426
|
}
|
|
945
1427
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -950,31 +1432,22 @@ class StableBrowser {
|
|
|
950
1432
|
world,
|
|
951
1433
|
type: Types.HOVER,
|
|
952
1434
|
text: `Hover element`,
|
|
1435
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
953
1436
|
operation: "hover",
|
|
954
1437
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
955
1438
|
};
|
|
956
1439
|
try {
|
|
957
1440
|
await _preCommand(state, this);
|
|
958
|
-
|
|
959
|
-
await state.element.hover();
|
|
960
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
961
|
-
}
|
|
962
|
-
catch (e) {
|
|
963
|
-
//await this.closeUnexpectedPopups();
|
|
964
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
965
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
966
|
-
await state.element.hover({ timeout: 10000 });
|
|
967
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
968
|
-
}
|
|
1441
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
969
1442
|
await _screenshot(state, this);
|
|
970
|
-
await this.waitForPageLoad();
|
|
1443
|
+
//await this.waitForPageLoad();
|
|
971
1444
|
return state.info;
|
|
972
1445
|
}
|
|
973
1446
|
catch (e) {
|
|
974
1447
|
await _commandError(state, e, this);
|
|
975
1448
|
}
|
|
976
1449
|
finally {
|
|
977
|
-
_commandFinally(state, this);
|
|
1450
|
+
await _commandFinally(state, this);
|
|
978
1451
|
}
|
|
979
1452
|
}
|
|
980
1453
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -989,6 +1462,7 @@ class StableBrowser {
|
|
|
989
1462
|
value: values.toString(),
|
|
990
1463
|
type: Types.SELECT,
|
|
991
1464
|
text: `Select option: ${values}`,
|
|
1465
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
992
1466
|
operation: "selectOption",
|
|
993
1467
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
994
1468
|
};
|
|
@@ -1002,14 +1476,14 @@ class StableBrowser {
|
|
|
1002
1476
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1003
1477
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1004
1478
|
}
|
|
1005
|
-
await this.waitForPageLoad();
|
|
1479
|
+
//await this.waitForPageLoad();
|
|
1006
1480
|
return state.info;
|
|
1007
1481
|
}
|
|
1008
1482
|
catch (e) {
|
|
1009
1483
|
await _commandError(state, e, this);
|
|
1010
1484
|
}
|
|
1011
1485
|
finally {
|
|
1012
|
-
_commandFinally(state, this);
|
|
1486
|
+
await _commandFinally(state, this);
|
|
1013
1487
|
}
|
|
1014
1488
|
}
|
|
1015
1489
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1023,6 +1497,7 @@ class StableBrowser {
|
|
|
1023
1497
|
highlight: false,
|
|
1024
1498
|
type: Types.TYPE_PRESS,
|
|
1025
1499
|
text: `Type value: ${_value}`,
|
|
1500
|
+
_text: `Type value: ${_value}`,
|
|
1026
1501
|
operation: "type",
|
|
1027
1502
|
log: "",
|
|
1028
1503
|
};
|
|
@@ -1054,7 +1529,7 @@ class StableBrowser {
|
|
|
1054
1529
|
await _commandError(state, e, this);
|
|
1055
1530
|
}
|
|
1056
1531
|
finally {
|
|
1057
|
-
_commandFinally(state, this);
|
|
1532
|
+
await _commandFinally(state, this);
|
|
1058
1533
|
}
|
|
1059
1534
|
}
|
|
1060
1535
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1090,37 +1565,35 @@ class StableBrowser {
|
|
|
1090
1565
|
await _commandError(state, e, this);
|
|
1091
1566
|
}
|
|
1092
1567
|
finally {
|
|
1093
|
-
_commandFinally(state, this);
|
|
1568
|
+
await _commandFinally(state, this);
|
|
1094
1569
|
}
|
|
1095
1570
|
}
|
|
1096
1571
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1572
|
+
const state = {
|
|
1573
|
+
selectors,
|
|
1574
|
+
_params,
|
|
1575
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1576
|
+
options,
|
|
1577
|
+
world,
|
|
1578
|
+
type: Types.SET_DATE_TIME,
|
|
1579
|
+
text: `Set date time value: ${value}`,
|
|
1580
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1581
|
+
operation: "setDateTime",
|
|
1582
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1583
|
+
throwError: false,
|
|
1584
|
+
};
|
|
1107
1585
|
try {
|
|
1108
|
-
|
|
1109
|
-
let element = await this._locate(selectors, info, _params);
|
|
1110
|
-
//insert red border around the element
|
|
1111
|
-
await this.scrollIfNeeded(element, info);
|
|
1112
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1113
|
-
await this._highlightElements(element);
|
|
1586
|
+
await _preCommand(state, this);
|
|
1114
1587
|
try {
|
|
1115
|
-
await element
|
|
1588
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1116
1589
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1117
1590
|
if (format) {
|
|
1118
|
-
value = dayjs(value).format(format);
|
|
1119
|
-
await element.fill(value);
|
|
1591
|
+
state.value = dayjs(state.value).format(format);
|
|
1592
|
+
await state.element.fill(state.value);
|
|
1120
1593
|
}
|
|
1121
1594
|
else {
|
|
1122
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1123
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1595
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1596
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1124
1597
|
el.value = ""; // clear input
|
|
1125
1598
|
el.value = dateTimeValue;
|
|
1126
1599
|
}, dateTimeValue);
|
|
@@ -1133,20 +1606,19 @@ class StableBrowser {
|
|
|
1133
1606
|
}
|
|
1134
1607
|
catch (err) {
|
|
1135
1608
|
//await this.closeUnexpectedPopups();
|
|
1136
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1609
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1137
1610
|
this.logger.info("Trying again");
|
|
1138
|
-
|
|
1139
|
-
info.
|
|
1140
|
-
Object.assign(err, { info: info });
|
|
1611
|
+
await _screenshot(state, this);
|
|
1612
|
+
Object.assign(err, { info: state.info });
|
|
1141
1613
|
await element.click();
|
|
1142
1614
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1143
1615
|
if (format) {
|
|
1144
|
-
value = dayjs(value).format(format);
|
|
1145
|
-
await element.fill(value);
|
|
1616
|
+
state.value = dayjs(state.value).format(format);
|
|
1617
|
+
await state.element.fill(state.value);
|
|
1146
1618
|
}
|
|
1147
1619
|
else {
|
|
1148
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1149
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1620
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1621
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1150
1622
|
el.value = ""; // clear input
|
|
1151
1623
|
el.value = dateTimeValue;
|
|
1152
1624
|
}, dateTimeValue);
|
|
@@ -1159,55 +1631,47 @@ class StableBrowser {
|
|
|
1159
1631
|
}
|
|
1160
1632
|
}
|
|
1161
1633
|
catch (e) {
|
|
1162
|
-
|
|
1163
|
-
throw e;
|
|
1634
|
+
await _commandError(state, e, this);
|
|
1164
1635
|
}
|
|
1165
1636
|
finally {
|
|
1166
|
-
|
|
1167
|
-
this._reportToWorld(world, {
|
|
1168
|
-
element_name: selectors.element_name,
|
|
1169
|
-
type: Types.SET_DATE_TIME,
|
|
1170
|
-
screenshotId,
|
|
1171
|
-
value: value,
|
|
1172
|
-
text: `setDateTime input with value: ${value}`,
|
|
1173
|
-
result: error
|
|
1174
|
-
? {
|
|
1175
|
-
status: "FAILED",
|
|
1176
|
-
startTime,
|
|
1177
|
-
endTime,
|
|
1178
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1179
|
-
}
|
|
1180
|
-
: {
|
|
1181
|
-
status: "PASSED",
|
|
1182
|
-
startTime,
|
|
1183
|
-
endTime,
|
|
1184
|
-
},
|
|
1185
|
-
info: info,
|
|
1186
|
-
});
|
|
1637
|
+
await _commandFinally(state, this);
|
|
1187
1638
|
}
|
|
1188
1639
|
}
|
|
1189
1640
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1641
|
+
_value = unEscapeString(_value);
|
|
1642
|
+
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1190
1643
|
const state = {
|
|
1191
1644
|
selectors,
|
|
1192
1645
|
_params,
|
|
1193
|
-
value:
|
|
1646
|
+
value: newValue,
|
|
1647
|
+
originalValue: _value,
|
|
1194
1648
|
options,
|
|
1195
1649
|
world,
|
|
1196
1650
|
type: Types.FILL,
|
|
1197
1651
|
text: `Click type input with value: ${_value}`,
|
|
1652
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1198
1653
|
operation: "clickType",
|
|
1199
|
-
log: "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n",
|
|
1654
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1200
1655
|
};
|
|
1201
|
-
|
|
1656
|
+
if (!options) {
|
|
1657
|
+
options = {};
|
|
1658
|
+
}
|
|
1202
1659
|
if (newValue !== _value) {
|
|
1203
1660
|
//this.logger.info(_value + "=" + newValue);
|
|
1204
1661
|
_value = newValue;
|
|
1205
|
-
state.value = newValue;
|
|
1206
1662
|
}
|
|
1207
1663
|
try {
|
|
1208
1664
|
await _preCommand(state, this);
|
|
1665
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1666
|
+
// tag the element
|
|
1667
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1668
|
+
// use attribute and not id
|
|
1669
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1670
|
+
el.setAttribute(attrName, "");
|
|
1671
|
+
return `[${attrName}]`;
|
|
1672
|
+
}, randomToken);
|
|
1209
1673
|
state.info.value = _value;
|
|
1210
|
-
if (
|
|
1674
|
+
if (!options.press) {
|
|
1211
1675
|
try {
|
|
1212
1676
|
let currentValue = await state.element.inputValue();
|
|
1213
1677
|
if (currentValue) {
|
|
@@ -1218,13 +1682,9 @@ class StableBrowser {
|
|
|
1218
1682
|
this.logger.info("unable to clear input value");
|
|
1219
1683
|
}
|
|
1220
1684
|
}
|
|
1221
|
-
if (options
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
catch (e) {
|
|
1226
|
-
await state.element.dispatchEvent("click");
|
|
1227
|
-
}
|
|
1685
|
+
if (options.press) {
|
|
1686
|
+
options.timeout = 5000;
|
|
1687
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1228
1688
|
}
|
|
1229
1689
|
else {
|
|
1230
1690
|
try {
|
|
@@ -1235,6 +1695,25 @@ class StableBrowser {
|
|
|
1235
1695
|
}
|
|
1236
1696
|
}
|
|
1237
1697
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1698
|
+
// check if the element exist after the click (no wait)
|
|
1699
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1700
|
+
if (count === 0) {
|
|
1701
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1702
|
+
const scope = state.element._frame ?? element.page();
|
|
1703
|
+
let prefixSelector = "";
|
|
1704
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1705
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1706
|
+
if (frameSelectorIndex !== -1) {
|
|
1707
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1708
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1709
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1710
|
+
}
|
|
1711
|
+
// if (element?._frame?._selector) {
|
|
1712
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1713
|
+
// }
|
|
1714
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1715
|
+
state.element = scope.locator(newSelector).first();
|
|
1716
|
+
}
|
|
1238
1717
|
const valueSegment = state.value.split("&&");
|
|
1239
1718
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1240
1719
|
if (i > 0) {
|
|
@@ -1255,14 +1734,21 @@ class StableBrowser {
|
|
|
1255
1734
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1256
1735
|
}
|
|
1257
1736
|
}
|
|
1737
|
+
//if (!this.fastMode) {
|
|
1258
1738
|
await _screenshot(state, this);
|
|
1739
|
+
//}
|
|
1259
1740
|
if (enter === true) {
|
|
1260
1741
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1261
1742
|
await this.page.keyboard.press("Enter");
|
|
1262
1743
|
await this.waitForPageLoad();
|
|
1263
1744
|
}
|
|
1264
1745
|
else if (enter === false) {
|
|
1265
|
-
|
|
1746
|
+
try {
|
|
1747
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1748
|
+
}
|
|
1749
|
+
catch (e) {
|
|
1750
|
+
// ignore
|
|
1751
|
+
}
|
|
1266
1752
|
//await this.page.keyboard.press("Tab");
|
|
1267
1753
|
}
|
|
1268
1754
|
else {
|
|
@@ -1277,7 +1763,7 @@ class StableBrowser {
|
|
|
1277
1763
|
await _commandError(state, e, this);
|
|
1278
1764
|
}
|
|
1279
1765
|
finally {
|
|
1280
|
-
_commandFinally(state, this);
|
|
1766
|
+
await _commandFinally(state, this);
|
|
1281
1767
|
}
|
|
1282
1768
|
}
|
|
1283
1769
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1299,30 +1785,67 @@ class StableBrowser {
|
|
|
1299
1785
|
if (enter) {
|
|
1300
1786
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1301
1787
|
await this.page.keyboard.press("Enter");
|
|
1788
|
+
await this.waitForPageLoad();
|
|
1789
|
+
}
|
|
1790
|
+
return state.info;
|
|
1791
|
+
}
|
|
1792
|
+
catch (e) {
|
|
1793
|
+
await _commandError(state, e, this);
|
|
1794
|
+
}
|
|
1795
|
+
finally {
|
|
1796
|
+
await _commandFinally(state, this);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1800
|
+
const state = {
|
|
1801
|
+
selectors,
|
|
1802
|
+
_params,
|
|
1803
|
+
files,
|
|
1804
|
+
value: '"' + files.join('", "') + '"',
|
|
1805
|
+
options,
|
|
1806
|
+
world,
|
|
1807
|
+
type: Types.SET_INPUT_FILES,
|
|
1808
|
+
text: `Set input files`,
|
|
1809
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1810
|
+
operation: "setInputFiles",
|
|
1811
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1812
|
+
};
|
|
1813
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1814
|
+
try {
|
|
1815
|
+
await _preCommand(state, this);
|
|
1816
|
+
for (let i = 0; i < files.length; i++) {
|
|
1817
|
+
const file = files[i];
|
|
1818
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1819
|
+
if (!fs.existsSync(filePath)) {
|
|
1820
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1821
|
+
}
|
|
1822
|
+
state.files[i] = filePath;
|
|
1302
1823
|
}
|
|
1303
|
-
await
|
|
1824
|
+
await state.element.setInputFiles(files);
|
|
1304
1825
|
return state.info;
|
|
1305
1826
|
}
|
|
1306
1827
|
catch (e) {
|
|
1307
1828
|
await _commandError(state, e, this);
|
|
1308
1829
|
}
|
|
1309
1830
|
finally {
|
|
1310
|
-
_commandFinally(state, this);
|
|
1831
|
+
await _commandFinally(state, this);
|
|
1311
1832
|
}
|
|
1312
1833
|
}
|
|
1313
1834
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1314
1835
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1315
1836
|
}
|
|
1316
1837
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1838
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1317
1839
|
_validateSelectors(selectors);
|
|
1318
1840
|
let screenshotId = null;
|
|
1319
1841
|
let screenshotPath = null;
|
|
1320
1842
|
if (!info.log) {
|
|
1321
1843
|
info.log = "";
|
|
1844
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1322
1845
|
}
|
|
1323
1846
|
info.operation = "getText";
|
|
1324
1847
|
info.selectors = selectors;
|
|
1325
|
-
let element = await this._locate(selectors, info, _params);
|
|
1848
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1326
1849
|
if (climb > 0) {
|
|
1327
1850
|
const climbArray = [];
|
|
1328
1851
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1341,6 +1864,18 @@ class StableBrowser {
|
|
|
1341
1864
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1342
1865
|
try {
|
|
1343
1866
|
await this._highlightElements(element);
|
|
1867
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1868
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1869
|
+
// this._highlightElements(element)
|
|
1870
|
+
// .then(async () => {
|
|
1871
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1872
|
+
// this._unhighlightElements(element).then(
|
|
1873
|
+
// () => {}
|
|
1874
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1875
|
+
// );
|
|
1876
|
+
// })
|
|
1877
|
+
// .catch(e);
|
|
1878
|
+
// }
|
|
1344
1879
|
const elementText = await element.innerText();
|
|
1345
1880
|
return {
|
|
1346
1881
|
text: elementText,
|
|
@@ -1352,7 +1887,7 @@ class StableBrowser {
|
|
|
1352
1887
|
}
|
|
1353
1888
|
catch (e) {
|
|
1354
1889
|
//await this.closeUnexpectedPopups();
|
|
1355
|
-
this.logger.info("no innerText will use textContent");
|
|
1890
|
+
this.logger.info("no innerText, will use textContent");
|
|
1356
1891
|
const elementText = await element.textContent();
|
|
1357
1892
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1358
1893
|
}
|
|
@@ -1377,6 +1912,7 @@ class StableBrowser {
|
|
|
1377
1912
|
highlight: false,
|
|
1378
1913
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1379
1914
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1915
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1380
1916
|
operation: "containsPattern",
|
|
1381
1917
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1382
1918
|
};
|
|
@@ -1408,10 +1944,12 @@ class StableBrowser {
|
|
|
1408
1944
|
await _commandError(state, e, this);
|
|
1409
1945
|
}
|
|
1410
1946
|
finally {
|
|
1411
|
-
_commandFinally(state, this);
|
|
1947
|
+
await _commandFinally(state, this);
|
|
1412
1948
|
}
|
|
1413
1949
|
}
|
|
1414
1950
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1951
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1952
|
+
const startTime = Date.now();
|
|
1415
1953
|
const state = {
|
|
1416
1954
|
selectors,
|
|
1417
1955
|
_params,
|
|
@@ -1438,61 +1976,130 @@ class StableBrowser {
|
|
|
1438
1976
|
}
|
|
1439
1977
|
let foundObj = null;
|
|
1440
1978
|
try {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1448
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1449
|
-
if (dateAlternatives.date) {
|
|
1450
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1451
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1452
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1453
|
-
return state.info;
|
|
1979
|
+
while (Date.now() - startTime < timeout) {
|
|
1980
|
+
try {
|
|
1981
|
+
await _preCommand(state, this);
|
|
1982
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1983
|
+
if (foundObj && foundObj.element) {
|
|
1984
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1454
1985
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1986
|
+
await _screenshot(state, this);
|
|
1987
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1988
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1989
|
+
if (dateAlternatives.date) {
|
|
1990
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1991
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1992
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1993
|
+
return state.info;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
else if (numberAlternatives.number) {
|
|
1998
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1999
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
2000
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
2001
|
+
return state.info;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1462
2006
|
return state.info;
|
|
1463
2007
|
}
|
|
1464
2008
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
throw new Error("element doesn't contain text " + text);
|
|
2009
|
+
catch (e) {
|
|
2010
|
+
// Log error but continue retrying until timeout is reached
|
|
2011
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
2012
|
+
}
|
|
2013
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1471
2014
|
}
|
|
1472
|
-
|
|
2015
|
+
state.info.foundText = foundObj?.text;
|
|
2016
|
+
state.info.value = foundObj?.value;
|
|
2017
|
+
throw new Error("element doesn't contain text " + text);
|
|
1473
2018
|
}
|
|
1474
2019
|
catch (e) {
|
|
1475
2020
|
await _commandError(state, e, this);
|
|
2021
|
+
throw e;
|
|
1476
2022
|
}
|
|
1477
2023
|
finally {
|
|
1478
|
-
_commandFinally(state, this);
|
|
2024
|
+
await _commandFinally(state, this);
|
|
1479
2025
|
}
|
|
1480
2026
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
2027
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
2028
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2029
|
+
const startTime = Date.now();
|
|
2030
|
+
const state = {
|
|
2031
|
+
_params,
|
|
2032
|
+
value: referanceSnapshot,
|
|
2033
|
+
options,
|
|
2034
|
+
world,
|
|
2035
|
+
locate: false,
|
|
2036
|
+
scroll: false,
|
|
2037
|
+
screenshot: true,
|
|
2038
|
+
highlight: false,
|
|
2039
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
2040
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
2041
|
+
operation: "snapshotValidation",
|
|
2042
|
+
log: "***** verify snapshot *****\n",
|
|
2043
|
+
};
|
|
2044
|
+
if (!referanceSnapshot) {
|
|
2045
|
+
throw new Error("referanceSnapshot is null");
|
|
2046
|
+
}
|
|
2047
|
+
let text = null;
|
|
2048
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
2049
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1485
2050
|
}
|
|
1486
|
-
else if (this.
|
|
1487
|
-
|
|
2051
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
2052
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1488
2053
|
}
|
|
1489
|
-
else if (
|
|
1490
|
-
|
|
2054
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2055
|
+
text = referanceSnapshot.substring(5);
|
|
1491
2056
|
}
|
|
1492
2057
|
else {
|
|
1493
|
-
|
|
2058
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
2059
|
+
}
|
|
2060
|
+
state.text = text;
|
|
2061
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2062
|
+
await _preCommand(state, this);
|
|
2063
|
+
let foundObj = null;
|
|
2064
|
+
try {
|
|
2065
|
+
let matchResult = null;
|
|
2066
|
+
while (Date.now() - startTime < timeout) {
|
|
2067
|
+
try {
|
|
2068
|
+
let scope = null;
|
|
2069
|
+
if (!frameSelectors) {
|
|
2070
|
+
scope = this.page;
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
2074
|
+
}
|
|
2075
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
2076
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
2077
|
+
if (matchResult.errorLine !== -1) {
|
|
2078
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
2079
|
+
}
|
|
2080
|
+
// highlight and screenshot
|
|
2081
|
+
try {
|
|
2082
|
+
await await highlightSnapshot(newValue, scope);
|
|
2083
|
+
await _screenshot(state, this);
|
|
2084
|
+
}
|
|
2085
|
+
catch (e) { }
|
|
2086
|
+
return state.info;
|
|
2087
|
+
}
|
|
2088
|
+
catch (e) {
|
|
2089
|
+
// Log error but continue retrying until timeout is reached
|
|
2090
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
2091
|
+
}
|
|
2092
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
2093
|
+
}
|
|
2094
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
2095
|
+
}
|
|
2096
|
+
catch (e) {
|
|
2097
|
+
await _commandError(state, e, this);
|
|
2098
|
+
throw e;
|
|
2099
|
+
}
|
|
2100
|
+
finally {
|
|
2101
|
+
await _commandFinally(state, this);
|
|
1494
2102
|
}
|
|
1495
|
-
return dataFile;
|
|
1496
2103
|
}
|
|
1497
2104
|
async waitForUserInput(message, world = null) {
|
|
1498
2105
|
if (!message) {
|
|
@@ -1522,13 +2129,22 @@ class StableBrowser {
|
|
|
1522
2129
|
return;
|
|
1523
2130
|
}
|
|
1524
2131
|
// if data file exists, load it
|
|
1525
|
-
const dataFile =
|
|
2132
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1526
2133
|
let data = this.getTestData(world);
|
|
1527
2134
|
// merge the testData with the existing data
|
|
1528
2135
|
Object.assign(data, testData);
|
|
1529
2136
|
// save the data to the file
|
|
1530
2137
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1531
2138
|
}
|
|
2139
|
+
overwriteTestData(testData, world = null) {
|
|
2140
|
+
if (!testData) {
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
// if data file exists, load it
|
|
2144
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2145
|
+
// save the data to the file
|
|
2146
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
2147
|
+
}
|
|
1532
2148
|
_getDataFilePath(fileName) {
|
|
1533
2149
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1534
2150
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1625,12 +2241,7 @@ class StableBrowser {
|
|
|
1625
2241
|
}
|
|
1626
2242
|
}
|
|
1627
2243
|
getTestData(world = null) {
|
|
1628
|
-
|
|
1629
|
-
let data = {};
|
|
1630
|
-
if (fs.existsSync(dataFile)) {
|
|
1631
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1632
|
-
}
|
|
1633
|
-
return data;
|
|
2244
|
+
return _getTestData(world, this.context, this);
|
|
1634
2245
|
}
|
|
1635
2246
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1636
2247
|
// collect url/path/title
|
|
@@ -1657,11 +2268,9 @@ class StableBrowser {
|
|
|
1657
2268
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1658
2269
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1659
2270
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
2271
|
+
// to make sure the path doesn't start with -
|
|
2272
|
+
const uuidStr = "id_" + randomUUID();
|
|
2273
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1665
2274
|
try {
|
|
1666
2275
|
await this.takeScreenshot(screenshotPath);
|
|
1667
2276
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1671,15 +2280,15 @@ class StableBrowser {
|
|
|
1671
2280
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1672
2281
|
// }
|
|
1673
2282
|
// });
|
|
2283
|
+
result.screenshotId = uuidStr;
|
|
2284
|
+
result.screenshotPath = screenshotPath;
|
|
2285
|
+
if (info && info.box) {
|
|
2286
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
2287
|
+
}
|
|
1674
2288
|
}
|
|
1675
2289
|
catch (e) {
|
|
1676
2290
|
this.logger.info("unable to take screenshot, ignored");
|
|
1677
2291
|
}
|
|
1678
|
-
result.screenshotId = nextIndex;
|
|
1679
|
-
result.screenshotPath = screenshotPath;
|
|
1680
|
-
if (info && info.box) {
|
|
1681
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1682
|
-
}
|
|
1683
2292
|
}
|
|
1684
2293
|
else if (options && options.screenshot) {
|
|
1685
2294
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1714,6 +2323,15 @@ class StableBrowser {
|
|
|
1714
2323
|
document.documentElement.clientWidth,
|
|
1715
2324
|
])));
|
|
1716
2325
|
let screenshotBuffer = null;
|
|
2326
|
+
// if (focusedElement) {
|
|
2327
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2328
|
+
// await this._unhighlightElements(focusedElement);
|
|
2329
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2330
|
+
// console.log(`Unhighlighted previous element`);
|
|
2331
|
+
// }
|
|
2332
|
+
// if (focusedElement) {
|
|
2333
|
+
// await this._highlightElements(focusedElement);
|
|
2334
|
+
// }
|
|
1717
2335
|
if (this.context.browserName === "chromium") {
|
|
1718
2336
|
const client = await playContext.newCDPSession(this.page);
|
|
1719
2337
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
@@ -1735,6 +2353,10 @@ class StableBrowser {
|
|
|
1735
2353
|
else {
|
|
1736
2354
|
screenshotBuffer = await this.page.screenshot();
|
|
1737
2355
|
}
|
|
2356
|
+
// if (focusedElement) {
|
|
2357
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2358
|
+
// await this._unhighlightElements(focusedElement);
|
|
2359
|
+
// }
|
|
1738
2360
|
let image = await Jimp.read(screenshotBuffer);
|
|
1739
2361
|
// Get the image dimensions
|
|
1740
2362
|
const { width, height } = image.bitmap;
|
|
@@ -1747,6 +2369,7 @@ class StableBrowser {
|
|
|
1747
2369
|
else {
|
|
1748
2370
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1749
2371
|
}
|
|
2372
|
+
return screenshotBuffer;
|
|
1750
2373
|
}
|
|
1751
2374
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1752
2375
|
const state = {
|
|
@@ -1769,113 +2392,532 @@ class StableBrowser {
|
|
|
1769
2392
|
await _commandError(state, e, this);
|
|
1770
2393
|
}
|
|
1771
2394
|
finally {
|
|
1772
|
-
_commandFinally(state, this);
|
|
2395
|
+
await _commandFinally(state, this);
|
|
1773
2396
|
}
|
|
1774
2397
|
}
|
|
1775
2398
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2399
|
+
const state = {
|
|
2400
|
+
selectors,
|
|
2401
|
+
_params,
|
|
2402
|
+
attribute,
|
|
2403
|
+
variable,
|
|
2404
|
+
options,
|
|
2405
|
+
world,
|
|
2406
|
+
type: Types.EXTRACT,
|
|
2407
|
+
text: `Extract attribute from element`,
|
|
2408
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
2409
|
+
operation: "extractAttribute",
|
|
2410
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2411
|
+
allowDisabled: true,
|
|
2412
|
+
};
|
|
1781
2413
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1782
|
-
const info = {};
|
|
1783
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1784
|
-
info.operation = "extract";
|
|
1785
|
-
info.selectors = selectors;
|
|
1786
2414
|
try {
|
|
1787
|
-
|
|
1788
|
-
await this._highlightElements(element);
|
|
1789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2415
|
+
await _preCommand(state, this);
|
|
1790
2416
|
switch (attribute) {
|
|
1791
2417
|
case "inner_text":
|
|
1792
|
-
|
|
2418
|
+
state.value = await state.element.innerText();
|
|
1793
2419
|
break;
|
|
1794
2420
|
case "href":
|
|
1795
|
-
|
|
2421
|
+
state.value = await state.element.getAttribute("href");
|
|
1796
2422
|
break;
|
|
1797
2423
|
case "value":
|
|
1798
|
-
|
|
2424
|
+
state.value = await state.element.inputValue();
|
|
2425
|
+
break;
|
|
2426
|
+
case "text":
|
|
2427
|
+
state.value = await state.element.textContent();
|
|
1799
2428
|
break;
|
|
1800
2429
|
default:
|
|
1801
|
-
|
|
2430
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1802
2431
|
break;
|
|
1803
2432
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2433
|
+
if (options !== null) {
|
|
2434
|
+
if (options.regex && options.regex !== "") {
|
|
2435
|
+
// Construct a regex pattern from the provided string
|
|
2436
|
+
const regex = options.regex.slice(1, -1);
|
|
2437
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2438
|
+
const matches = state.value.match(regexPattern);
|
|
2439
|
+
if (matches) {
|
|
2440
|
+
let newValue = "";
|
|
2441
|
+
for (const match of matches) {
|
|
2442
|
+
newValue += match;
|
|
2443
|
+
}
|
|
2444
|
+
state.value = newValue;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2448
|
+
state.value = state.value.trim();
|
|
2449
|
+
}
|
|
1807
2450
|
}
|
|
1808
|
-
|
|
1809
|
-
this.
|
|
1810
|
-
|
|
2451
|
+
state.info.value = state.value;
|
|
2452
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2453
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2454
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2455
|
+
return state.info;
|
|
1811
2456
|
}
|
|
1812
2457
|
catch (e) {
|
|
1813
|
-
|
|
1814
|
-
this.logger.error("extract failed " + info.log);
|
|
1815
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1816
|
-
info.screenshotPath = screenshotPath;
|
|
1817
|
-
Object.assign(e, { info: info });
|
|
1818
|
-
error = e;
|
|
1819
|
-
throw e;
|
|
2458
|
+
await _commandError(state, e, this);
|
|
1820
2459
|
}
|
|
1821
2460
|
finally {
|
|
1822
|
-
|
|
1823
|
-
this._reportToWorld(world, {
|
|
1824
|
-
element_name: selectors.element_name,
|
|
1825
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1826
|
-
variable: variable,
|
|
1827
|
-
value: info.value,
|
|
1828
|
-
text: "Extract attribute from element",
|
|
1829
|
-
screenshotId,
|
|
1830
|
-
result: error
|
|
1831
|
-
? {
|
|
1832
|
-
status: "FAILED",
|
|
1833
|
-
startTime,
|
|
1834
|
-
endTime,
|
|
1835
|
-
message: error?.message,
|
|
1836
|
-
}
|
|
1837
|
-
: {
|
|
1838
|
-
status: "PASSED",
|
|
1839
|
-
startTime,
|
|
1840
|
-
endTime,
|
|
1841
|
-
},
|
|
1842
|
-
info: info,
|
|
1843
|
-
});
|
|
2461
|
+
await _commandFinally(state, this);
|
|
1844
2462
|
}
|
|
1845
2463
|
}
|
|
1846
|
-
async
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2464
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2465
|
+
const state = {
|
|
2466
|
+
selectors,
|
|
2467
|
+
_params,
|
|
2468
|
+
property,
|
|
2469
|
+
variable,
|
|
2470
|
+
options,
|
|
2471
|
+
world,
|
|
2472
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2473
|
+
text: `Extract property from element`,
|
|
2474
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2475
|
+
operation: "extractProperty",
|
|
2476
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2477
|
+
allowDisabled: true,
|
|
2478
|
+
};
|
|
2479
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2480
|
+
try {
|
|
2481
|
+
await _preCommand(state, this);
|
|
2482
|
+
switch (property) {
|
|
2483
|
+
case "inner_text":
|
|
2484
|
+
state.value = await state.element.innerText();
|
|
2485
|
+
break;
|
|
2486
|
+
case "href":
|
|
2487
|
+
state.value = await state.element.getAttribute("href");
|
|
2488
|
+
break;
|
|
2489
|
+
case "value":
|
|
2490
|
+
state.value = await state.element.inputValue();
|
|
2491
|
+
break;
|
|
2492
|
+
case "text":
|
|
2493
|
+
state.value = await state.element.textContent();
|
|
2494
|
+
break;
|
|
2495
|
+
default:
|
|
2496
|
+
if (property.startsWith("dataset.")) {
|
|
2497
|
+
const dataAttribute = property.substring(8);
|
|
2498
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2499
|
+
}
|
|
2500
|
+
else {
|
|
2501
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
if (options !== null) {
|
|
2505
|
+
if (options.regex && options.regex !== "") {
|
|
2506
|
+
// Construct a regex pattern from the provided string
|
|
2507
|
+
const regex = options.regex.slice(1, -1);
|
|
2508
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2509
|
+
const matches = state.value.match(regexPattern);
|
|
2510
|
+
if (matches) {
|
|
2511
|
+
let newValue = "";
|
|
2512
|
+
for (const match of matches) {
|
|
2513
|
+
newValue += match;
|
|
2514
|
+
}
|
|
2515
|
+
state.value = newValue;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2519
|
+
state.value = state.value.trim();
|
|
2520
|
+
}
|
|
1857
2521
|
}
|
|
2522
|
+
state.info.value = state.value;
|
|
2523
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2524
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2525
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2526
|
+
return state.info;
|
|
1858
2527
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
if (options && options.timeout) {
|
|
1862
|
-
timeout = options.timeout;
|
|
2528
|
+
catch (e) {
|
|
2529
|
+
await _commandError(state, e, this);
|
|
1863
2530
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2531
|
+
finally {
|
|
2532
|
+
await _commandFinally(state, this);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2536
|
+
const state = {
|
|
2537
|
+
selectors,
|
|
2538
|
+
_params,
|
|
2539
|
+
attribute,
|
|
2540
|
+
value,
|
|
2541
|
+
options,
|
|
2542
|
+
world,
|
|
2543
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2544
|
+
highlight: true,
|
|
2545
|
+
screenshot: true,
|
|
2546
|
+
text: `Verify element attribute`,
|
|
2547
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2548
|
+
operation: "verifyAttribute",
|
|
2549
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2550
|
+
allowDisabled: true,
|
|
1875
2551
|
};
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2552
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2553
|
+
let val;
|
|
2554
|
+
let expectedValue;
|
|
2555
|
+
try {
|
|
2556
|
+
await _preCommand(state, this);
|
|
2557
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2558
|
+
state.info.expectedValue = expectedValue;
|
|
2559
|
+
switch (attribute) {
|
|
2560
|
+
case "innerText":
|
|
2561
|
+
val = String(await state.element.innerText());
|
|
2562
|
+
break;
|
|
2563
|
+
case "text":
|
|
2564
|
+
val = String(await state.element.textContent());
|
|
2565
|
+
break;
|
|
2566
|
+
case "value":
|
|
2567
|
+
val = String(await state.element.inputValue());
|
|
2568
|
+
break;
|
|
2569
|
+
case "checked":
|
|
2570
|
+
val = String(await state.element.isChecked());
|
|
2571
|
+
break;
|
|
2572
|
+
case "disabled":
|
|
2573
|
+
val = String(await state.element.isDisabled());
|
|
2574
|
+
break;
|
|
2575
|
+
case "readOnly":
|
|
2576
|
+
const isEditable = await state.element.isEditable();
|
|
2577
|
+
val = String(!isEditable);
|
|
2578
|
+
break;
|
|
2579
|
+
default:
|
|
2580
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2581
|
+
break;
|
|
2582
|
+
}
|
|
2583
|
+
state.info.value = val;
|
|
2584
|
+
let regex;
|
|
2585
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2586
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2587
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2588
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2589
|
+
state.info.regex = true;
|
|
2590
|
+
}
|
|
2591
|
+
else {
|
|
2592
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2593
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2594
|
+
}
|
|
2595
|
+
if (attribute === "innerText") {
|
|
2596
|
+
if (state.info.regex) {
|
|
2597
|
+
if (!regex.test(val)) {
|
|
2598
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2599
|
+
state.info.failCause.assertionFailed = true;
|
|
2600
|
+
state.info.failCause.lastError = errorMessage;
|
|
2601
|
+
throw new Error(errorMessage);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
else {
|
|
2605
|
+
const valLines = val.split("\n");
|
|
2606
|
+
const expectedLines = expectedValue.split("\n");
|
|
2607
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2608
|
+
if (!isPart) {
|
|
2609
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2610
|
+
state.info.failCause.assertionFailed = true;
|
|
2611
|
+
state.info.failCause.lastError = errorMessage;
|
|
2612
|
+
throw new Error(errorMessage);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
else {
|
|
2617
|
+
if (!val.match(regex)) {
|
|
2618
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2619
|
+
state.info.failCause.assertionFailed = true;
|
|
2620
|
+
state.info.failCause.lastError = errorMessage;
|
|
2621
|
+
throw new Error(errorMessage);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return state.info;
|
|
2625
|
+
}
|
|
2626
|
+
catch (e) {
|
|
2627
|
+
await _commandError(state, e, this);
|
|
2628
|
+
}
|
|
2629
|
+
finally {
|
|
2630
|
+
await _commandFinally(state, this);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2634
|
+
const state = {
|
|
2635
|
+
selectors,
|
|
2636
|
+
_params,
|
|
2637
|
+
property,
|
|
2638
|
+
value,
|
|
2639
|
+
options,
|
|
2640
|
+
world,
|
|
2641
|
+
type: Types.VERIFY_PROPERTY,
|
|
2642
|
+
highlight: true,
|
|
2643
|
+
screenshot: true,
|
|
2644
|
+
text: `Verify element property`,
|
|
2645
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2646
|
+
operation: "verifyProperty",
|
|
2647
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2648
|
+
allowDisabled: true,
|
|
2649
|
+
};
|
|
2650
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2651
|
+
let val;
|
|
2652
|
+
let expectedValue;
|
|
2653
|
+
try {
|
|
2654
|
+
await _preCommand(state, this);
|
|
2655
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2656
|
+
state.info.expectedValue = expectedValue;
|
|
2657
|
+
switch (property) {
|
|
2658
|
+
case "innerText":
|
|
2659
|
+
val = String(await state.element.innerText());
|
|
2660
|
+
break;
|
|
2661
|
+
case "text":
|
|
2662
|
+
val = String(await state.element.textContent());
|
|
2663
|
+
break;
|
|
2664
|
+
case "value":
|
|
2665
|
+
val = String(await state.element.inputValue());
|
|
2666
|
+
break;
|
|
2667
|
+
case "checked":
|
|
2668
|
+
val = String(await state.element.isChecked());
|
|
2669
|
+
break;
|
|
2670
|
+
case "disabled":
|
|
2671
|
+
val = String(await state.element.isDisabled());
|
|
2672
|
+
break;
|
|
2673
|
+
case "readOnly":
|
|
2674
|
+
const isEditable = await state.element.isEditable();
|
|
2675
|
+
val = String(!isEditable);
|
|
2676
|
+
break;
|
|
2677
|
+
case "innerHTML":
|
|
2678
|
+
val = String(await state.element.innerHTML());
|
|
2679
|
+
break;
|
|
2680
|
+
case "outerHTML":
|
|
2681
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2682
|
+
break;
|
|
2683
|
+
default:
|
|
2684
|
+
if (property.startsWith("dataset.")) {
|
|
2685
|
+
const dataAttribute = property.substring(8);
|
|
2686
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2687
|
+
}
|
|
2688
|
+
else {
|
|
2689
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
// Helper function to remove all style="" attributes
|
|
2693
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2694
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2695
|
+
};
|
|
2696
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2697
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2698
|
+
val = removeStyleAttributes(val);
|
|
2699
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2700
|
+
}
|
|
2701
|
+
state.info.value = val;
|
|
2702
|
+
let regex;
|
|
2703
|
+
state.info.value = val;
|
|
2704
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2705
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2706
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2707
|
+
let matchPassed = false;
|
|
2708
|
+
if (isRegex) {
|
|
2709
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2710
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2711
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2712
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2713
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2714
|
+
const regex = new RegExp(patternBody, flags);
|
|
2715
|
+
state.info.regex = true;
|
|
2716
|
+
matchPassed = regex.test(val);
|
|
2717
|
+
}
|
|
2718
|
+
else {
|
|
2719
|
+
// Fallback: treat as literal
|
|
2720
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2721
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2722
|
+
matchPassed = regex.test(val);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
else if (isContains) {
|
|
2726
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2727
|
+
matchPassed = val.includes(containsValue);
|
|
2728
|
+
}
|
|
2729
|
+
else if (isExact) {
|
|
2730
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2731
|
+
matchPassed = val === exactValue;
|
|
2732
|
+
}
|
|
2733
|
+
else if (property === "innerText") {
|
|
2734
|
+
// Default innerText logic
|
|
2735
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2736
|
+
const valLines = val.split("\n");
|
|
2737
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2738
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2739
|
+
}
|
|
2740
|
+
else {
|
|
2741
|
+
// Fallback exact or loose match
|
|
2742
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2743
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2744
|
+
matchPassed = regex.test(val);
|
|
2745
|
+
}
|
|
2746
|
+
if (!matchPassed) {
|
|
2747
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2748
|
+
state.info.failCause.assertionFailed = true;
|
|
2749
|
+
state.info.failCause.lastError = errorMessage;
|
|
2750
|
+
throw new Error(errorMessage);
|
|
2751
|
+
}
|
|
2752
|
+
return state.info;
|
|
2753
|
+
}
|
|
2754
|
+
catch (e) {
|
|
2755
|
+
await _commandError(state, e, this);
|
|
2756
|
+
}
|
|
2757
|
+
finally {
|
|
2758
|
+
await _commandFinally(state, this);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2762
|
+
// Convert timeout from seconds to milliseconds
|
|
2763
|
+
const timeoutMs = timeout * 1000;
|
|
2764
|
+
const state = {
|
|
2765
|
+
selectors,
|
|
2766
|
+
_params,
|
|
2767
|
+
condition,
|
|
2768
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2769
|
+
options,
|
|
2770
|
+
world,
|
|
2771
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2772
|
+
highlight: true,
|
|
2773
|
+
screenshot: true,
|
|
2774
|
+
text: `Conditional wait for element`,
|
|
2775
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2776
|
+
operation: "conditionalWait",
|
|
2777
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2778
|
+
allowDisabled: true,
|
|
2779
|
+
info: {},
|
|
2780
|
+
};
|
|
2781
|
+
state.options ??= { timeout: timeoutMs };
|
|
2782
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2783
|
+
const startTime = Date.now();
|
|
2784
|
+
let conditionMet = false;
|
|
2785
|
+
let currentValue = null;
|
|
2786
|
+
let lastError = null;
|
|
2787
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2788
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2789
|
+
const elapsedTime = Date.now() - startTime;
|
|
2790
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2791
|
+
try {
|
|
2792
|
+
// Try to execute _preCommand (element location)
|
|
2793
|
+
await _preCommand(state, this);
|
|
2794
|
+
// If _preCommand succeeds, start condition checking
|
|
2795
|
+
const checkCondition = async () => {
|
|
2796
|
+
try {
|
|
2797
|
+
switch (condition.toLowerCase()) {
|
|
2798
|
+
case "checked":
|
|
2799
|
+
currentValue = await state.element.isChecked();
|
|
2800
|
+
return currentValue === true;
|
|
2801
|
+
case "unchecked":
|
|
2802
|
+
currentValue = await state.element.isChecked();
|
|
2803
|
+
return currentValue === false;
|
|
2804
|
+
case "visible":
|
|
2805
|
+
currentValue = await state.element.isVisible();
|
|
2806
|
+
return currentValue === true;
|
|
2807
|
+
case "hidden":
|
|
2808
|
+
currentValue = await state.element.isVisible();
|
|
2809
|
+
return currentValue === false;
|
|
2810
|
+
case "enabled":
|
|
2811
|
+
currentValue = await state.element.isDisabled();
|
|
2812
|
+
return currentValue === false;
|
|
2813
|
+
case "disabled":
|
|
2814
|
+
currentValue = await state.element.isDisabled();
|
|
2815
|
+
return currentValue === true;
|
|
2816
|
+
case "editable":
|
|
2817
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2818
|
+
currentValue = await state.element.isContentEditable();
|
|
2819
|
+
return currentValue === true;
|
|
2820
|
+
default:
|
|
2821
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2822
|
+
state.info.success = false;
|
|
2823
|
+
return false;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
catch (error) {
|
|
2827
|
+
// Don't throw here, just return false to continue retrying
|
|
2828
|
+
return false;
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
// Inner loop for condition checking (once element is located)
|
|
2832
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2833
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2834
|
+
conditionMet = await checkCondition();
|
|
2835
|
+
if (conditionMet) {
|
|
2836
|
+
break;
|
|
2837
|
+
}
|
|
2838
|
+
// Check if we still have time for another attempt
|
|
2839
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2840
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2841
|
+
}
|
|
2842
|
+
else {
|
|
2843
|
+
break;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
// If we got here and condition is met, break out of main loop
|
|
2847
|
+
if (conditionMet) {
|
|
2848
|
+
break;
|
|
2849
|
+
}
|
|
2850
|
+
// If condition not met but no exception, we've timed out
|
|
2851
|
+
break;
|
|
2852
|
+
}
|
|
2853
|
+
catch (e) {
|
|
2854
|
+
lastError = e;
|
|
2855
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2856
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2857
|
+
// Check if we have enough time left to retry
|
|
2858
|
+
if (timeLeft > 100) {
|
|
2859
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2860
|
+
}
|
|
2861
|
+
else {
|
|
2862
|
+
break;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2867
|
+
state.info = {
|
|
2868
|
+
success: conditionMet,
|
|
2869
|
+
conditionMet,
|
|
2870
|
+
actualWaitTime,
|
|
2871
|
+
currentValue,
|
|
2872
|
+
lastError: lastError?.message || null,
|
|
2873
|
+
message: conditionMet
|
|
2874
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2875
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2876
|
+
};
|
|
2877
|
+
if (lastError) {
|
|
2878
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2879
|
+
}
|
|
2880
|
+
try {
|
|
2881
|
+
await _commandFinally(state, this);
|
|
2882
|
+
}
|
|
2883
|
+
catch (finallyError) {
|
|
2884
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2885
|
+
}
|
|
2886
|
+
return state.info;
|
|
2887
|
+
}
|
|
2888
|
+
async extractEmailData(emailAddress, options, world) {
|
|
2889
|
+
if (!emailAddress) {
|
|
2890
|
+
throw new Error("email address is null");
|
|
2891
|
+
}
|
|
2892
|
+
// check if address contain @
|
|
2893
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
2894
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
2895
|
+
}
|
|
2896
|
+
else {
|
|
2897
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
2898
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const startTime = Date.now();
|
|
2902
|
+
let timeout = 60000;
|
|
2903
|
+
if (options && options.timeout) {
|
|
2904
|
+
timeout = options.timeout;
|
|
2905
|
+
}
|
|
2906
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
2907
|
+
const request = {
|
|
2908
|
+
method: "POST",
|
|
2909
|
+
url: serviceUrl,
|
|
2910
|
+
headers: {
|
|
2911
|
+
"Content-Type": "application/json",
|
|
2912
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2913
|
+
},
|
|
2914
|
+
data: JSON.stringify({
|
|
2915
|
+
email: emailAddress,
|
|
2916
|
+
}),
|
|
2917
|
+
};
|
|
2918
|
+
let errorCount = 0;
|
|
2919
|
+
while (true) {
|
|
2920
|
+
try {
|
|
1879
2921
|
let result = await this.context.api.request(request);
|
|
1880
2922
|
// the response body expected to be the following:
|
|
1881
2923
|
// {
|
|
@@ -1917,7 +2959,8 @@ class StableBrowser {
|
|
|
1917
2959
|
catch (e) {
|
|
1918
2960
|
errorCount++;
|
|
1919
2961
|
if (errorCount > 3) {
|
|
1920
|
-
throw e;
|
|
2962
|
+
// throw e;
|
|
2963
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1921
2964
|
}
|
|
1922
2965
|
// ignore
|
|
1923
2966
|
}
|
|
@@ -1931,27 +2974,32 @@ class StableBrowser {
|
|
|
1931
2974
|
async _highlightElements(scope, css) {
|
|
1932
2975
|
try {
|
|
1933
2976
|
if (!scope) {
|
|
2977
|
+
// console.log(`Scope is not defined`);
|
|
1934
2978
|
return;
|
|
1935
2979
|
}
|
|
1936
2980
|
if (!css) {
|
|
1937
2981
|
scope
|
|
1938
2982
|
.evaluate((node) => {
|
|
1939
2983
|
if (node && node.style) {
|
|
1940
|
-
let
|
|
1941
|
-
|
|
2984
|
+
let originalOutline = node.style.outline;
|
|
2985
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
2986
|
+
// node.__previousOutline = originalOutline;
|
|
2987
|
+
node.style.outline = "2px solid red";
|
|
2988
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
1942
2989
|
if (window) {
|
|
1943
2990
|
window.addEventListener("beforeunload", function (e) {
|
|
1944
|
-
node.style.
|
|
2991
|
+
node.style.outline = originalOutline;
|
|
1945
2992
|
});
|
|
1946
2993
|
}
|
|
1947
2994
|
setTimeout(function () {
|
|
1948
|
-
node.style.
|
|
2995
|
+
node.style.outline = originalOutline;
|
|
1949
2996
|
}, 2000);
|
|
1950
2997
|
}
|
|
1951
2998
|
})
|
|
1952
2999
|
.then(() => { })
|
|
1953
3000
|
.catch((e) => {
|
|
1954
3001
|
// ignore
|
|
3002
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1955
3003
|
});
|
|
1956
3004
|
}
|
|
1957
3005
|
else {
|
|
@@ -1967,17 +3015,18 @@ class StableBrowser {
|
|
|
1967
3015
|
if (!element.style) {
|
|
1968
3016
|
return;
|
|
1969
3017
|
}
|
|
1970
|
-
|
|
3018
|
+
let originalOutline = element.style.outline;
|
|
3019
|
+
element.__previousOutline = originalOutline;
|
|
1971
3020
|
// Set the new border to be red and 2px solid
|
|
1972
|
-
element.style.
|
|
3021
|
+
element.style.outline = "2px solid red";
|
|
1973
3022
|
if (window) {
|
|
1974
3023
|
window.addEventListener("beforeunload", function (e) {
|
|
1975
|
-
element.style.
|
|
3024
|
+
element.style.outline = originalOutline;
|
|
1976
3025
|
});
|
|
1977
3026
|
}
|
|
1978
3027
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1979
3028
|
setTimeout(function () {
|
|
1980
|
-
element.style.
|
|
3029
|
+
element.style.outline = originalOutline;
|
|
1981
3030
|
}, 2000);
|
|
1982
3031
|
}
|
|
1983
3032
|
return;
|
|
@@ -1985,6 +3034,7 @@ class StableBrowser {
|
|
|
1985
3034
|
.then(() => { })
|
|
1986
3035
|
.catch((e) => {
|
|
1987
3036
|
// ignore
|
|
3037
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1988
3038
|
});
|
|
1989
3039
|
}
|
|
1990
3040
|
}
|
|
@@ -1992,8 +3042,49 @@ class StableBrowser {
|
|
|
1992
3042
|
console.debug(error);
|
|
1993
3043
|
}
|
|
1994
3044
|
}
|
|
3045
|
+
_matcher(text) {
|
|
3046
|
+
if (!text) {
|
|
3047
|
+
return { matcher: "contains", queryText: "" };
|
|
3048
|
+
}
|
|
3049
|
+
if (text.length < 2) {
|
|
3050
|
+
return { matcher: "contains", queryText: text };
|
|
3051
|
+
}
|
|
3052
|
+
const split = text.split(":");
|
|
3053
|
+
const matcher = split[0].toLowerCase();
|
|
3054
|
+
const queryText = split.slice(1).join(":").trim();
|
|
3055
|
+
return { matcher, queryText };
|
|
3056
|
+
}
|
|
3057
|
+
_getDomain(url) {
|
|
3058
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
3059
|
+
return "";
|
|
3060
|
+
}
|
|
3061
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
3062
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
3063
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
3064
|
+
}
|
|
3065
|
+
let n = hostnameFragments.length;
|
|
3066
|
+
let fragments = [...hostnameFragments];
|
|
3067
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
3068
|
+
hostnameFragments.pop();
|
|
3069
|
+
n = hostnameFragments.length;
|
|
3070
|
+
}
|
|
3071
|
+
if (n == 0) {
|
|
3072
|
+
if (fragments[0] === "www")
|
|
3073
|
+
fragments = fragments.slice(1);
|
|
3074
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
3075
|
+
}
|
|
3076
|
+
if (hostnameFragments[0] === "www")
|
|
3077
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
3078
|
+
return hostnameFragments.join(".");
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Verify the page path matches the given path.
|
|
3082
|
+
* @param {string} pathPart - The path to verify.
|
|
3083
|
+
* @param {object} options - Options for verification.
|
|
3084
|
+
* @param {object} world - The world context.
|
|
3085
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3086
|
+
*/
|
|
1995
3087
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
1996
|
-
const startTime = Date.now();
|
|
1997
3088
|
let error = null;
|
|
1998
3089
|
let screenshotId = null;
|
|
1999
3090
|
let screenshotPath = null;
|
|
@@ -2007,159 +3098,534 @@ class StableBrowser {
|
|
|
2007
3098
|
pathPart = newValue;
|
|
2008
3099
|
}
|
|
2009
3100
|
info.pathPart = pathPart;
|
|
3101
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
3102
|
+
const state = {
|
|
3103
|
+
text_search: queryText,
|
|
3104
|
+
options,
|
|
3105
|
+
world,
|
|
3106
|
+
locate: false,
|
|
3107
|
+
scroll: false,
|
|
3108
|
+
highlight: false,
|
|
3109
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
3110
|
+
text: `Verify the page url is ${queryText}`,
|
|
3111
|
+
_text: `Verify the page url is ${queryText}`,
|
|
3112
|
+
operation: "verifyPagePath",
|
|
3113
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
3114
|
+
};
|
|
2010
3115
|
try {
|
|
3116
|
+
await _preCommand(state, this);
|
|
3117
|
+
state.info.text = queryText;
|
|
2011
3118
|
for (let i = 0; i < 30; i++) {
|
|
2012
3119
|
const url = await this.page.url();
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
3120
|
+
switch (matcher) {
|
|
3121
|
+
case "exact":
|
|
3122
|
+
if (url !== queryText) {
|
|
3123
|
+
if (i === 29) {
|
|
3124
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
3125
|
+
}
|
|
3126
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
break;
|
|
3130
|
+
case "contains":
|
|
3131
|
+
if (!url.includes(queryText)) {
|
|
3132
|
+
if (i === 29) {
|
|
3133
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
3134
|
+
}
|
|
3135
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3136
|
+
continue;
|
|
3137
|
+
}
|
|
3138
|
+
break;
|
|
3139
|
+
case "starts-with":
|
|
3140
|
+
{
|
|
3141
|
+
const domain = this._getDomain(url);
|
|
3142
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
3143
|
+
if (i === 29) {
|
|
3144
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
3145
|
+
}
|
|
3146
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3147
|
+
continue;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
break;
|
|
3151
|
+
case "ends-with":
|
|
3152
|
+
{
|
|
3153
|
+
const urlObj = new URL(url);
|
|
3154
|
+
let route = "/";
|
|
3155
|
+
if (urlObj.pathname !== "/") {
|
|
3156
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
3157
|
+
}
|
|
3158
|
+
else {
|
|
3159
|
+
route = "/";
|
|
3160
|
+
}
|
|
3161
|
+
if (route !== queryText) {
|
|
3162
|
+
if (i === 29) {
|
|
3163
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
3164
|
+
}
|
|
3165
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
break;
|
|
3170
|
+
case "regex":
|
|
3171
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3172
|
+
if (!regex.test(url)) {
|
|
3173
|
+
if (i === 29) {
|
|
3174
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
3175
|
+
}
|
|
3176
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3177
|
+
continue;
|
|
3178
|
+
}
|
|
3179
|
+
break;
|
|
3180
|
+
default:
|
|
3181
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3182
|
+
if (!url.includes(pathPart)) {
|
|
3183
|
+
if (i === 29) {
|
|
3184
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
3185
|
+
}
|
|
3186
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
await _screenshot(state, this);
|
|
3191
|
+
return state.info;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
catch (e) {
|
|
3195
|
+
state.info.failCause.lastError = e.message;
|
|
3196
|
+
state.info.failCause.assertionFailed = true;
|
|
3197
|
+
await _commandError(state, e, this);
|
|
3198
|
+
}
|
|
3199
|
+
finally {
|
|
3200
|
+
await _commandFinally(state, this);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Verify the page title matches the given title.
|
|
3205
|
+
* @param {string} title - The title to verify.
|
|
3206
|
+
* @param {object} options - Options for verification.
|
|
3207
|
+
* @param {object} world - The world context.
|
|
3208
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3209
|
+
*/
|
|
3210
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
3211
|
+
let error = null;
|
|
3212
|
+
let screenshotId = null;
|
|
3213
|
+
let screenshotPath = null;
|
|
3214
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3215
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
3216
|
+
if (newValue !== title) {
|
|
3217
|
+
this.logger.info(title + "=" + newValue);
|
|
3218
|
+
title = newValue;
|
|
3219
|
+
}
|
|
3220
|
+
const { matcher, queryText } = this._matcher(title);
|
|
3221
|
+
const state = {
|
|
3222
|
+
text_search: queryText,
|
|
3223
|
+
options,
|
|
3224
|
+
world,
|
|
3225
|
+
locate: false,
|
|
3226
|
+
scroll: false,
|
|
3227
|
+
highlight: false,
|
|
3228
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
3229
|
+
text: `Verify the page title is ${queryText}`,
|
|
3230
|
+
_text: `Verify the page title is ${queryText}`,
|
|
3231
|
+
operation: "verifyPageTitle",
|
|
3232
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
3233
|
+
};
|
|
3234
|
+
try {
|
|
3235
|
+
await _preCommand(state, this);
|
|
3236
|
+
state.info.text = queryText;
|
|
3237
|
+
for (let i = 0; i < 30; i++) {
|
|
3238
|
+
const foundTitle = await this.page.title();
|
|
3239
|
+
switch (matcher) {
|
|
3240
|
+
case "exact":
|
|
3241
|
+
if (foundTitle !== queryText) {
|
|
3242
|
+
if (i === 29) {
|
|
3243
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
3244
|
+
}
|
|
3245
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
break;
|
|
3249
|
+
case "contains":
|
|
3250
|
+
if (!foundTitle.includes(queryText)) {
|
|
3251
|
+
if (i === 29) {
|
|
3252
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
3253
|
+
}
|
|
3254
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
break;
|
|
3258
|
+
case "starts-with":
|
|
3259
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
3260
|
+
if (i === 29) {
|
|
3261
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
3262
|
+
}
|
|
3263
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
break;
|
|
3267
|
+
case "ends-with":
|
|
3268
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
3269
|
+
if (i === 29) {
|
|
3270
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
3271
|
+
}
|
|
3272
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
break;
|
|
3276
|
+
case "regex":
|
|
3277
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3278
|
+
if (!regex.test(foundTitle)) {
|
|
3279
|
+
if (i === 29) {
|
|
3280
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
3281
|
+
}
|
|
3282
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3283
|
+
continue;
|
|
3284
|
+
}
|
|
3285
|
+
break;
|
|
3286
|
+
default:
|
|
3287
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3288
|
+
if (!foundTitle.includes(title)) {
|
|
3289
|
+
if (i === 29) {
|
|
3290
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
3291
|
+
}
|
|
3292
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
await _screenshot(state, this);
|
|
3297
|
+
return state.info;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
catch (e) {
|
|
3301
|
+
state.info.failCause.lastError = e.message;
|
|
3302
|
+
state.info.failCause.assertionFailed = true;
|
|
3303
|
+
await _commandError(state, e, this);
|
|
3304
|
+
}
|
|
3305
|
+
finally {
|
|
3306
|
+
await _commandFinally(state, this);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
3310
|
+
const frames = this.page.frames();
|
|
3311
|
+
let results = [];
|
|
3312
|
+
// let ignoreCase = false;
|
|
3313
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3314
|
+
if (dateAlternatives.date) {
|
|
3315
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
3316
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3317
|
+
result.frame = frames[i];
|
|
3318
|
+
results.push(result);
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
else if (numberAlternatives.number) {
|
|
3322
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
3323
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3324
|
+
result.frame = frames[i];
|
|
3325
|
+
results.push(result);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
else {
|
|
3329
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3330
|
+
result.frame = frames[i];
|
|
3331
|
+
results.push(result);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
state.info.results = results;
|
|
3335
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3336
|
+
return resultWithElementsFound;
|
|
3337
|
+
}
|
|
3338
|
+
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
3339
|
+
text = unEscapeString(text);
|
|
3340
|
+
const state = {
|
|
3341
|
+
text_search: text,
|
|
3342
|
+
options,
|
|
3343
|
+
world,
|
|
3344
|
+
locate: false,
|
|
3345
|
+
scroll: false,
|
|
3346
|
+
highlight: false,
|
|
3347
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
3348
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
3349
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
3350
|
+
operation: "verifyTextExistInPage",
|
|
3351
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
3352
|
+
};
|
|
3353
|
+
if (testForRegex(text)) {
|
|
3354
|
+
text = text.replace(/\\"/g, '"');
|
|
3355
|
+
}
|
|
3356
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3357
|
+
//if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
3358
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
3359
|
+
if (!stepFastMode) {
|
|
3360
|
+
if (!this.fastMode) {
|
|
3361
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3362
|
+
}
|
|
3363
|
+
else {
|
|
3364
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
3368
|
+
if (newValue !== text) {
|
|
3369
|
+
this.logger.info(text + "=" + newValue);
|
|
3370
|
+
text = newValue;
|
|
3371
|
+
}
|
|
3372
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
3373
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
3374
|
+
if (stepFastMode) {
|
|
3375
|
+
state.onlyFailuresScreenshot = true;
|
|
3376
|
+
state.scroll = false;
|
|
3377
|
+
state.highlight = false;
|
|
3378
|
+
}
|
|
3379
|
+
try {
|
|
3380
|
+
await _preCommand(state, this);
|
|
3381
|
+
state.info.text = text;
|
|
3382
|
+
while (true) {
|
|
3383
|
+
let resultWithElementsFound = {
|
|
3384
|
+
length: 0,
|
|
3385
|
+
};
|
|
3386
|
+
try {
|
|
3387
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3388
|
+
}
|
|
3389
|
+
catch (error) {
|
|
3390
|
+
// ignore
|
|
3391
|
+
}
|
|
3392
|
+
if (resultWithElementsFound.length === 0) {
|
|
3393
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3394
|
+
throw new Error(`Text ${text} not found in page`);
|
|
2016
3395
|
}
|
|
2017
3396
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2018
3397
|
continue;
|
|
2019
3398
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
3399
|
+
try {
|
|
3400
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
3401
|
+
const frame = resultWithElementsFound[0].frame;
|
|
3402
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
3403
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3404
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3405
|
+
if (element) {
|
|
3406
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3407
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
await _screenshot(state, this);
|
|
3411
|
+
return state.info;
|
|
3412
|
+
}
|
|
3413
|
+
catch (error) {
|
|
3414
|
+
console.error(error);
|
|
3415
|
+
}
|
|
2022
3416
|
}
|
|
2023
3417
|
}
|
|
2024
3418
|
catch (e) {
|
|
2025
|
-
|
|
2026
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2027
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2028
|
-
info.screenshotPath = screenshotPath;
|
|
2029
|
-
Object.assign(e, { info: info });
|
|
2030
|
-
error = e;
|
|
2031
|
-
throw e;
|
|
3419
|
+
await _commandError(state, e, this);
|
|
2032
3420
|
}
|
|
2033
3421
|
finally {
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
3422
|
+
await _commandFinally(state, this);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
3426
|
+
text = unEscapeString(text);
|
|
3427
|
+
const state = {
|
|
3428
|
+
text_search: text,
|
|
3429
|
+
options,
|
|
3430
|
+
world,
|
|
3431
|
+
locate: false,
|
|
3432
|
+
scroll: false,
|
|
3433
|
+
highlight: false,
|
|
3434
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
3435
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
3436
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
3437
|
+
operation: "verifyTextNotExistInPage",
|
|
3438
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
3439
|
+
};
|
|
3440
|
+
if (testForRegex(text)) {
|
|
3441
|
+
text = text.replace(/\\"/g, '"');
|
|
3442
|
+
}
|
|
3443
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3444
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3445
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
3446
|
+
if (newValue !== text) {
|
|
3447
|
+
this.logger.info(text + "=" + newValue);
|
|
3448
|
+
text = newValue;
|
|
3449
|
+
}
|
|
3450
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
3451
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
3452
|
+
try {
|
|
3453
|
+
await _preCommand(state, this);
|
|
3454
|
+
state.info.text = text;
|
|
3455
|
+
let resultWithElementsFound = {
|
|
3456
|
+
length: null, // initial cannot be 0
|
|
3457
|
+
};
|
|
3458
|
+
while (true) {
|
|
3459
|
+
try {
|
|
3460
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3461
|
+
}
|
|
3462
|
+
catch (error) {
|
|
3463
|
+
// ignore
|
|
3464
|
+
}
|
|
3465
|
+
if (resultWithElementsFound.length === 0) {
|
|
3466
|
+
await _screenshot(state, this);
|
|
3467
|
+
return state.info;
|
|
3468
|
+
}
|
|
3469
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3470
|
+
throw new Error(`Text ${text} found in page`);
|
|
3471
|
+
}
|
|
3472
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
catch (e) {
|
|
3476
|
+
await _commandError(state, e, this);
|
|
3477
|
+
}
|
|
3478
|
+
finally {
|
|
3479
|
+
await _commandFinally(state, this);
|
|
2053
3480
|
}
|
|
2054
3481
|
}
|
|
2055
|
-
async
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
3482
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
3483
|
+
textAnchor = unEscapeString(textAnchor);
|
|
3484
|
+
textToVerify = unEscapeString(textToVerify);
|
|
3485
|
+
const state = {
|
|
3486
|
+
text_search: textToVerify,
|
|
3487
|
+
options,
|
|
3488
|
+
world,
|
|
3489
|
+
locate: false,
|
|
3490
|
+
scroll: false,
|
|
3491
|
+
highlight: false,
|
|
3492
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
3493
|
+
text: `Verify text with relation to another text`,
|
|
3494
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
3495
|
+
operation: "verify_text_with_relation",
|
|
3496
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3497
|
+
};
|
|
3498
|
+
const cmdStartTime = Date.now();
|
|
3499
|
+
let cmdEndTime = null;
|
|
3500
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2062
3501
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
let
|
|
3502
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
3503
|
+
if (newValue !== textAnchor) {
|
|
3504
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
3505
|
+
textAnchor = newValue;
|
|
3506
|
+
}
|
|
3507
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
3508
|
+
if (newValue !== textToVerify) {
|
|
3509
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
3510
|
+
textToVerify = newValue;
|
|
3511
|
+
}
|
|
3512
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
3513
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
3514
|
+
let foundAncore = false;
|
|
2074
3515
|
try {
|
|
3516
|
+
await _preCommand(state, this);
|
|
3517
|
+
state.info.text = textToVerify;
|
|
3518
|
+
let resultWithElementsFound = {
|
|
3519
|
+
length: 0,
|
|
3520
|
+
};
|
|
2075
3521
|
while (true) {
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
|
|
2082
|
-
result.frame = frames[i];
|
|
2083
|
-
results.push(result);
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
else if (numberAlternatives.number) {
|
|
2087
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2088
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
|
|
2089
|
-
result.frame = frames[i];
|
|
2090
|
-
results.push(result);
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
else {
|
|
2094
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
|
|
2095
|
-
result.frame = frames[i];
|
|
2096
|
-
results.push(result);
|
|
2097
|
-
}
|
|
3522
|
+
try {
|
|
3523
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3524
|
+
}
|
|
3525
|
+
catch (error) {
|
|
3526
|
+
// ignore
|
|
2098
3527
|
}
|
|
2099
|
-
info.results = results;
|
|
2100
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2101
3528
|
if (resultWithElementsFound.length === 0) {
|
|
2102
|
-
if (Date.now() - startTime > timeout) {
|
|
2103
|
-
throw new Error(`Text ${
|
|
3529
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3530
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2104
3531
|
}
|
|
2105
3532
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2106
3533
|
continue;
|
|
2107
3534
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
3535
|
+
else {
|
|
3536
|
+
cmdEndTime = Date.now();
|
|
3537
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3538
|
+
if (foundAncore) {
|
|
3539
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3540
|
+
}
|
|
3541
|
+
else {
|
|
3542
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
try {
|
|
3547
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3548
|
+
foundAncore = true;
|
|
3549
|
+
const result = resultWithElementsFound[i];
|
|
3550
|
+
const token = result.randomToken;
|
|
3551
|
+
const frame = result.frame;
|
|
3552
|
+
let css = `[data-blinq-id-${token}]`;
|
|
3553
|
+
const climbArray1 = [];
|
|
3554
|
+
for (let i = 0; i < climb; i++) {
|
|
3555
|
+
climbArray1.push("..");
|
|
3556
|
+
}
|
|
3557
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
3558
|
+
css = css + " >> " + climbXpath;
|
|
3559
|
+
const count = await frame.locator(css).count();
|
|
3560
|
+
for (let j = 0; j < count; j++) {
|
|
3561
|
+
const continer = await frame.locator(css).nth(j);
|
|
3562
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
3563
|
+
if (result.elementCount > 0) {
|
|
3564
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
3565
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3566
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
3567
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
3568
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
3569
|
+
// this._highlightElements(frame, dataAttribute)
|
|
3570
|
+
// .then(async () => {
|
|
3571
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3572
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
3573
|
+
// () => {}
|
|
3574
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
3575
|
+
// );
|
|
3576
|
+
// })
|
|
3577
|
+
// .catch(e);
|
|
3578
|
+
// }
|
|
3579
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
3580
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3581
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3582
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
3583
|
+
if (element) {
|
|
3584
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3585
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3586
|
+
}
|
|
3587
|
+
await _screenshot(state, this);
|
|
3588
|
+
return state.info;
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
2116
3591
|
}
|
|
2117
3592
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
3593
|
+
catch (error) {
|
|
3594
|
+
console.error(error);
|
|
3595
|
+
}
|
|
2120
3596
|
}
|
|
2121
3597
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2122
3598
|
}
|
|
2123
3599
|
catch (e) {
|
|
2124
|
-
|
|
2125
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2126
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2127
|
-
info.screenshotPath = screenshotPath;
|
|
2128
|
-
Object.assign(e, { info: info });
|
|
2129
|
-
error = e;
|
|
2130
|
-
throw e;
|
|
3600
|
+
await _commandError(state, e, this);
|
|
2131
3601
|
}
|
|
2132
3602
|
finally {
|
|
2133
|
-
|
|
2134
|
-
this._reportToWorld(world, {
|
|
2135
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2136
|
-
text: "Verify text exists in page",
|
|
2137
|
-
screenshotId,
|
|
2138
|
-
result: error
|
|
2139
|
-
? {
|
|
2140
|
-
status: "FAILED",
|
|
2141
|
-
startTime,
|
|
2142
|
-
endTime,
|
|
2143
|
-
message: error?.message,
|
|
2144
|
-
}
|
|
2145
|
-
: {
|
|
2146
|
-
status: "PASSED",
|
|
2147
|
-
startTime,
|
|
2148
|
-
endTime,
|
|
2149
|
-
},
|
|
2150
|
-
info: info,
|
|
2151
|
-
});
|
|
3603
|
+
await _commandFinally(state, this);
|
|
2152
3604
|
}
|
|
2153
3605
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
3606
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
3607
|
+
const frames = this.page.frames();
|
|
3608
|
+
let results = [];
|
|
3609
|
+
let ignoreCase = false;
|
|
3610
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3611
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
3612
|
+
result.frame = frames[i];
|
|
3613
|
+
const climbArray = [];
|
|
3614
|
+
for (let i = 0; i < climb; i++) {
|
|
3615
|
+
climbArray.push("..");
|
|
3616
|
+
}
|
|
3617
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
3618
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
3619
|
+
const count = await frames[i].locator(newLocator).count();
|
|
3620
|
+
if (count > 0) {
|
|
3621
|
+
result.elementCount = count;
|
|
3622
|
+
result.locator = newLocator;
|
|
3623
|
+
results.push(result);
|
|
3624
|
+
}
|
|
2161
3625
|
}
|
|
2162
|
-
|
|
3626
|
+
// state.info.results = results;
|
|
3627
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3628
|
+
return resultWithElementsFound;
|
|
2163
3629
|
}
|
|
2164
3630
|
async visualVerification(text, options = {}, world = null) {
|
|
2165
3631
|
const startTime = Date.now();
|
|
@@ -2175,14 +3641,17 @@ class StableBrowser {
|
|
|
2175
3641
|
throw new Error("TOKEN is not set");
|
|
2176
3642
|
}
|
|
2177
3643
|
try {
|
|
2178
|
-
let serviceUrl =
|
|
3644
|
+
let serviceUrl = _getServerUrl();
|
|
2179
3645
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2180
3646
|
info.screenshotPath = screenshotPath;
|
|
2181
3647
|
const screenshot = await this.takeScreenshot();
|
|
2182
|
-
|
|
2183
|
-
method: "
|
|
3648
|
+
let request = {
|
|
3649
|
+
method: "post",
|
|
3650
|
+
maxBodyLength: Infinity,
|
|
2184
3651
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2185
3652
|
headers: {
|
|
3653
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3654
|
+
"x-source": "aaa",
|
|
2186
3655
|
"Content-Type": "application/json",
|
|
2187
3656
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2188
3657
|
},
|
|
@@ -2191,7 +3660,7 @@ class StableBrowser {
|
|
|
2191
3660
|
screenshot: screenshot,
|
|
2192
3661
|
}),
|
|
2193
3662
|
};
|
|
2194
|
-
|
|
3663
|
+
const result = await axios.request(request);
|
|
2195
3664
|
if (result.data.status !== true) {
|
|
2196
3665
|
throw new Error("Visual validation failed");
|
|
2197
3666
|
}
|
|
@@ -2211,13 +3680,15 @@ class StableBrowser {
|
|
|
2211
3680
|
info.screenshotPath = screenshotPath;
|
|
2212
3681
|
Object.assign(e, { info: info });
|
|
2213
3682
|
error = e;
|
|
2214
|
-
throw e;
|
|
3683
|
+
// throw e;
|
|
3684
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2215
3685
|
}
|
|
2216
3686
|
finally {
|
|
2217
3687
|
const endTime = Date.now();
|
|
2218
|
-
|
|
3688
|
+
_reportToWorld(world, {
|
|
2219
3689
|
type: Types.VERIFY_VISUAL,
|
|
2220
3690
|
text: "Visual verification",
|
|
3691
|
+
_text: "Visual verification of " + text,
|
|
2221
3692
|
screenshotId,
|
|
2222
3693
|
result: error
|
|
2223
3694
|
? {
|
|
@@ -2263,6 +3734,7 @@ class StableBrowser {
|
|
|
2263
3734
|
let screenshotPath = null;
|
|
2264
3735
|
const info = {};
|
|
2265
3736
|
info.log = "";
|
|
3737
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2266
3738
|
info.operation = "getTableData";
|
|
2267
3739
|
info.selectors = selectors;
|
|
2268
3740
|
try {
|
|
@@ -2278,11 +3750,12 @@ class StableBrowser {
|
|
|
2278
3750
|
info.screenshotPath = screenshotPath;
|
|
2279
3751
|
Object.assign(e, { info: info });
|
|
2280
3752
|
error = e;
|
|
2281
|
-
throw e;
|
|
3753
|
+
// throw e;
|
|
3754
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2282
3755
|
}
|
|
2283
3756
|
finally {
|
|
2284
3757
|
const endTime = Date.now();
|
|
2285
|
-
|
|
3758
|
+
_reportToWorld(world, {
|
|
2286
3759
|
element_name: selectors.element_name,
|
|
2287
3760
|
type: Types.GET_TABLE_DATA,
|
|
2288
3761
|
text: "Get table data",
|
|
@@ -2337,7 +3810,7 @@ class StableBrowser {
|
|
|
2337
3810
|
info.operation = "analyzeTable";
|
|
2338
3811
|
info.selectors = selectors;
|
|
2339
3812
|
info.query = query;
|
|
2340
|
-
query =
|
|
3813
|
+
query = _fixUsingParams(query, _params);
|
|
2341
3814
|
info.query_fixed = query;
|
|
2342
3815
|
info.operator = operator;
|
|
2343
3816
|
info.value = value;
|
|
@@ -2443,11 +3916,12 @@ class StableBrowser {
|
|
|
2443
3916
|
info.screenshotPath = screenshotPath;
|
|
2444
3917
|
Object.assign(e, { info: info });
|
|
2445
3918
|
error = e;
|
|
2446
|
-
throw e;
|
|
3919
|
+
// throw e;
|
|
3920
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2447
3921
|
}
|
|
2448
3922
|
finally {
|
|
2449
3923
|
const endTime = Date.now();
|
|
2450
|
-
|
|
3924
|
+
_reportToWorld(world, {
|
|
2451
3925
|
element_name: selectors.element_name,
|
|
2452
3926
|
type: Types.ANALYZE_TABLE,
|
|
2453
3927
|
text: "Analyze table",
|
|
@@ -2468,28 +3942,51 @@ class StableBrowser {
|
|
|
2468
3942
|
});
|
|
2469
3943
|
}
|
|
2470
3944
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
3945
|
+
/**
|
|
3946
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3947
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3948
|
+
* @param options - Optional configuration object
|
|
3949
|
+
* @param world - Optional world context
|
|
3950
|
+
* @returns Promise that resolves after the specified duration
|
|
3951
|
+
*/
|
|
3952
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3953
|
+
const state = {
|
|
3954
|
+
duration,
|
|
3955
|
+
options,
|
|
3956
|
+
world,
|
|
3957
|
+
locate: false,
|
|
3958
|
+
scroll: false,
|
|
3959
|
+
screenshot: false,
|
|
3960
|
+
highlight: false,
|
|
3961
|
+
type: Types.SLEEP,
|
|
3962
|
+
text: `Sleep for ${duration} ms`,
|
|
3963
|
+
_text: `Sleep for ${duration} ms`,
|
|
3964
|
+
operation: "sleep",
|
|
3965
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3966
|
+
};
|
|
3967
|
+
try {
|
|
3968
|
+
await _preCommand(state, this);
|
|
3969
|
+
if (duration < 0) {
|
|
3970
|
+
throw new Error("Sleep duration cannot be negative");
|
|
2487
3971
|
}
|
|
3972
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3973
|
+
return state.info;
|
|
3974
|
+
}
|
|
3975
|
+
catch (e) {
|
|
3976
|
+
await _commandError(state, e, this);
|
|
3977
|
+
}
|
|
3978
|
+
finally {
|
|
3979
|
+
await _commandFinally(state, this);
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
3983
|
+
try {
|
|
3984
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2488
3985
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
3986
|
+
catch (error) {
|
|
3987
|
+
this.logger.debug(error);
|
|
3988
|
+
throw error;
|
|
2491
3989
|
}
|
|
2492
|
-
return value;
|
|
2493
3990
|
}
|
|
2494
3991
|
_getLoadTimeout(options) {
|
|
2495
3992
|
let timeout = 15000;
|
|
@@ -2501,7 +3998,54 @@ class StableBrowser {
|
|
|
2501
3998
|
}
|
|
2502
3999
|
return timeout;
|
|
2503
4000
|
}
|
|
4001
|
+
_getFindElementTimeout(options) {
|
|
4002
|
+
if (options && options.timeout) {
|
|
4003
|
+
return options.timeout;
|
|
4004
|
+
}
|
|
4005
|
+
if (this.configuration.find_element_timeout) {
|
|
4006
|
+
return this.configuration.find_element_timeout;
|
|
4007
|
+
}
|
|
4008
|
+
return 30000;
|
|
4009
|
+
}
|
|
4010
|
+
async saveStoreState(path = null, world = null) {
|
|
4011
|
+
const storageState = await this.page.context().storageState();
|
|
4012
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4013
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
4014
|
+
if (path) {
|
|
4015
|
+
// save { storageState: storageState } into the path
|
|
4016
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
4017
|
+
}
|
|
4018
|
+
else {
|
|
4019
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
async restoreSaveState(path = null, world = null) {
|
|
4023
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4024
|
+
await refreshBrowser(this, path, world);
|
|
4025
|
+
this.registerEventListeners(this.context);
|
|
4026
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
4027
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
4028
|
+
if (this.onRestoreSaveState) {
|
|
4029
|
+
this.onRestoreSaveState(path);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
2504
4032
|
async waitForPageLoad(options = {}, world = null) {
|
|
4033
|
+
// try {
|
|
4034
|
+
// let currentPagePath = null;
|
|
4035
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4036
|
+
// if (this.latestPagePath) {
|
|
4037
|
+
// // get the currect page path and compare with the latest page path
|
|
4038
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4039
|
+
// // if the page path is the same, do not wait for page load
|
|
4040
|
+
// console.log("No page change: " + currentPagePath);
|
|
4041
|
+
// return;
|
|
4042
|
+
// }
|
|
4043
|
+
// }
|
|
4044
|
+
// this.latestPagePath = currentPagePath;
|
|
4045
|
+
// } catch (e) {
|
|
4046
|
+
// console.debug("Error getting current page path: ", e);
|
|
4047
|
+
// }
|
|
4048
|
+
//console.log("Waiting for page load");
|
|
2505
4049
|
let timeout = this._getLoadTimeout(options);
|
|
2506
4050
|
const promiseArray = [];
|
|
2507
4051
|
// let waitForNetworkIdle = true;
|
|
@@ -2534,13 +4078,15 @@ class StableBrowser {
|
|
|
2534
4078
|
else if (e.label === "domcontentloaded") {
|
|
2535
4079
|
console.log("waited for the domcontent loaded timeout");
|
|
2536
4080
|
}
|
|
2537
|
-
console.log(".");
|
|
2538
4081
|
}
|
|
2539
4082
|
finally {
|
|
2540
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4083
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4084
|
+
if (options && !options.noSleep) {
|
|
4085
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4086
|
+
}
|
|
2541
4087
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2542
4088
|
const endTime = Date.now();
|
|
2543
|
-
|
|
4089
|
+
_reportToWorld(world, {
|
|
2544
4090
|
type: Types.GET_PAGE_STATUS,
|
|
2545
4091
|
text: "Wait for page load",
|
|
2546
4092
|
screenshotId,
|
|
@@ -2560,41 +4106,133 @@ class StableBrowser {
|
|
|
2560
4106
|
}
|
|
2561
4107
|
}
|
|
2562
4108
|
async closePage(options = {}, world = null) {
|
|
2563
|
-
const
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
4109
|
+
const state = {
|
|
4110
|
+
options,
|
|
4111
|
+
world,
|
|
4112
|
+
locate: false,
|
|
4113
|
+
scroll: false,
|
|
4114
|
+
highlight: false,
|
|
4115
|
+
type: Types.CLOSE_PAGE,
|
|
4116
|
+
text: `Close page`,
|
|
4117
|
+
_text: `Close the page`,
|
|
4118
|
+
operation: "closePage",
|
|
4119
|
+
log: "***** close page *****\n",
|
|
4120
|
+
throwError: false,
|
|
4121
|
+
};
|
|
2568
4122
|
try {
|
|
4123
|
+
await _preCommand(state, this);
|
|
2569
4124
|
await this.page.close();
|
|
2570
4125
|
}
|
|
2571
4126
|
catch (e) {
|
|
2572
|
-
|
|
4127
|
+
await _commandError(state, e, this);
|
|
2573
4128
|
}
|
|
2574
4129
|
finally {
|
|
2575
|
-
await
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
4130
|
+
await _commandFinally(state, this);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
4134
|
+
let operation = null;
|
|
4135
|
+
if (!options || !options.operation) {
|
|
4136
|
+
throw new Error("operation is not defined");
|
|
4137
|
+
}
|
|
4138
|
+
operation = options.operation;
|
|
4139
|
+
// validate operation is one of the supported operations
|
|
4140
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
4141
|
+
throw new Error("operation is not supported");
|
|
4142
|
+
}
|
|
4143
|
+
const state = {
|
|
4144
|
+
options,
|
|
4145
|
+
world,
|
|
4146
|
+
locate: false,
|
|
4147
|
+
scroll: false,
|
|
4148
|
+
highlight: false,
|
|
4149
|
+
type: Types.TABLE_OPERATION,
|
|
4150
|
+
text: `Table operation`,
|
|
4151
|
+
_text: `Table ${operation} operation`,
|
|
4152
|
+
operation: operation,
|
|
4153
|
+
log: "***** Table operation *****\n",
|
|
4154
|
+
};
|
|
4155
|
+
const timeout = this._getFindElementTimeout(options);
|
|
4156
|
+
try {
|
|
4157
|
+
await _preCommand(state, this);
|
|
4158
|
+
const start = Date.now();
|
|
4159
|
+
let cellArea = null;
|
|
4160
|
+
while (true) {
|
|
4161
|
+
try {
|
|
4162
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
4163
|
+
if (cellArea) {
|
|
4164
|
+
break;
|
|
2588
4165
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
4166
|
+
}
|
|
4167
|
+
catch (e) {
|
|
4168
|
+
// ignore
|
|
4169
|
+
}
|
|
4170
|
+
if (Date.now() - start > timeout) {
|
|
4171
|
+
throw new Error(`Cell not found in table`);
|
|
4172
|
+
}
|
|
4173
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
4174
|
+
}
|
|
4175
|
+
switch (operation) {
|
|
4176
|
+
case "click":
|
|
4177
|
+
if (!options.css) {
|
|
4178
|
+
// will click in the center of the cell
|
|
4179
|
+
let xOffset = 0;
|
|
4180
|
+
let yOffset = 0;
|
|
4181
|
+
if (options.xOffset) {
|
|
4182
|
+
xOffset = options.xOffset;
|
|
4183
|
+
}
|
|
4184
|
+
if (options.yOffset) {
|
|
4185
|
+
yOffset = options.yOffset;
|
|
4186
|
+
}
|
|
4187
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
4188
|
+
}
|
|
4189
|
+
else {
|
|
4190
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4191
|
+
if (results.length === 0) {
|
|
4192
|
+
throw new Error(`Element not found in cell area`);
|
|
4193
|
+
}
|
|
4194
|
+
state.element = results[0];
|
|
4195
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
4196
|
+
}
|
|
4197
|
+
break;
|
|
4198
|
+
case "hover+click":
|
|
4199
|
+
if (!options.css) {
|
|
4200
|
+
throw new Error("css is not defined");
|
|
4201
|
+
}
|
|
4202
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4203
|
+
if (results.length === 0) {
|
|
4204
|
+
throw new Error(`Element not found in cell area`);
|
|
4205
|
+
}
|
|
4206
|
+
state.element = results[0];
|
|
4207
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
4208
|
+
break;
|
|
4209
|
+
case "hover":
|
|
4210
|
+
if (!options.css) {
|
|
4211
|
+
throw new Error("css is not defined");
|
|
4212
|
+
}
|
|
4213
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4214
|
+
if (result1.length === 0) {
|
|
4215
|
+
throw new Error(`Element not found in cell area`);
|
|
4216
|
+
}
|
|
4217
|
+
state.element = result1[0];
|
|
4218
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4219
|
+
break;
|
|
4220
|
+
default:
|
|
4221
|
+
throw new Error("operation is not supported");
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
catch (e) {
|
|
4225
|
+
await _commandError(state, e, this);
|
|
4226
|
+
}
|
|
4227
|
+
finally {
|
|
4228
|
+
await _commandFinally(state, this);
|
|
2596
4229
|
}
|
|
2597
4230
|
}
|
|
4231
|
+
saveTestDataAsGlobal(options, world) {
|
|
4232
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
4233
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4234
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
4235
|
+
}
|
|
2598
4236
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2599
4237
|
const startTime = Date.now();
|
|
2600
4238
|
let error = null;
|
|
@@ -2611,15 +4249,16 @@ class StableBrowser {
|
|
|
2611
4249
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2612
4250
|
}
|
|
2613
4251
|
catch (e) {
|
|
2614
|
-
|
|
4252
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2615
4253
|
}
|
|
2616
4254
|
finally {
|
|
2617
4255
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2618
4256
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2619
4257
|
const endTime = Date.now();
|
|
2620
|
-
|
|
4258
|
+
_reportToWorld(world, {
|
|
2621
4259
|
type: Types.SET_VIEWPORT,
|
|
2622
4260
|
text: "set viewport size to " + width + "x" + hight,
|
|
4261
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2623
4262
|
screenshotId,
|
|
2624
4263
|
result: error
|
|
2625
4264
|
? {
|
|
@@ -2647,13 +4286,13 @@ class StableBrowser {
|
|
|
2647
4286
|
await this.page.reload();
|
|
2648
4287
|
}
|
|
2649
4288
|
catch (e) {
|
|
2650
|
-
|
|
4289
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2651
4290
|
}
|
|
2652
4291
|
finally {
|
|
2653
4292
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2654
4293
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2655
4294
|
const endTime = Date.now();
|
|
2656
|
-
|
|
4295
|
+
_reportToWorld(world, {
|
|
2657
4296
|
type: Types.GET_PAGE_STATUS,
|
|
2658
4297
|
text: "page relaod",
|
|
2659
4298
|
screenshotId,
|
|
@@ -2689,11 +4328,239 @@ class StableBrowser {
|
|
|
2689
4328
|
console.log("#-#");
|
|
2690
4329
|
}
|
|
2691
4330
|
}
|
|
2692
|
-
|
|
2693
|
-
if (
|
|
2694
|
-
|
|
4331
|
+
async beforeScenario(world, scenario) {
|
|
4332
|
+
if (world && world.attach) {
|
|
4333
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4334
|
+
}
|
|
4335
|
+
this.context.loadedRoutes = null;
|
|
4336
|
+
this.beforeScenarioCalled = true;
|
|
4337
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4338
|
+
this.scenarioName = scenario.pickle.name;
|
|
4339
|
+
}
|
|
4340
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
4341
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
4342
|
+
}
|
|
4343
|
+
if (this.context) {
|
|
4344
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
4345
|
+
}
|
|
4346
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4347
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4348
|
+
// check if @global_test_data tag is present
|
|
4349
|
+
if (this.tags.includes("@global_test_data")) {
|
|
4350
|
+
this.saveTestDataAsGlobal({}, world);
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
// update test data based on feature/scenario
|
|
4354
|
+
let envName = null;
|
|
4355
|
+
if (this.context && this.context.environment) {
|
|
4356
|
+
envName = this.context.environment.name;
|
|
4357
|
+
}
|
|
4358
|
+
if (!process.env.TEMP_RUN) {
|
|
4359
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4360
|
+
}
|
|
4361
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4362
|
+
}
|
|
4363
|
+
async afterScenario(world, scenario) { }
|
|
4364
|
+
async beforeStep(world, step) {
|
|
4365
|
+
this.stepTags = [];
|
|
4366
|
+
if (!this.beforeScenarioCalled) {
|
|
4367
|
+
this.beforeScenario(world, step);
|
|
4368
|
+
this.context.loadedRoutes = null;
|
|
4369
|
+
}
|
|
4370
|
+
if (this.stepIndex === undefined) {
|
|
4371
|
+
this.stepIndex = 0;
|
|
4372
|
+
}
|
|
4373
|
+
else {
|
|
4374
|
+
this.stepIndex++;
|
|
4375
|
+
}
|
|
4376
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4377
|
+
this.stepName = step.pickleStep.text;
|
|
4378
|
+
let printableStepName = this.stepName;
|
|
4379
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4380
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4381
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4382
|
+
});
|
|
4383
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4384
|
+
}
|
|
4385
|
+
else if (step && step.text) {
|
|
4386
|
+
this.stepName = step.text;
|
|
4387
|
+
}
|
|
4388
|
+
else {
|
|
4389
|
+
this.stepName = "step " + this.stepIndex;
|
|
4390
|
+
}
|
|
4391
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4392
|
+
if (this.context.browserObject.context) {
|
|
4393
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
if (this.initSnapshotTaken === false) {
|
|
4397
|
+
this.initSnapshotTaken = true;
|
|
4398
|
+
if (world &&
|
|
4399
|
+
world.attach &&
|
|
4400
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4401
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4402
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4403
|
+
if (snapshot) {
|
|
4404
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
this.context.routeResults = null;
|
|
4409
|
+
this.context.loadedRoutes = null;
|
|
4410
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4411
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4412
|
+
}
|
|
4413
|
+
setStepTags(tags) {
|
|
4414
|
+
this.stepTags = tags;
|
|
4415
|
+
}
|
|
4416
|
+
async getAriaSnapshot() {
|
|
4417
|
+
try {
|
|
4418
|
+
// find the page url
|
|
4419
|
+
const url = await this.page.url();
|
|
4420
|
+
// extract the path from the url
|
|
4421
|
+
const path = new URL(url).pathname;
|
|
4422
|
+
// get the page title
|
|
4423
|
+
const title = await this.page.title();
|
|
4424
|
+
// go over other frams
|
|
4425
|
+
const frames = this.page.frames();
|
|
4426
|
+
const snapshots = [];
|
|
4427
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4428
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4429
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4430
|
+
const frame = frames[i];
|
|
4431
|
+
try {
|
|
4432
|
+
// Ensure frame is attached and has body
|
|
4433
|
+
const body = frame.locator("body");
|
|
4434
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4435
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4436
|
+
if (!snapshot) {
|
|
4437
|
+
continue;
|
|
4438
|
+
}
|
|
4439
|
+
content.push(`- frame: ${i}`);
|
|
4440
|
+
content.push(snapshot);
|
|
4441
|
+
}
|
|
4442
|
+
catch (innerErr) {
|
|
4443
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4444
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
return content.join("\n");
|
|
4448
|
+
}
|
|
4449
|
+
catch (e) {
|
|
4450
|
+
console.log("Error in getAriaSnapshot");
|
|
4451
|
+
//console.debug(e);
|
|
4452
|
+
}
|
|
4453
|
+
return null;
|
|
4454
|
+
}
|
|
4455
|
+
/**
|
|
4456
|
+
* Sends command with custom payload to report.
|
|
4457
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4458
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4459
|
+
* @param content - Content of the command to be shown in the report.
|
|
4460
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4461
|
+
* @param world - Optional world context.
|
|
4462
|
+
* @public
|
|
4463
|
+
*/
|
|
4464
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4465
|
+
const state = {
|
|
4466
|
+
options,
|
|
4467
|
+
world,
|
|
4468
|
+
locate: false,
|
|
4469
|
+
scroll: false,
|
|
4470
|
+
screenshot: options.screenshot ?? false,
|
|
4471
|
+
highlight: options.highlight ?? false,
|
|
4472
|
+
type: Types.REPORT_COMMAND,
|
|
4473
|
+
text: commandText,
|
|
4474
|
+
_text: commandText,
|
|
4475
|
+
operation: "report_command",
|
|
4476
|
+
log: "***** " + commandText + " *****\n",
|
|
4477
|
+
};
|
|
4478
|
+
try {
|
|
4479
|
+
await _preCommand(state, this);
|
|
4480
|
+
const payload = {
|
|
4481
|
+
type: options.type ?? "text",
|
|
4482
|
+
content: content,
|
|
4483
|
+
screenshotId: null,
|
|
4484
|
+
};
|
|
4485
|
+
state.payload = payload;
|
|
4486
|
+
if (commandStatus === "FAILED") {
|
|
4487
|
+
state.throwError = true;
|
|
4488
|
+
throw new Error("Command failed");
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
catch (e) {
|
|
4492
|
+
await _commandError(state, e, this);
|
|
4493
|
+
}
|
|
4494
|
+
finally {
|
|
4495
|
+
await _commandFinally(state, this);
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
async afterStep(world, step) {
|
|
4499
|
+
this.stepName = null;
|
|
4500
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4501
|
+
if (this.context.browserObject.context) {
|
|
4502
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
4503
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4504
|
+
});
|
|
4505
|
+
if (world && world.attach) {
|
|
4506
|
+
await world.attach(JSON.stringify({
|
|
4507
|
+
type: "trace",
|
|
4508
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4509
|
+
}), "application/json+trace");
|
|
4510
|
+
}
|
|
4511
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
if (this.context) {
|
|
4515
|
+
this.context.examplesRow = null;
|
|
4516
|
+
}
|
|
4517
|
+
if (world &&
|
|
4518
|
+
world.attach &&
|
|
4519
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4520
|
+
!this.fastMode &&
|
|
4521
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4522
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4523
|
+
if (snapshot) {
|
|
4524
|
+
const obj = {};
|
|
4525
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4529
|
+
if (this.context.routeResults) {
|
|
4530
|
+
if (world && world.attach) {
|
|
4531
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
if (!process.env.TEMP_RUN) {
|
|
4535
|
+
const state = {
|
|
4536
|
+
world,
|
|
4537
|
+
locate: false,
|
|
4538
|
+
scroll: false,
|
|
4539
|
+
screenshot: true,
|
|
4540
|
+
highlight: true,
|
|
4541
|
+
type: Types.STEP_COMPLETE,
|
|
4542
|
+
text: "end of scenario",
|
|
4543
|
+
_text: "end of scenario",
|
|
4544
|
+
operation: "step_complete",
|
|
4545
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4546
|
+
};
|
|
4547
|
+
try {
|
|
4548
|
+
await _preCommand(state, this);
|
|
4549
|
+
}
|
|
4550
|
+
catch (e) {
|
|
4551
|
+
await _commandError(state, e, this);
|
|
4552
|
+
}
|
|
4553
|
+
finally {
|
|
4554
|
+
await _commandFinally(state, this);
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
networkAfterStep(this.stepName, this.context);
|
|
4558
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4559
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4560
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4561
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4562
|
+
}
|
|
2695
4563
|
}
|
|
2696
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2697
4564
|
}
|
|
2698
4565
|
}
|
|
2699
4566
|
function createTimedPromise(promise, label) {
|
|
@@ -2701,156 +4568,5 @@ function createTimedPromise(promise, label) {
|
|
|
2701
4568
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2702
4569
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2703
4570
|
}
|
|
2704
|
-
const KEYBOARD_EVENTS = [
|
|
2705
|
-
"ALT",
|
|
2706
|
-
"AltGraph",
|
|
2707
|
-
"CapsLock",
|
|
2708
|
-
"Control",
|
|
2709
|
-
"Fn",
|
|
2710
|
-
"FnLock",
|
|
2711
|
-
"Hyper",
|
|
2712
|
-
"Meta",
|
|
2713
|
-
"NumLock",
|
|
2714
|
-
"ScrollLock",
|
|
2715
|
-
"Shift",
|
|
2716
|
-
"Super",
|
|
2717
|
-
"Symbol",
|
|
2718
|
-
"SymbolLock",
|
|
2719
|
-
"Enter",
|
|
2720
|
-
"Tab",
|
|
2721
|
-
"ArrowDown",
|
|
2722
|
-
"ArrowLeft",
|
|
2723
|
-
"ArrowRight",
|
|
2724
|
-
"ArrowUp",
|
|
2725
|
-
"End",
|
|
2726
|
-
"Home",
|
|
2727
|
-
"PageDown",
|
|
2728
|
-
"PageUp",
|
|
2729
|
-
"Backspace",
|
|
2730
|
-
"Clear",
|
|
2731
|
-
"Copy",
|
|
2732
|
-
"CrSel",
|
|
2733
|
-
"Cut",
|
|
2734
|
-
"Delete",
|
|
2735
|
-
"EraseEof",
|
|
2736
|
-
"ExSel",
|
|
2737
|
-
"Insert",
|
|
2738
|
-
"Paste",
|
|
2739
|
-
"Redo",
|
|
2740
|
-
"Undo",
|
|
2741
|
-
"Accept",
|
|
2742
|
-
"Again",
|
|
2743
|
-
"Attn",
|
|
2744
|
-
"Cancel",
|
|
2745
|
-
"ContextMenu",
|
|
2746
|
-
"Escape",
|
|
2747
|
-
"Execute",
|
|
2748
|
-
"Find",
|
|
2749
|
-
"Finish",
|
|
2750
|
-
"Help",
|
|
2751
|
-
"Pause",
|
|
2752
|
-
"Play",
|
|
2753
|
-
"Props",
|
|
2754
|
-
"Select",
|
|
2755
|
-
"ZoomIn",
|
|
2756
|
-
"ZoomOut",
|
|
2757
|
-
"BrightnessDown",
|
|
2758
|
-
"BrightnessUp",
|
|
2759
|
-
"Eject",
|
|
2760
|
-
"LogOff",
|
|
2761
|
-
"Power",
|
|
2762
|
-
"PowerOff",
|
|
2763
|
-
"PrintScreen",
|
|
2764
|
-
"Hibernate",
|
|
2765
|
-
"Standby",
|
|
2766
|
-
"WakeUp",
|
|
2767
|
-
"AllCandidates",
|
|
2768
|
-
"Alphanumeric",
|
|
2769
|
-
"CodeInput",
|
|
2770
|
-
"Compose",
|
|
2771
|
-
"Convert",
|
|
2772
|
-
"Dead",
|
|
2773
|
-
"FinalMode",
|
|
2774
|
-
"GroupFirst",
|
|
2775
|
-
"GroupLast",
|
|
2776
|
-
"GroupNext",
|
|
2777
|
-
"GroupPrevious",
|
|
2778
|
-
"ModeChange",
|
|
2779
|
-
"NextCandidate",
|
|
2780
|
-
"NonConvert",
|
|
2781
|
-
"PreviousCandidate",
|
|
2782
|
-
"Process",
|
|
2783
|
-
"SingleCandidate",
|
|
2784
|
-
"HangulMode",
|
|
2785
|
-
"HanjaMode",
|
|
2786
|
-
"JunjaMode",
|
|
2787
|
-
"Eisu",
|
|
2788
|
-
"Hankaku",
|
|
2789
|
-
"Hiragana",
|
|
2790
|
-
"HiraganaKatakana",
|
|
2791
|
-
"KanaMode",
|
|
2792
|
-
"KanjiMode",
|
|
2793
|
-
"Katakana",
|
|
2794
|
-
"Romaji",
|
|
2795
|
-
"Zenkaku",
|
|
2796
|
-
"ZenkakuHanaku",
|
|
2797
|
-
"F1",
|
|
2798
|
-
"F2",
|
|
2799
|
-
"F3",
|
|
2800
|
-
"F4",
|
|
2801
|
-
"F5",
|
|
2802
|
-
"F6",
|
|
2803
|
-
"F7",
|
|
2804
|
-
"F8",
|
|
2805
|
-
"F9",
|
|
2806
|
-
"F10",
|
|
2807
|
-
"F11",
|
|
2808
|
-
"F12",
|
|
2809
|
-
"Soft1",
|
|
2810
|
-
"Soft2",
|
|
2811
|
-
"Soft3",
|
|
2812
|
-
"Soft4",
|
|
2813
|
-
"ChannelDown",
|
|
2814
|
-
"ChannelUp",
|
|
2815
|
-
"Close",
|
|
2816
|
-
"MailForward",
|
|
2817
|
-
"MailReply",
|
|
2818
|
-
"MailSend",
|
|
2819
|
-
"MediaFastForward",
|
|
2820
|
-
"MediaPause",
|
|
2821
|
-
"MediaPlay",
|
|
2822
|
-
"MediaPlayPause",
|
|
2823
|
-
"MediaRecord",
|
|
2824
|
-
"MediaRewind",
|
|
2825
|
-
"MediaStop",
|
|
2826
|
-
"MediaTrackNext",
|
|
2827
|
-
"MediaTrackPrevious",
|
|
2828
|
-
"AudioBalanceLeft",
|
|
2829
|
-
"AudioBalanceRight",
|
|
2830
|
-
"AudioBassBoostDown",
|
|
2831
|
-
"AudioBassBoostToggle",
|
|
2832
|
-
"AudioBassBoostUp",
|
|
2833
|
-
"AudioFaderFront",
|
|
2834
|
-
"AudioFaderRear",
|
|
2835
|
-
"AudioSurroundModeNext",
|
|
2836
|
-
"AudioTrebleDown",
|
|
2837
|
-
"AudioTrebleUp",
|
|
2838
|
-
"AudioVolumeDown",
|
|
2839
|
-
"AudioVolumeMute",
|
|
2840
|
-
"AudioVolumeUp",
|
|
2841
|
-
"MicrophoneToggle",
|
|
2842
|
-
"MicrophoneVolumeDown",
|
|
2843
|
-
"MicrophoneVolumeMute",
|
|
2844
|
-
"MicrophoneVolumeUp",
|
|
2845
|
-
"TV",
|
|
2846
|
-
"TV3DMode",
|
|
2847
|
-
"TVAntennaCable",
|
|
2848
|
-
"TVAudioDescription",
|
|
2849
|
-
];
|
|
2850
|
-
function unEscapeString(str) {
|
|
2851
|
-
const placeholder = "__NEWLINE__";
|
|
2852
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2853
|
-
return str;
|
|
2854
|
-
}
|
|
2855
4571
|
export { StableBrowser };
|
|
2856
4572
|
//# sourceMappingURL=stable_browser.js.map
|