automation_model 1.0.481-dev → 1.0.481
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 +2538 -837
- 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,34 @@ 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
|
+
if (locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
975
|
+
locator.css = locator.css + " >> visible=true";
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if (!info) {
|
|
980
|
+
info = {};
|
|
981
|
+
info.failCause = {};
|
|
982
|
+
info.log = "";
|
|
983
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
984
|
+
}
|
|
665
985
|
let highPriorityTimeout = 5000;
|
|
666
986
|
let visibleOnlyTimeout = 6000;
|
|
667
|
-
let startTime =
|
|
987
|
+
let startTime = Date.now();
|
|
668
988
|
let locatorsCount = 0;
|
|
989
|
+
let lazy_scroll = false;
|
|
669
990
|
//let arrayMode = Array.isArray(selectors);
|
|
670
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
671
991
|
let selectorsLocators = null;
|
|
672
992
|
selectorsLocators = selectors.locators;
|
|
673
993
|
// group selectors by priority
|
|
@@ -695,6 +1015,7 @@ class StableBrowser {
|
|
|
695
1015
|
let highPriorityOnly = true;
|
|
696
1016
|
let visibleOnly = true;
|
|
697
1017
|
while (true) {
|
|
1018
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
698
1019
|
locatorsCount = 0;
|
|
699
1020
|
let result = [];
|
|
700
1021
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -703,18 +1024,13 @@ class StableBrowser {
|
|
|
703
1024
|
}
|
|
704
1025
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
705
1026
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
706
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
1027
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
707
1028
|
if (result.foundElements.length === 0) {
|
|
708
1029
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
709
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
1030
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
710
1031
|
}
|
|
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
|
-
}
|
|
1032
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1033
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
718
1034
|
}
|
|
719
1035
|
let foundElements = result.foundElements;
|
|
720
1036
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -754,24 +1070,43 @@ class StableBrowser {
|
|
|
754
1070
|
return maxCountElement.locator;
|
|
755
1071
|
}
|
|
756
1072
|
}
|
|
757
|
-
if (
|
|
1073
|
+
if (Date.now() - startTime > timeout) {
|
|
758
1074
|
break;
|
|
759
1075
|
}
|
|
760
|
-
if (
|
|
761
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
1076
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
1077
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
762
1078
|
highPriorityOnly = false;
|
|
1079
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
1080
|
+
lazy_scroll = true;
|
|
1081
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
1082
|
+
}
|
|
763
1083
|
}
|
|
764
|
-
if (
|
|
765
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
1084
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
1085
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
766
1086
|
visibleOnly = false;
|
|
767
1087
|
}
|
|
768
1088
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1089
|
+
// sheck of more of half of the timeout has passed
|
|
1090
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
1091
|
+
highPriorityOnly = false;
|
|
1092
|
+
visibleOnly = false;
|
|
1093
|
+
}
|
|
769
1094
|
}
|
|
770
1095
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
771
|
-
info.
|
|
1096
|
+
// if (info.locatorLog) {
|
|
1097
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
1098
|
+
// for (let line of lines) {
|
|
1099
|
+
// this.logger.debug(line);
|
|
1100
|
+
// }
|
|
1101
|
+
// }
|
|
1102
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
1103
|
+
info.failCause.locatorNotFound = true;
|
|
1104
|
+
if (!info?.failCause?.lastError) {
|
|
1105
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
1106
|
+
}
|
|
772
1107
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
773
1108
|
}
|
|
774
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1109
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
775
1110
|
let foundElements = [];
|
|
776
1111
|
const result = {
|
|
777
1112
|
foundElements: foundElements,
|
|
@@ -779,31 +1114,88 @@ class StableBrowser {
|
|
|
779
1114
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
780
1115
|
let foundLocators = [];
|
|
781
1116
|
try {
|
|
782
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1117
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
783
1118
|
}
|
|
784
1119
|
catch (e) {
|
|
785
|
-
this
|
|
786
|
-
this.logger.debug(
|
|
1120
|
+
// this call can fail it the browser is navigating
|
|
1121
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
1122
|
+
// this.logger.debug(e);
|
|
787
1123
|
foundLocators = [];
|
|
788
1124
|
try {
|
|
789
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
1125
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
790
1126
|
}
|
|
791
1127
|
catch (e) {
|
|
792
|
-
|
|
1128
|
+
if (logErrors) {
|
|
1129
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1130
|
+
}
|
|
793
1131
|
}
|
|
794
1132
|
}
|
|
795
1133
|
if (foundLocators.length === 1) {
|
|
1134
|
+
let box = null;
|
|
1135
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1136
|
+
box = await foundLocators[0].boundingBox();
|
|
1137
|
+
}
|
|
796
1138
|
result.foundElements.push({
|
|
797
1139
|
locator: foundLocators[0],
|
|
798
|
-
box:
|
|
1140
|
+
box: box,
|
|
799
1141
|
unique: true,
|
|
800
1142
|
});
|
|
801
1143
|
result.locatorIndex = i;
|
|
802
1144
|
}
|
|
1145
|
+
if (foundLocators.length > 1) {
|
|
1146
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
1147
|
+
const boxes = [];
|
|
1148
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
1149
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
1150
|
+
}
|
|
1151
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
1152
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
1153
|
+
if (j === k) {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
1157
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
1158
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
1159
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
1160
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
1161
|
+
// as the element is not unique, will remove it
|
|
1162
|
+
boxes.splice(k, 1);
|
|
1163
|
+
k--;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (boxes.length === 1) {
|
|
1168
|
+
result.foundElements.push({
|
|
1169
|
+
locator: boxes[0].locator.first(),
|
|
1170
|
+
box: boxes[0].box,
|
|
1171
|
+
unique: true,
|
|
1172
|
+
});
|
|
1173
|
+
result.locatorIndex = i;
|
|
1174
|
+
}
|
|
1175
|
+
else if (logErrors) {
|
|
1176
|
+
info.failCause.foundMultiple = true;
|
|
1177
|
+
if (info.locatorLog) {
|
|
1178
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
803
1182
|
}
|
|
804
1183
|
return result;
|
|
805
1184
|
}
|
|
806
1185
|
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
1186
|
+
const state = {
|
|
1187
|
+
locate: false,
|
|
1188
|
+
scroll: false,
|
|
1189
|
+
highlight: false,
|
|
1190
|
+
_params,
|
|
1191
|
+
options,
|
|
1192
|
+
world,
|
|
1193
|
+
type: Types.CLICK,
|
|
1194
|
+
text: "Click element",
|
|
1195
|
+
operation: "simpleClick",
|
|
1196
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
1197
|
+
};
|
|
1198
|
+
_preCommand(state, this);
|
|
807
1199
|
const startTime = Date.now();
|
|
808
1200
|
let timeout = 30000;
|
|
809
1201
|
if (options && options.timeout) {
|
|
@@ -827,13 +1219,32 @@ class StableBrowser {
|
|
|
827
1219
|
}
|
|
828
1220
|
catch (e) {
|
|
829
1221
|
if (performance.now() - startTime > timeout) {
|
|
830
|
-
throw e;
|
|
1222
|
+
// throw e;
|
|
1223
|
+
try {
|
|
1224
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1225
|
+
}
|
|
1226
|
+
finally {
|
|
1227
|
+
await _commandFinally(state, this);
|
|
1228
|
+
}
|
|
831
1229
|
}
|
|
832
1230
|
}
|
|
833
1231
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
834
1232
|
}
|
|
835
1233
|
}
|
|
836
1234
|
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
1235
|
+
const state = {
|
|
1236
|
+
locate: false,
|
|
1237
|
+
scroll: false,
|
|
1238
|
+
highlight: false,
|
|
1239
|
+
_params,
|
|
1240
|
+
options,
|
|
1241
|
+
world,
|
|
1242
|
+
type: Types.FILL,
|
|
1243
|
+
text: "Fill element",
|
|
1244
|
+
operation: "simpleClickType",
|
|
1245
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
1246
|
+
};
|
|
1247
|
+
_preCommand(state, this);
|
|
837
1248
|
const startTime = Date.now();
|
|
838
1249
|
let timeout = 30000;
|
|
839
1250
|
if (options && options.timeout) {
|
|
@@ -857,7 +1268,13 @@ class StableBrowser {
|
|
|
857
1268
|
}
|
|
858
1269
|
catch (e) {
|
|
859
1270
|
if (performance.now() - startTime > timeout) {
|
|
860
|
-
throw e;
|
|
1271
|
+
// throw e;
|
|
1272
|
+
try {
|
|
1273
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1274
|
+
}
|
|
1275
|
+
finally {
|
|
1276
|
+
await _commandFinally(state, this);
|
|
1277
|
+
}
|
|
861
1278
|
}
|
|
862
1279
|
}
|
|
863
1280
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -870,34 +1287,74 @@ class StableBrowser {
|
|
|
870
1287
|
options,
|
|
871
1288
|
world,
|
|
872
1289
|
text: "Click element",
|
|
1290
|
+
_text: "Click on " + selectors.element_name,
|
|
873
1291
|
type: Types.CLICK,
|
|
874
1292
|
operation: "click",
|
|
875
1293
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
876
1294
|
};
|
|
1295
|
+
check_performance("click_all ***", this.context, true);
|
|
1296
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
1297
|
+
if (stepFastMode) {
|
|
1298
|
+
state.onlyFailuresScreenshot = true;
|
|
1299
|
+
state.scroll = false;
|
|
1300
|
+
state.highlight = false;
|
|
1301
|
+
}
|
|
877
1302
|
try {
|
|
1303
|
+
check_performance("click_preCommand", this.context, true);
|
|
878
1304
|
await _preCommand(state, this);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
await
|
|
884
|
-
|
|
1305
|
+
check_performance("click_preCommand", this.context, false);
|
|
1306
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1307
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1308
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1309
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1310
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
885
1311
|
}
|
|
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
1312
|
return state.info;
|
|
894
1313
|
}
|
|
895
1314
|
catch (e) {
|
|
896
1315
|
await _commandError(state, e, this);
|
|
897
1316
|
}
|
|
898
1317
|
finally {
|
|
899
|
-
|
|
1318
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1319
|
+
await _commandFinally(state, this);
|
|
1320
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1321
|
+
check_performance("click_all ***", this.context, false);
|
|
1322
|
+
if (this.context.profile) {
|
|
1323
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1328
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1329
|
+
const state = {
|
|
1330
|
+
selectors,
|
|
1331
|
+
_params,
|
|
1332
|
+
options,
|
|
1333
|
+
world,
|
|
1334
|
+
text: "Wait for element",
|
|
1335
|
+
_text: "Wait for " + selectors.element_name,
|
|
1336
|
+
type: Types.WAIT_ELEMENT,
|
|
1337
|
+
operation: "waitForElement",
|
|
1338
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1339
|
+
};
|
|
1340
|
+
let found = false;
|
|
1341
|
+
try {
|
|
1342
|
+
await _preCommand(state, this);
|
|
1343
|
+
// if (state.options && state.options.context) {
|
|
1344
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1345
|
+
// }
|
|
1346
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1347
|
+
found = true;
|
|
1348
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1349
|
+
}
|
|
1350
|
+
catch (e) {
|
|
1351
|
+
console.error("Error on waitForElement", e);
|
|
1352
|
+
// await _commandError(state, e, this);
|
|
1353
|
+
}
|
|
1354
|
+
finally {
|
|
1355
|
+
await _commandFinally(state, this);
|
|
900
1356
|
}
|
|
1357
|
+
return found;
|
|
901
1358
|
}
|
|
902
1359
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
903
1360
|
const state = {
|
|
@@ -907,6 +1364,7 @@ class StableBrowser {
|
|
|
907
1364
|
world,
|
|
908
1365
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
909
1366
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1367
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
910
1368
|
operation: "setCheck",
|
|
911
1369
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
912
1370
|
};
|
|
@@ -916,30 +1374,53 @@ class StableBrowser {
|
|
|
916
1374
|
// let element = await this._locate(selectors, info, _params);
|
|
917
1375
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
918
1376
|
try {
|
|
919
|
-
//
|
|
920
|
-
|
|
1377
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1378
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1379
|
+
await this._highlightElements(state.element);
|
|
1380
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
921
1381
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1382
|
+
// await this._unHighlightElements(element);
|
|
1383
|
+
// }
|
|
1384
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1385
|
+
// await this._unHighlightElements(element);
|
|
922
1386
|
}
|
|
923
1387
|
catch (e) {
|
|
924
1388
|
if (e.message && e.message.includes("did not change its state")) {
|
|
925
1389
|
this.logger.info("element did not change its state, ignoring...");
|
|
926
1390
|
}
|
|
927
1391
|
else {
|
|
1392
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
928
1393
|
//await this.closeUnexpectedPopups();
|
|
929
1394
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
930
|
-
state.
|
|
931
|
-
|
|
932
|
-
|
|
1395
|
+
state.element_found = false;
|
|
1396
|
+
try {
|
|
1397
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1398
|
+
state.element_found = true;
|
|
1399
|
+
// check the check state
|
|
1400
|
+
}
|
|
1401
|
+
catch (error) {
|
|
1402
|
+
// element dismissed
|
|
1403
|
+
}
|
|
1404
|
+
if (state.element_found) {
|
|
1405
|
+
const isChecked = await state.element.isChecked();
|
|
1406
|
+
if (isChecked !== checked) {
|
|
1407
|
+
// perform click
|
|
1408
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
933
1414
|
}
|
|
934
1415
|
}
|
|
935
|
-
await this.waitForPageLoad();
|
|
1416
|
+
//await this.waitForPageLoad();
|
|
936
1417
|
return state.info;
|
|
937
1418
|
}
|
|
938
1419
|
catch (e) {
|
|
939
1420
|
await _commandError(state, e, this);
|
|
940
1421
|
}
|
|
941
1422
|
finally {
|
|
942
|
-
_commandFinally(state, this);
|
|
1423
|
+
await _commandFinally(state, this);
|
|
943
1424
|
}
|
|
944
1425
|
}
|
|
945
1426
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -950,31 +1431,22 @@ class StableBrowser {
|
|
|
950
1431
|
world,
|
|
951
1432
|
type: Types.HOVER,
|
|
952
1433
|
text: `Hover element`,
|
|
1434
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
953
1435
|
operation: "hover",
|
|
954
1436
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
955
1437
|
};
|
|
956
1438
|
try {
|
|
957
1439
|
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
|
-
}
|
|
1440
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
969
1441
|
await _screenshot(state, this);
|
|
970
|
-
await this.waitForPageLoad();
|
|
1442
|
+
//await this.waitForPageLoad();
|
|
971
1443
|
return state.info;
|
|
972
1444
|
}
|
|
973
1445
|
catch (e) {
|
|
974
1446
|
await _commandError(state, e, this);
|
|
975
1447
|
}
|
|
976
1448
|
finally {
|
|
977
|
-
_commandFinally(state, this);
|
|
1449
|
+
await _commandFinally(state, this);
|
|
978
1450
|
}
|
|
979
1451
|
}
|
|
980
1452
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -989,6 +1461,7 @@ class StableBrowser {
|
|
|
989
1461
|
value: values.toString(),
|
|
990
1462
|
type: Types.SELECT,
|
|
991
1463
|
text: `Select option: ${values}`,
|
|
1464
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
992
1465
|
operation: "selectOption",
|
|
993
1466
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
994
1467
|
};
|
|
@@ -1002,14 +1475,14 @@ class StableBrowser {
|
|
|
1002
1475
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1003
1476
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1004
1477
|
}
|
|
1005
|
-
await this.waitForPageLoad();
|
|
1478
|
+
//await this.waitForPageLoad();
|
|
1006
1479
|
return state.info;
|
|
1007
1480
|
}
|
|
1008
1481
|
catch (e) {
|
|
1009
1482
|
await _commandError(state, e, this);
|
|
1010
1483
|
}
|
|
1011
1484
|
finally {
|
|
1012
|
-
_commandFinally(state, this);
|
|
1485
|
+
await _commandFinally(state, this);
|
|
1013
1486
|
}
|
|
1014
1487
|
}
|
|
1015
1488
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1023,6 +1496,7 @@ class StableBrowser {
|
|
|
1023
1496
|
highlight: false,
|
|
1024
1497
|
type: Types.TYPE_PRESS,
|
|
1025
1498
|
text: `Type value: ${_value}`,
|
|
1499
|
+
_text: `Type value: ${_value}`,
|
|
1026
1500
|
operation: "type",
|
|
1027
1501
|
log: "",
|
|
1028
1502
|
};
|
|
@@ -1054,7 +1528,7 @@ class StableBrowser {
|
|
|
1054
1528
|
await _commandError(state, e, this);
|
|
1055
1529
|
}
|
|
1056
1530
|
finally {
|
|
1057
|
-
_commandFinally(state, this);
|
|
1531
|
+
await _commandFinally(state, this);
|
|
1058
1532
|
}
|
|
1059
1533
|
}
|
|
1060
1534
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1090,37 +1564,35 @@ class StableBrowser {
|
|
|
1090
1564
|
await _commandError(state, e, this);
|
|
1091
1565
|
}
|
|
1092
1566
|
finally {
|
|
1093
|
-
_commandFinally(state, this);
|
|
1567
|
+
await _commandFinally(state, this);
|
|
1094
1568
|
}
|
|
1095
1569
|
}
|
|
1096
1570
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1571
|
+
const state = {
|
|
1572
|
+
selectors,
|
|
1573
|
+
_params,
|
|
1574
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1575
|
+
options,
|
|
1576
|
+
world,
|
|
1577
|
+
type: Types.SET_DATE_TIME,
|
|
1578
|
+
text: `Set date time value: ${value}`,
|
|
1579
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1580
|
+
operation: "setDateTime",
|
|
1581
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1582
|
+
throwError: false,
|
|
1583
|
+
};
|
|
1107
1584
|
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);
|
|
1585
|
+
await _preCommand(state, this);
|
|
1114
1586
|
try {
|
|
1115
|
-
await element
|
|
1587
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1116
1588
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1117
1589
|
if (format) {
|
|
1118
|
-
value = dayjs(value).format(format);
|
|
1119
|
-
await element.fill(value);
|
|
1590
|
+
state.value = dayjs(state.value).format(format);
|
|
1591
|
+
await state.element.fill(state.value);
|
|
1120
1592
|
}
|
|
1121
1593
|
else {
|
|
1122
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1123
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1594
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1595
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1124
1596
|
el.value = ""; // clear input
|
|
1125
1597
|
el.value = dateTimeValue;
|
|
1126
1598
|
}, dateTimeValue);
|
|
@@ -1133,20 +1605,19 @@ class StableBrowser {
|
|
|
1133
1605
|
}
|
|
1134
1606
|
catch (err) {
|
|
1135
1607
|
//await this.closeUnexpectedPopups();
|
|
1136
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1608
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1137
1609
|
this.logger.info("Trying again");
|
|
1138
|
-
|
|
1139
|
-
info.
|
|
1140
|
-
Object.assign(err, { info: info });
|
|
1610
|
+
await _screenshot(state, this);
|
|
1611
|
+
Object.assign(err, { info: state.info });
|
|
1141
1612
|
await element.click();
|
|
1142
1613
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1143
1614
|
if (format) {
|
|
1144
|
-
value = dayjs(value).format(format);
|
|
1145
|
-
await element.fill(value);
|
|
1615
|
+
state.value = dayjs(state.value).format(format);
|
|
1616
|
+
await state.element.fill(state.value);
|
|
1146
1617
|
}
|
|
1147
1618
|
else {
|
|
1148
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1149
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1619
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1620
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1150
1621
|
el.value = ""; // clear input
|
|
1151
1622
|
el.value = dateTimeValue;
|
|
1152
1623
|
}, dateTimeValue);
|
|
@@ -1159,72 +1630,60 @@ class StableBrowser {
|
|
|
1159
1630
|
}
|
|
1160
1631
|
}
|
|
1161
1632
|
catch (e) {
|
|
1162
|
-
|
|
1163
|
-
throw e;
|
|
1633
|
+
await _commandError(state, e, this);
|
|
1164
1634
|
}
|
|
1165
1635
|
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
|
-
});
|
|
1636
|
+
await _commandFinally(state, this);
|
|
1187
1637
|
}
|
|
1188
1638
|
}
|
|
1189
1639
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1640
|
+
_value = unEscapeString(_value);
|
|
1641
|
+
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1190
1642
|
const state = {
|
|
1191
1643
|
selectors,
|
|
1192
1644
|
_params,
|
|
1193
|
-
value:
|
|
1645
|
+
value: newValue,
|
|
1646
|
+
originalValue: _value,
|
|
1194
1647
|
options,
|
|
1195
1648
|
world,
|
|
1196
1649
|
type: Types.FILL,
|
|
1197
1650
|
text: `Click type input with value: ${_value}`,
|
|
1651
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1198
1652
|
operation: "clickType",
|
|
1199
|
-
log: "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n",
|
|
1653
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1200
1654
|
};
|
|
1201
|
-
|
|
1655
|
+
if (!options) {
|
|
1656
|
+
options = {};
|
|
1657
|
+
}
|
|
1202
1658
|
if (newValue !== _value) {
|
|
1203
1659
|
//this.logger.info(_value + "=" + newValue);
|
|
1204
1660
|
_value = newValue;
|
|
1205
|
-
state.value = newValue;
|
|
1206
1661
|
}
|
|
1207
1662
|
try {
|
|
1208
1663
|
await _preCommand(state, this);
|
|
1664
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1665
|
+
// tag the element
|
|
1666
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1667
|
+
// use attribute and not id
|
|
1668
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1669
|
+
el.setAttribute(attrName, "");
|
|
1670
|
+
return `[${attrName}]`;
|
|
1671
|
+
}, randomToken);
|
|
1209
1672
|
state.info.value = _value;
|
|
1210
|
-
if (
|
|
1673
|
+
if (!options.press) {
|
|
1211
1674
|
try {
|
|
1212
1675
|
let currentValue = await state.element.inputValue();
|
|
1213
1676
|
if (currentValue) {
|
|
1214
|
-
await element.fill("");
|
|
1677
|
+
await state.element.fill("");
|
|
1215
1678
|
}
|
|
1216
1679
|
}
|
|
1217
1680
|
catch (e) {
|
|
1218
1681
|
this.logger.info("unable to clear input value");
|
|
1219
1682
|
}
|
|
1220
1683
|
}
|
|
1221
|
-
if (options
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
catch (e) {
|
|
1226
|
-
await state.element.dispatchEvent("click");
|
|
1227
|
-
}
|
|
1684
|
+
if (options.press) {
|
|
1685
|
+
options.timeout = 5000;
|
|
1686
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1228
1687
|
}
|
|
1229
1688
|
else {
|
|
1230
1689
|
try {
|
|
@@ -1235,6 +1694,25 @@ class StableBrowser {
|
|
|
1235
1694
|
}
|
|
1236
1695
|
}
|
|
1237
1696
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1697
|
+
// check if the element exist after the click (no wait)
|
|
1698
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1699
|
+
if (count === 0) {
|
|
1700
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1701
|
+
const scope = state.element._frame ?? element.page();
|
|
1702
|
+
let prefixSelector = "";
|
|
1703
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1704
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1705
|
+
if (frameSelectorIndex !== -1) {
|
|
1706
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1707
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1708
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1709
|
+
}
|
|
1710
|
+
// if (element?._frame?._selector) {
|
|
1711
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1712
|
+
// }
|
|
1713
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1714
|
+
state.element = scope.locator(newSelector).first();
|
|
1715
|
+
}
|
|
1238
1716
|
const valueSegment = state.value.split("&&");
|
|
1239
1717
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1240
1718
|
if (i > 0) {
|
|
@@ -1255,14 +1733,21 @@ class StableBrowser {
|
|
|
1255
1733
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1256
1734
|
}
|
|
1257
1735
|
}
|
|
1736
|
+
//if (!this.fastMode) {
|
|
1258
1737
|
await _screenshot(state, this);
|
|
1738
|
+
//}
|
|
1259
1739
|
if (enter === true) {
|
|
1260
1740
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1261
1741
|
await this.page.keyboard.press("Enter");
|
|
1262
1742
|
await this.waitForPageLoad();
|
|
1263
1743
|
}
|
|
1264
1744
|
else if (enter === false) {
|
|
1265
|
-
|
|
1745
|
+
try {
|
|
1746
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1747
|
+
}
|
|
1748
|
+
catch (e) {
|
|
1749
|
+
// ignore
|
|
1750
|
+
}
|
|
1266
1751
|
//await this.page.keyboard.press("Tab");
|
|
1267
1752
|
}
|
|
1268
1753
|
else {
|
|
@@ -1277,7 +1762,7 @@ class StableBrowser {
|
|
|
1277
1762
|
await _commandError(state, e, this);
|
|
1278
1763
|
}
|
|
1279
1764
|
finally {
|
|
1280
|
-
_commandFinally(state, this);
|
|
1765
|
+
await _commandFinally(state, this);
|
|
1281
1766
|
}
|
|
1282
1767
|
}
|
|
1283
1768
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1299,30 +1784,67 @@ class StableBrowser {
|
|
|
1299
1784
|
if (enter) {
|
|
1300
1785
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1301
1786
|
await this.page.keyboard.press("Enter");
|
|
1787
|
+
await this.waitForPageLoad();
|
|
1788
|
+
}
|
|
1789
|
+
return state.info;
|
|
1790
|
+
}
|
|
1791
|
+
catch (e) {
|
|
1792
|
+
await _commandError(state, e, this);
|
|
1793
|
+
}
|
|
1794
|
+
finally {
|
|
1795
|
+
await _commandFinally(state, this);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1799
|
+
const state = {
|
|
1800
|
+
selectors,
|
|
1801
|
+
_params,
|
|
1802
|
+
files,
|
|
1803
|
+
value: '"' + files.join('", "') + '"',
|
|
1804
|
+
options,
|
|
1805
|
+
world,
|
|
1806
|
+
type: Types.SET_INPUT_FILES,
|
|
1807
|
+
text: `Set input files`,
|
|
1808
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1809
|
+
operation: "setInputFiles",
|
|
1810
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1811
|
+
};
|
|
1812
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1813
|
+
try {
|
|
1814
|
+
await _preCommand(state, this);
|
|
1815
|
+
for (let i = 0; i < files.length; i++) {
|
|
1816
|
+
const file = files[i];
|
|
1817
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1818
|
+
if (!fs.existsSync(filePath)) {
|
|
1819
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1820
|
+
}
|
|
1821
|
+
state.files[i] = filePath;
|
|
1302
1822
|
}
|
|
1303
|
-
await
|
|
1823
|
+
await state.element.setInputFiles(files);
|
|
1304
1824
|
return state.info;
|
|
1305
1825
|
}
|
|
1306
1826
|
catch (e) {
|
|
1307
1827
|
await _commandError(state, e, this);
|
|
1308
1828
|
}
|
|
1309
1829
|
finally {
|
|
1310
|
-
_commandFinally(state, this);
|
|
1830
|
+
await _commandFinally(state, this);
|
|
1311
1831
|
}
|
|
1312
1832
|
}
|
|
1313
1833
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1314
1834
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1315
1835
|
}
|
|
1316
1836
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1837
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1317
1838
|
_validateSelectors(selectors);
|
|
1318
1839
|
let screenshotId = null;
|
|
1319
1840
|
let screenshotPath = null;
|
|
1320
1841
|
if (!info.log) {
|
|
1321
1842
|
info.log = "";
|
|
1843
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1322
1844
|
}
|
|
1323
1845
|
info.operation = "getText";
|
|
1324
1846
|
info.selectors = selectors;
|
|
1325
|
-
let element = await this._locate(selectors, info, _params);
|
|
1847
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1326
1848
|
if (climb > 0) {
|
|
1327
1849
|
const climbArray = [];
|
|
1328
1850
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1341,6 +1863,18 @@ class StableBrowser {
|
|
|
1341
1863
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1342
1864
|
try {
|
|
1343
1865
|
await this._highlightElements(element);
|
|
1866
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1867
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1868
|
+
// this._highlightElements(element)
|
|
1869
|
+
// .then(async () => {
|
|
1870
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1871
|
+
// this._unhighlightElements(element).then(
|
|
1872
|
+
// () => {}
|
|
1873
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1874
|
+
// );
|
|
1875
|
+
// })
|
|
1876
|
+
// .catch(e);
|
|
1877
|
+
// }
|
|
1344
1878
|
const elementText = await element.innerText();
|
|
1345
1879
|
return {
|
|
1346
1880
|
text: elementText,
|
|
@@ -1352,7 +1886,7 @@ class StableBrowser {
|
|
|
1352
1886
|
}
|
|
1353
1887
|
catch (e) {
|
|
1354
1888
|
//await this.closeUnexpectedPopups();
|
|
1355
|
-
this.logger.info("no innerText will use textContent");
|
|
1889
|
+
this.logger.info("no innerText, will use textContent");
|
|
1356
1890
|
const elementText = await element.textContent();
|
|
1357
1891
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1358
1892
|
}
|
|
@@ -1377,6 +1911,7 @@ class StableBrowser {
|
|
|
1377
1911
|
highlight: false,
|
|
1378
1912
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1379
1913
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1914
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1380
1915
|
operation: "containsPattern",
|
|
1381
1916
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1382
1917
|
};
|
|
@@ -1408,10 +1943,12 @@ class StableBrowser {
|
|
|
1408
1943
|
await _commandError(state, e, this);
|
|
1409
1944
|
}
|
|
1410
1945
|
finally {
|
|
1411
|
-
_commandFinally(state, this);
|
|
1946
|
+
await _commandFinally(state, this);
|
|
1412
1947
|
}
|
|
1413
1948
|
}
|
|
1414
1949
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1950
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1951
|
+
const startTime = Date.now();
|
|
1415
1952
|
const state = {
|
|
1416
1953
|
selectors,
|
|
1417
1954
|
_params,
|
|
@@ -1438,61 +1975,130 @@ class StableBrowser {
|
|
|
1438
1975
|
}
|
|
1439
1976
|
let foundObj = null;
|
|
1440
1977
|
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;
|
|
1978
|
+
while (Date.now() - startTime < timeout) {
|
|
1979
|
+
try {
|
|
1980
|
+
await _preCommand(state, this);
|
|
1981
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1982
|
+
if (foundObj && foundObj.element) {
|
|
1983
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1454
1984
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1985
|
+
await _screenshot(state, this);
|
|
1986
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1987
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1988
|
+
if (dateAlternatives.date) {
|
|
1989
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1990
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1991
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1992
|
+
return state.info;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
else if (numberAlternatives.number) {
|
|
1997
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1998
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1999
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
2000
|
+
return state.info;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1462
2005
|
return state.info;
|
|
1463
2006
|
}
|
|
1464
2007
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
throw new Error("element doesn't contain text " + text);
|
|
2008
|
+
catch (e) {
|
|
2009
|
+
// Log error but continue retrying until timeout is reached
|
|
2010
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
2011
|
+
}
|
|
2012
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1471
2013
|
}
|
|
1472
|
-
|
|
2014
|
+
state.info.foundText = foundObj?.text;
|
|
2015
|
+
state.info.value = foundObj?.value;
|
|
2016
|
+
throw new Error("element doesn't contain text " + text);
|
|
1473
2017
|
}
|
|
1474
2018
|
catch (e) {
|
|
1475
2019
|
await _commandError(state, e, this);
|
|
2020
|
+
throw e;
|
|
1476
2021
|
}
|
|
1477
2022
|
finally {
|
|
1478
|
-
_commandFinally(state, this);
|
|
2023
|
+
await _commandFinally(state, this);
|
|
1479
2024
|
}
|
|
1480
2025
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
2026
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
2027
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2028
|
+
const startTime = Date.now();
|
|
2029
|
+
const state = {
|
|
2030
|
+
_params,
|
|
2031
|
+
value: referanceSnapshot,
|
|
2032
|
+
options,
|
|
2033
|
+
world,
|
|
2034
|
+
locate: false,
|
|
2035
|
+
scroll: false,
|
|
2036
|
+
screenshot: true,
|
|
2037
|
+
highlight: false,
|
|
2038
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
2039
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
2040
|
+
operation: "snapshotValidation",
|
|
2041
|
+
log: "***** verify snapshot *****\n",
|
|
2042
|
+
};
|
|
2043
|
+
if (!referanceSnapshot) {
|
|
2044
|
+
throw new Error("referanceSnapshot is null");
|
|
2045
|
+
}
|
|
2046
|
+
let text = null;
|
|
2047
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
2048
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1485
2049
|
}
|
|
1486
|
-
else if (this.
|
|
1487
|
-
|
|
2050
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
2051
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1488
2052
|
}
|
|
1489
|
-
else if (
|
|
1490
|
-
|
|
2053
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2054
|
+
text = referanceSnapshot.substring(5);
|
|
1491
2055
|
}
|
|
1492
2056
|
else {
|
|
1493
|
-
|
|
2057
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
2058
|
+
}
|
|
2059
|
+
state.text = text;
|
|
2060
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2061
|
+
await _preCommand(state, this);
|
|
2062
|
+
let foundObj = null;
|
|
2063
|
+
try {
|
|
2064
|
+
let matchResult = null;
|
|
2065
|
+
while (Date.now() - startTime < timeout) {
|
|
2066
|
+
try {
|
|
2067
|
+
let scope = null;
|
|
2068
|
+
if (!frameSelectors) {
|
|
2069
|
+
scope = this.page;
|
|
2070
|
+
}
|
|
2071
|
+
else {
|
|
2072
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
2073
|
+
}
|
|
2074
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
2075
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
2076
|
+
if (matchResult.errorLine !== -1) {
|
|
2077
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
2078
|
+
}
|
|
2079
|
+
// highlight and screenshot
|
|
2080
|
+
try {
|
|
2081
|
+
await await highlightSnapshot(newValue, scope);
|
|
2082
|
+
await _screenshot(state, this);
|
|
2083
|
+
}
|
|
2084
|
+
catch (e) { }
|
|
2085
|
+
return state.info;
|
|
2086
|
+
}
|
|
2087
|
+
catch (e) {
|
|
2088
|
+
// Log error but continue retrying until timeout is reached
|
|
2089
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
2090
|
+
}
|
|
2091
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
2092
|
+
}
|
|
2093
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
2094
|
+
}
|
|
2095
|
+
catch (e) {
|
|
2096
|
+
await _commandError(state, e, this);
|
|
2097
|
+
throw e;
|
|
2098
|
+
}
|
|
2099
|
+
finally {
|
|
2100
|
+
await _commandFinally(state, this);
|
|
1494
2101
|
}
|
|
1495
|
-
return dataFile;
|
|
1496
2102
|
}
|
|
1497
2103
|
async waitForUserInput(message, world = null) {
|
|
1498
2104
|
if (!message) {
|
|
@@ -1522,13 +2128,22 @@ class StableBrowser {
|
|
|
1522
2128
|
return;
|
|
1523
2129
|
}
|
|
1524
2130
|
// if data file exists, load it
|
|
1525
|
-
const dataFile =
|
|
2131
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1526
2132
|
let data = this.getTestData(world);
|
|
1527
2133
|
// merge the testData with the existing data
|
|
1528
2134
|
Object.assign(data, testData);
|
|
1529
2135
|
// save the data to the file
|
|
1530
2136
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1531
2137
|
}
|
|
2138
|
+
overwriteTestData(testData, world = null) {
|
|
2139
|
+
if (!testData) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
// if data file exists, load it
|
|
2143
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2144
|
+
// save the data to the file
|
|
2145
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
2146
|
+
}
|
|
1532
2147
|
_getDataFilePath(fileName) {
|
|
1533
2148
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1534
2149
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1625,12 +2240,7 @@ class StableBrowser {
|
|
|
1625
2240
|
}
|
|
1626
2241
|
}
|
|
1627
2242
|
getTestData(world = null) {
|
|
1628
|
-
|
|
1629
|
-
let data = {};
|
|
1630
|
-
if (fs.existsSync(dataFile)) {
|
|
1631
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1632
|
-
}
|
|
1633
|
-
return data;
|
|
2243
|
+
return _getTestData(world, this.context, this);
|
|
1634
2244
|
}
|
|
1635
2245
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1636
2246
|
// collect url/path/title
|
|
@@ -1657,11 +2267,9 @@ class StableBrowser {
|
|
|
1657
2267
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1658
2268
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1659
2269
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
2270
|
+
// to make sure the path doesn't start with -
|
|
2271
|
+
const uuidStr = "id_" + randomUUID();
|
|
2272
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1665
2273
|
try {
|
|
1666
2274
|
await this.takeScreenshot(screenshotPath);
|
|
1667
2275
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1671,15 +2279,15 @@ class StableBrowser {
|
|
|
1671
2279
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1672
2280
|
// }
|
|
1673
2281
|
// });
|
|
2282
|
+
result.screenshotId = uuidStr;
|
|
2283
|
+
result.screenshotPath = screenshotPath;
|
|
2284
|
+
if (info && info.box) {
|
|
2285
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
2286
|
+
}
|
|
1674
2287
|
}
|
|
1675
2288
|
catch (e) {
|
|
1676
2289
|
this.logger.info("unable to take screenshot, ignored");
|
|
1677
2290
|
}
|
|
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
2291
|
}
|
|
1684
2292
|
else if (options && options.screenshot) {
|
|
1685
2293
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1714,6 +2322,15 @@ class StableBrowser {
|
|
|
1714
2322
|
document.documentElement.clientWidth,
|
|
1715
2323
|
])));
|
|
1716
2324
|
let screenshotBuffer = null;
|
|
2325
|
+
// if (focusedElement) {
|
|
2326
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2327
|
+
// await this._unhighlightElements(focusedElement);
|
|
2328
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2329
|
+
// console.log(`Unhighlighted previous element`);
|
|
2330
|
+
// }
|
|
2331
|
+
// if (focusedElement) {
|
|
2332
|
+
// await this._highlightElements(focusedElement);
|
|
2333
|
+
// }
|
|
1717
2334
|
if (this.context.browserName === "chromium") {
|
|
1718
2335
|
const client = await playContext.newCDPSession(this.page);
|
|
1719
2336
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
@@ -1735,6 +2352,10 @@ class StableBrowser {
|
|
|
1735
2352
|
else {
|
|
1736
2353
|
screenshotBuffer = await this.page.screenshot();
|
|
1737
2354
|
}
|
|
2355
|
+
// if (focusedElement) {
|
|
2356
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2357
|
+
// await this._unhighlightElements(focusedElement);
|
|
2358
|
+
// }
|
|
1738
2359
|
let image = await Jimp.read(screenshotBuffer);
|
|
1739
2360
|
// Get the image dimensions
|
|
1740
2361
|
const { width, height } = image.bitmap;
|
|
@@ -1747,6 +2368,7 @@ class StableBrowser {
|
|
|
1747
2368
|
else {
|
|
1748
2369
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1749
2370
|
}
|
|
2371
|
+
return screenshotBuffer;
|
|
1750
2372
|
}
|
|
1751
2373
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1752
2374
|
const state = {
|
|
@@ -1769,113 +2391,532 @@ class StableBrowser {
|
|
|
1769
2391
|
await _commandError(state, e, this);
|
|
1770
2392
|
}
|
|
1771
2393
|
finally {
|
|
1772
|
-
_commandFinally(state, this);
|
|
2394
|
+
await _commandFinally(state, this);
|
|
1773
2395
|
}
|
|
1774
2396
|
}
|
|
1775
2397
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2398
|
+
const state = {
|
|
2399
|
+
selectors,
|
|
2400
|
+
_params,
|
|
2401
|
+
attribute,
|
|
2402
|
+
variable,
|
|
2403
|
+
options,
|
|
2404
|
+
world,
|
|
2405
|
+
type: Types.EXTRACT,
|
|
2406
|
+
text: `Extract attribute from element`,
|
|
2407
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
2408
|
+
operation: "extractAttribute",
|
|
2409
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2410
|
+
allowDisabled: true,
|
|
2411
|
+
};
|
|
1781
2412
|
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
2413
|
try {
|
|
1787
|
-
|
|
1788
|
-
await this._highlightElements(element);
|
|
1789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2414
|
+
await _preCommand(state, this);
|
|
1790
2415
|
switch (attribute) {
|
|
1791
2416
|
case "inner_text":
|
|
1792
|
-
|
|
2417
|
+
state.value = await state.element.innerText();
|
|
1793
2418
|
break;
|
|
1794
2419
|
case "href":
|
|
1795
|
-
|
|
2420
|
+
state.value = await state.element.getAttribute("href");
|
|
1796
2421
|
break;
|
|
1797
2422
|
case "value":
|
|
1798
|
-
|
|
2423
|
+
state.value = await state.element.inputValue();
|
|
2424
|
+
break;
|
|
2425
|
+
case "text":
|
|
2426
|
+
state.value = await state.element.textContent();
|
|
1799
2427
|
break;
|
|
1800
2428
|
default:
|
|
1801
|
-
|
|
2429
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1802
2430
|
break;
|
|
1803
2431
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2432
|
+
if (options !== null) {
|
|
2433
|
+
if (options.regex && options.regex !== "") {
|
|
2434
|
+
// Construct a regex pattern from the provided string
|
|
2435
|
+
const regex = options.regex.slice(1, -1);
|
|
2436
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2437
|
+
const matches = state.value.match(regexPattern);
|
|
2438
|
+
if (matches) {
|
|
2439
|
+
let newValue = "";
|
|
2440
|
+
for (const match of matches) {
|
|
2441
|
+
newValue += match;
|
|
2442
|
+
}
|
|
2443
|
+
state.value = newValue;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2447
|
+
state.value = state.value.trim();
|
|
2448
|
+
}
|
|
1807
2449
|
}
|
|
1808
|
-
|
|
1809
|
-
this.
|
|
1810
|
-
|
|
2450
|
+
state.info.value = state.value;
|
|
2451
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2452
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2453
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2454
|
+
return state.info;
|
|
1811
2455
|
}
|
|
1812
2456
|
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;
|
|
2457
|
+
await _commandError(state, e, this);
|
|
1820
2458
|
}
|
|
1821
2459
|
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
|
-
});
|
|
2460
|
+
await _commandFinally(state, this);
|
|
1844
2461
|
}
|
|
1845
2462
|
}
|
|
1846
|
-
async
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2463
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2464
|
+
const state = {
|
|
2465
|
+
selectors,
|
|
2466
|
+
_params,
|
|
2467
|
+
property,
|
|
2468
|
+
variable,
|
|
2469
|
+
options,
|
|
2470
|
+
world,
|
|
2471
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2472
|
+
text: `Extract property from element`,
|
|
2473
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2474
|
+
operation: "extractProperty",
|
|
2475
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2476
|
+
allowDisabled: true,
|
|
2477
|
+
};
|
|
2478
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2479
|
+
try {
|
|
2480
|
+
await _preCommand(state, this);
|
|
2481
|
+
switch (property) {
|
|
2482
|
+
case "inner_text":
|
|
2483
|
+
state.value = await state.element.innerText();
|
|
2484
|
+
break;
|
|
2485
|
+
case "href":
|
|
2486
|
+
state.value = await state.element.getAttribute("href");
|
|
2487
|
+
break;
|
|
2488
|
+
case "value":
|
|
2489
|
+
state.value = await state.element.inputValue();
|
|
2490
|
+
break;
|
|
2491
|
+
case "text":
|
|
2492
|
+
state.value = await state.element.textContent();
|
|
2493
|
+
break;
|
|
2494
|
+
default:
|
|
2495
|
+
if (property.startsWith("dataset.")) {
|
|
2496
|
+
const dataAttribute = property.substring(8);
|
|
2497
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2498
|
+
}
|
|
2499
|
+
else {
|
|
2500
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (options !== null) {
|
|
2504
|
+
if (options.regex && options.regex !== "") {
|
|
2505
|
+
// Construct a regex pattern from the provided string
|
|
2506
|
+
const regex = options.regex.slice(1, -1);
|
|
2507
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2508
|
+
const matches = state.value.match(regexPattern);
|
|
2509
|
+
if (matches) {
|
|
2510
|
+
let newValue = "";
|
|
2511
|
+
for (const match of matches) {
|
|
2512
|
+
newValue += match;
|
|
2513
|
+
}
|
|
2514
|
+
state.value = newValue;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2518
|
+
state.value = state.value.trim();
|
|
2519
|
+
}
|
|
1857
2520
|
}
|
|
2521
|
+
state.info.value = state.value;
|
|
2522
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2523
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2524
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2525
|
+
return state.info;
|
|
1858
2526
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
if (options && options.timeout) {
|
|
1862
|
-
timeout = options.timeout;
|
|
2527
|
+
catch (e) {
|
|
2528
|
+
await _commandError(state, e, this);
|
|
1863
2529
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2530
|
+
finally {
|
|
2531
|
+
await _commandFinally(state, this);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2535
|
+
const state = {
|
|
2536
|
+
selectors,
|
|
2537
|
+
_params,
|
|
2538
|
+
attribute,
|
|
2539
|
+
value,
|
|
2540
|
+
options,
|
|
2541
|
+
world,
|
|
2542
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2543
|
+
highlight: true,
|
|
2544
|
+
screenshot: true,
|
|
2545
|
+
text: `Verify element attribute`,
|
|
2546
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2547
|
+
operation: "verifyAttribute",
|
|
2548
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2549
|
+
allowDisabled: true,
|
|
1875
2550
|
};
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2551
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2552
|
+
let val;
|
|
2553
|
+
let expectedValue;
|
|
2554
|
+
try {
|
|
2555
|
+
await _preCommand(state, this);
|
|
2556
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2557
|
+
state.info.expectedValue = expectedValue;
|
|
2558
|
+
switch (attribute) {
|
|
2559
|
+
case "innerText":
|
|
2560
|
+
val = String(await state.element.innerText());
|
|
2561
|
+
break;
|
|
2562
|
+
case "text":
|
|
2563
|
+
val = String(await state.element.textContent());
|
|
2564
|
+
break;
|
|
2565
|
+
case "value":
|
|
2566
|
+
val = String(await state.element.inputValue());
|
|
2567
|
+
break;
|
|
2568
|
+
case "checked":
|
|
2569
|
+
val = String(await state.element.isChecked());
|
|
2570
|
+
break;
|
|
2571
|
+
case "disabled":
|
|
2572
|
+
val = String(await state.element.isDisabled());
|
|
2573
|
+
break;
|
|
2574
|
+
case "readOnly":
|
|
2575
|
+
const isEditable = await state.element.isEditable();
|
|
2576
|
+
val = String(!isEditable);
|
|
2577
|
+
break;
|
|
2578
|
+
default:
|
|
2579
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
state.info.value = val;
|
|
2583
|
+
let regex;
|
|
2584
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2585
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2586
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2587
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2588
|
+
state.info.regex = true;
|
|
2589
|
+
}
|
|
2590
|
+
else {
|
|
2591
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2592
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2593
|
+
}
|
|
2594
|
+
if (attribute === "innerText") {
|
|
2595
|
+
if (state.info.regex) {
|
|
2596
|
+
if (!regex.test(val)) {
|
|
2597
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2598
|
+
state.info.failCause.assertionFailed = true;
|
|
2599
|
+
state.info.failCause.lastError = errorMessage;
|
|
2600
|
+
throw new Error(errorMessage);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
else {
|
|
2604
|
+
const valLines = val.split("\n");
|
|
2605
|
+
const expectedLines = expectedValue.split("\n");
|
|
2606
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2607
|
+
if (!isPart) {
|
|
2608
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2609
|
+
state.info.failCause.assertionFailed = true;
|
|
2610
|
+
state.info.failCause.lastError = errorMessage;
|
|
2611
|
+
throw new Error(errorMessage);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
else {
|
|
2616
|
+
if (!val.match(regex)) {
|
|
2617
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2618
|
+
state.info.failCause.assertionFailed = true;
|
|
2619
|
+
state.info.failCause.lastError = errorMessage;
|
|
2620
|
+
throw new Error(errorMessage);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return state.info;
|
|
2624
|
+
}
|
|
2625
|
+
catch (e) {
|
|
2626
|
+
await _commandError(state, e, this);
|
|
2627
|
+
}
|
|
2628
|
+
finally {
|
|
2629
|
+
await _commandFinally(state, this);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2633
|
+
const state = {
|
|
2634
|
+
selectors,
|
|
2635
|
+
_params,
|
|
2636
|
+
property,
|
|
2637
|
+
value,
|
|
2638
|
+
options,
|
|
2639
|
+
world,
|
|
2640
|
+
type: Types.VERIFY_PROPERTY,
|
|
2641
|
+
highlight: true,
|
|
2642
|
+
screenshot: true,
|
|
2643
|
+
text: `Verify element property`,
|
|
2644
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2645
|
+
operation: "verifyProperty",
|
|
2646
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2647
|
+
allowDisabled: true,
|
|
2648
|
+
};
|
|
2649
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2650
|
+
let val;
|
|
2651
|
+
let expectedValue;
|
|
2652
|
+
try {
|
|
2653
|
+
await _preCommand(state, this);
|
|
2654
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2655
|
+
state.info.expectedValue = expectedValue;
|
|
2656
|
+
switch (property) {
|
|
2657
|
+
case "innerText":
|
|
2658
|
+
val = String(await state.element.innerText());
|
|
2659
|
+
break;
|
|
2660
|
+
case "text":
|
|
2661
|
+
val = String(await state.element.textContent());
|
|
2662
|
+
break;
|
|
2663
|
+
case "value":
|
|
2664
|
+
val = String(await state.element.inputValue());
|
|
2665
|
+
break;
|
|
2666
|
+
case "checked":
|
|
2667
|
+
val = String(await state.element.isChecked());
|
|
2668
|
+
break;
|
|
2669
|
+
case "disabled":
|
|
2670
|
+
val = String(await state.element.isDisabled());
|
|
2671
|
+
break;
|
|
2672
|
+
case "readOnly":
|
|
2673
|
+
const isEditable = await state.element.isEditable();
|
|
2674
|
+
val = String(!isEditable);
|
|
2675
|
+
break;
|
|
2676
|
+
case "innerHTML":
|
|
2677
|
+
val = String(await state.element.innerHTML());
|
|
2678
|
+
break;
|
|
2679
|
+
case "outerHTML":
|
|
2680
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2681
|
+
break;
|
|
2682
|
+
default:
|
|
2683
|
+
if (property.startsWith("dataset.")) {
|
|
2684
|
+
const dataAttribute = property.substring(8);
|
|
2685
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2686
|
+
}
|
|
2687
|
+
else {
|
|
2688
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
// Helper function to remove all style="" attributes
|
|
2692
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2693
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2694
|
+
};
|
|
2695
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2696
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2697
|
+
val = removeStyleAttributes(val);
|
|
2698
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2699
|
+
}
|
|
2700
|
+
state.info.value = val;
|
|
2701
|
+
let regex;
|
|
2702
|
+
state.info.value = val;
|
|
2703
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2704
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2705
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2706
|
+
let matchPassed = false;
|
|
2707
|
+
if (isRegex) {
|
|
2708
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2709
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2710
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2711
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2712
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2713
|
+
const regex = new RegExp(patternBody, flags);
|
|
2714
|
+
state.info.regex = true;
|
|
2715
|
+
matchPassed = regex.test(val);
|
|
2716
|
+
}
|
|
2717
|
+
else {
|
|
2718
|
+
// Fallback: treat as literal
|
|
2719
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2720
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2721
|
+
matchPassed = regex.test(val);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
else if (isContains) {
|
|
2725
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2726
|
+
matchPassed = val.includes(containsValue);
|
|
2727
|
+
}
|
|
2728
|
+
else if (isExact) {
|
|
2729
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2730
|
+
matchPassed = val === exactValue;
|
|
2731
|
+
}
|
|
2732
|
+
else if (property === "innerText") {
|
|
2733
|
+
// Default innerText logic
|
|
2734
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2735
|
+
const valLines = val.split("\n");
|
|
2736
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2737
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2738
|
+
}
|
|
2739
|
+
else {
|
|
2740
|
+
// Fallback exact or loose match
|
|
2741
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2742
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2743
|
+
matchPassed = regex.test(val);
|
|
2744
|
+
}
|
|
2745
|
+
if (!matchPassed) {
|
|
2746
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2747
|
+
state.info.failCause.assertionFailed = true;
|
|
2748
|
+
state.info.failCause.lastError = errorMessage;
|
|
2749
|
+
throw new Error(errorMessage);
|
|
2750
|
+
}
|
|
2751
|
+
return state.info;
|
|
2752
|
+
}
|
|
2753
|
+
catch (e) {
|
|
2754
|
+
await _commandError(state, e, this);
|
|
2755
|
+
}
|
|
2756
|
+
finally {
|
|
2757
|
+
await _commandFinally(state, this);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2761
|
+
// Convert timeout from seconds to milliseconds
|
|
2762
|
+
const timeoutMs = timeout * 1000;
|
|
2763
|
+
const state = {
|
|
2764
|
+
selectors,
|
|
2765
|
+
_params,
|
|
2766
|
+
condition,
|
|
2767
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2768
|
+
options,
|
|
2769
|
+
world,
|
|
2770
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2771
|
+
highlight: true,
|
|
2772
|
+
screenshot: true,
|
|
2773
|
+
text: `Conditional wait for element`,
|
|
2774
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2775
|
+
operation: "conditionalWait",
|
|
2776
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2777
|
+
allowDisabled: true,
|
|
2778
|
+
info: {},
|
|
2779
|
+
};
|
|
2780
|
+
state.options ??= { timeout: timeoutMs };
|
|
2781
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2782
|
+
const startTime = Date.now();
|
|
2783
|
+
let conditionMet = false;
|
|
2784
|
+
let currentValue = null;
|
|
2785
|
+
let lastError = null;
|
|
2786
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2787
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2788
|
+
const elapsedTime = Date.now() - startTime;
|
|
2789
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2790
|
+
try {
|
|
2791
|
+
// Try to execute _preCommand (element location)
|
|
2792
|
+
await _preCommand(state, this);
|
|
2793
|
+
// If _preCommand succeeds, start condition checking
|
|
2794
|
+
const checkCondition = async () => {
|
|
2795
|
+
try {
|
|
2796
|
+
switch (condition.toLowerCase()) {
|
|
2797
|
+
case "checked":
|
|
2798
|
+
currentValue = await state.element.isChecked();
|
|
2799
|
+
return currentValue === true;
|
|
2800
|
+
case "unchecked":
|
|
2801
|
+
currentValue = await state.element.isChecked();
|
|
2802
|
+
return currentValue === false;
|
|
2803
|
+
case "visible":
|
|
2804
|
+
currentValue = await state.element.isVisible();
|
|
2805
|
+
return currentValue === true;
|
|
2806
|
+
case "hidden":
|
|
2807
|
+
currentValue = await state.element.isVisible();
|
|
2808
|
+
return currentValue === false;
|
|
2809
|
+
case "enabled":
|
|
2810
|
+
currentValue = await state.element.isDisabled();
|
|
2811
|
+
return currentValue === false;
|
|
2812
|
+
case "disabled":
|
|
2813
|
+
currentValue = await state.element.isDisabled();
|
|
2814
|
+
return currentValue === true;
|
|
2815
|
+
case "editable":
|
|
2816
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2817
|
+
currentValue = await state.element.isContentEditable();
|
|
2818
|
+
return currentValue === true;
|
|
2819
|
+
default:
|
|
2820
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2821
|
+
state.info.success = false;
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
catch (error) {
|
|
2826
|
+
// Don't throw here, just return false to continue retrying
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2830
|
+
// Inner loop for condition checking (once element is located)
|
|
2831
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2832
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2833
|
+
conditionMet = await checkCondition();
|
|
2834
|
+
if (conditionMet) {
|
|
2835
|
+
break;
|
|
2836
|
+
}
|
|
2837
|
+
// Check if we still have time for another attempt
|
|
2838
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2839
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2840
|
+
}
|
|
2841
|
+
else {
|
|
2842
|
+
break;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
// If we got here and condition is met, break out of main loop
|
|
2846
|
+
if (conditionMet) {
|
|
2847
|
+
break;
|
|
2848
|
+
}
|
|
2849
|
+
// If condition not met but no exception, we've timed out
|
|
2850
|
+
break;
|
|
2851
|
+
}
|
|
2852
|
+
catch (e) {
|
|
2853
|
+
lastError = e;
|
|
2854
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2855
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2856
|
+
// Check if we have enough time left to retry
|
|
2857
|
+
if (timeLeft > 100) {
|
|
2858
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2859
|
+
}
|
|
2860
|
+
else {
|
|
2861
|
+
break;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2866
|
+
state.info = {
|
|
2867
|
+
success: conditionMet,
|
|
2868
|
+
conditionMet,
|
|
2869
|
+
actualWaitTime,
|
|
2870
|
+
currentValue,
|
|
2871
|
+
lastError: lastError?.message || null,
|
|
2872
|
+
message: conditionMet
|
|
2873
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2874
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2875
|
+
};
|
|
2876
|
+
if (lastError) {
|
|
2877
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2878
|
+
}
|
|
2879
|
+
try {
|
|
2880
|
+
await _commandFinally(state, this);
|
|
2881
|
+
}
|
|
2882
|
+
catch (finallyError) {
|
|
2883
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2884
|
+
}
|
|
2885
|
+
return state.info;
|
|
2886
|
+
}
|
|
2887
|
+
async extractEmailData(emailAddress, options, world) {
|
|
2888
|
+
if (!emailAddress) {
|
|
2889
|
+
throw new Error("email address is null");
|
|
2890
|
+
}
|
|
2891
|
+
// check if address contain @
|
|
2892
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
2893
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
2894
|
+
}
|
|
2895
|
+
else {
|
|
2896
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
2897
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
const startTime = Date.now();
|
|
2901
|
+
let timeout = 60000;
|
|
2902
|
+
if (options && options.timeout) {
|
|
2903
|
+
timeout = options.timeout;
|
|
2904
|
+
}
|
|
2905
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
2906
|
+
const request = {
|
|
2907
|
+
method: "POST",
|
|
2908
|
+
url: serviceUrl,
|
|
2909
|
+
headers: {
|
|
2910
|
+
"Content-Type": "application/json",
|
|
2911
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2912
|
+
},
|
|
2913
|
+
data: JSON.stringify({
|
|
2914
|
+
email: emailAddress,
|
|
2915
|
+
}),
|
|
2916
|
+
};
|
|
2917
|
+
let errorCount = 0;
|
|
2918
|
+
while (true) {
|
|
2919
|
+
try {
|
|
1879
2920
|
let result = await this.context.api.request(request);
|
|
1880
2921
|
// the response body expected to be the following:
|
|
1881
2922
|
// {
|
|
@@ -1917,7 +2958,8 @@ class StableBrowser {
|
|
|
1917
2958
|
catch (e) {
|
|
1918
2959
|
errorCount++;
|
|
1919
2960
|
if (errorCount > 3) {
|
|
1920
|
-
throw e;
|
|
2961
|
+
// throw e;
|
|
2962
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1921
2963
|
}
|
|
1922
2964
|
// ignore
|
|
1923
2965
|
}
|
|
@@ -1931,27 +2973,32 @@ class StableBrowser {
|
|
|
1931
2973
|
async _highlightElements(scope, css) {
|
|
1932
2974
|
try {
|
|
1933
2975
|
if (!scope) {
|
|
2976
|
+
// console.log(`Scope is not defined`);
|
|
1934
2977
|
return;
|
|
1935
2978
|
}
|
|
1936
2979
|
if (!css) {
|
|
1937
2980
|
scope
|
|
1938
2981
|
.evaluate((node) => {
|
|
1939
2982
|
if (node && node.style) {
|
|
1940
|
-
let
|
|
1941
|
-
|
|
2983
|
+
let originalOutline = node.style.outline;
|
|
2984
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
2985
|
+
// node.__previousOutline = originalOutline;
|
|
2986
|
+
node.style.outline = "2px solid red";
|
|
2987
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
1942
2988
|
if (window) {
|
|
1943
2989
|
window.addEventListener("beforeunload", function (e) {
|
|
1944
|
-
node.style.
|
|
2990
|
+
node.style.outline = originalOutline;
|
|
1945
2991
|
});
|
|
1946
2992
|
}
|
|
1947
2993
|
setTimeout(function () {
|
|
1948
|
-
node.style.
|
|
2994
|
+
node.style.outline = originalOutline;
|
|
1949
2995
|
}, 2000);
|
|
1950
2996
|
}
|
|
1951
2997
|
})
|
|
1952
2998
|
.then(() => { })
|
|
1953
2999
|
.catch((e) => {
|
|
1954
3000
|
// ignore
|
|
3001
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1955
3002
|
});
|
|
1956
3003
|
}
|
|
1957
3004
|
else {
|
|
@@ -1967,17 +3014,18 @@ class StableBrowser {
|
|
|
1967
3014
|
if (!element.style) {
|
|
1968
3015
|
return;
|
|
1969
3016
|
}
|
|
1970
|
-
|
|
3017
|
+
let originalOutline = element.style.outline;
|
|
3018
|
+
element.__previousOutline = originalOutline;
|
|
1971
3019
|
// Set the new border to be red and 2px solid
|
|
1972
|
-
element.style.
|
|
3020
|
+
element.style.outline = "2px solid red";
|
|
1973
3021
|
if (window) {
|
|
1974
3022
|
window.addEventListener("beforeunload", function (e) {
|
|
1975
|
-
element.style.
|
|
3023
|
+
element.style.outline = originalOutline;
|
|
1976
3024
|
});
|
|
1977
3025
|
}
|
|
1978
3026
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1979
3027
|
setTimeout(function () {
|
|
1980
|
-
element.style.
|
|
3028
|
+
element.style.outline = originalOutline;
|
|
1981
3029
|
}, 2000);
|
|
1982
3030
|
}
|
|
1983
3031
|
return;
|
|
@@ -1985,6 +3033,7 @@ class StableBrowser {
|
|
|
1985
3033
|
.then(() => { })
|
|
1986
3034
|
.catch((e) => {
|
|
1987
3035
|
// ignore
|
|
3036
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1988
3037
|
});
|
|
1989
3038
|
}
|
|
1990
3039
|
}
|
|
@@ -1992,8 +3041,49 @@ class StableBrowser {
|
|
|
1992
3041
|
console.debug(error);
|
|
1993
3042
|
}
|
|
1994
3043
|
}
|
|
3044
|
+
_matcher(text) {
|
|
3045
|
+
if (!text) {
|
|
3046
|
+
return { matcher: "contains", queryText: "" };
|
|
3047
|
+
}
|
|
3048
|
+
if (text.length < 2) {
|
|
3049
|
+
return { matcher: "contains", queryText: text };
|
|
3050
|
+
}
|
|
3051
|
+
const split = text.split(":");
|
|
3052
|
+
const matcher = split[0].toLowerCase();
|
|
3053
|
+
const queryText = split.slice(1).join(":").trim();
|
|
3054
|
+
return { matcher, queryText };
|
|
3055
|
+
}
|
|
3056
|
+
_getDomain(url) {
|
|
3057
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
3058
|
+
return "";
|
|
3059
|
+
}
|
|
3060
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
3061
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
3062
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
3063
|
+
}
|
|
3064
|
+
let n = hostnameFragments.length;
|
|
3065
|
+
let fragments = [...hostnameFragments];
|
|
3066
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
3067
|
+
hostnameFragments.pop();
|
|
3068
|
+
n = hostnameFragments.length;
|
|
3069
|
+
}
|
|
3070
|
+
if (n == 0) {
|
|
3071
|
+
if (fragments[0] === "www")
|
|
3072
|
+
fragments = fragments.slice(1);
|
|
3073
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
3074
|
+
}
|
|
3075
|
+
if (hostnameFragments[0] === "www")
|
|
3076
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
3077
|
+
return hostnameFragments.join(".");
|
|
3078
|
+
}
|
|
3079
|
+
/**
|
|
3080
|
+
* Verify the page path matches the given path.
|
|
3081
|
+
* @param {string} pathPart - The path to verify.
|
|
3082
|
+
* @param {object} options - Options for verification.
|
|
3083
|
+
* @param {object} world - The world context.
|
|
3084
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3085
|
+
*/
|
|
1995
3086
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
1996
|
-
const startTime = Date.now();
|
|
1997
3087
|
let error = null;
|
|
1998
3088
|
let screenshotId = null;
|
|
1999
3089
|
let screenshotPath = null;
|
|
@@ -2007,159 +3097,520 @@ class StableBrowser {
|
|
|
2007
3097
|
pathPart = newValue;
|
|
2008
3098
|
}
|
|
2009
3099
|
info.pathPart = pathPart;
|
|
3100
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
3101
|
+
const state = {
|
|
3102
|
+
text_search: queryText,
|
|
3103
|
+
options,
|
|
3104
|
+
world,
|
|
3105
|
+
locate: false,
|
|
3106
|
+
scroll: false,
|
|
3107
|
+
highlight: false,
|
|
3108
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
3109
|
+
text: `Verify the page url is ${queryText}`,
|
|
3110
|
+
_text: `Verify the page url is ${queryText}`,
|
|
3111
|
+
operation: "verifyPagePath",
|
|
3112
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
3113
|
+
};
|
|
2010
3114
|
try {
|
|
3115
|
+
await _preCommand(state, this);
|
|
3116
|
+
state.info.text = queryText;
|
|
2011
3117
|
for (let i = 0; i < 30; i++) {
|
|
2012
3118
|
const url = await this.page.url();
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
3119
|
+
switch (matcher) {
|
|
3120
|
+
case "exact":
|
|
3121
|
+
if (url !== queryText) {
|
|
3122
|
+
if (i === 29) {
|
|
3123
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
3124
|
+
}
|
|
3125
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
break;
|
|
3129
|
+
case "contains":
|
|
3130
|
+
if (!url.includes(queryText)) {
|
|
3131
|
+
if (i === 29) {
|
|
3132
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
3133
|
+
}
|
|
3134
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
break;
|
|
3138
|
+
case "starts-with":
|
|
3139
|
+
{
|
|
3140
|
+
const domain = this._getDomain(url);
|
|
3141
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
3142
|
+
if (i === 29) {
|
|
3143
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
3144
|
+
}
|
|
3145
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
break;
|
|
3150
|
+
case "ends-with":
|
|
3151
|
+
{
|
|
3152
|
+
const urlObj = new URL(url);
|
|
3153
|
+
let route = "/";
|
|
3154
|
+
if (urlObj.pathname !== "/") {
|
|
3155
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
3156
|
+
}
|
|
3157
|
+
else {
|
|
3158
|
+
route = "/";
|
|
3159
|
+
}
|
|
3160
|
+
if (route !== queryText) {
|
|
3161
|
+
if (i === 29) {
|
|
3162
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
3163
|
+
}
|
|
3164
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3165
|
+
continue;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
break;
|
|
3169
|
+
case "regex":
|
|
3170
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3171
|
+
if (!regex.test(url)) {
|
|
3172
|
+
if (i === 29) {
|
|
3173
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
3174
|
+
}
|
|
3175
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
break;
|
|
3179
|
+
default:
|
|
3180
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3181
|
+
if (!url.includes(pathPart)) {
|
|
3182
|
+
if (i === 29) {
|
|
3183
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
3184
|
+
}
|
|
3185
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
await _screenshot(state, this);
|
|
3190
|
+
return state.info;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
catch (e) {
|
|
3194
|
+
state.info.failCause.lastError = e.message;
|
|
3195
|
+
state.info.failCause.assertionFailed = true;
|
|
3196
|
+
await _commandError(state, e, this);
|
|
3197
|
+
}
|
|
3198
|
+
finally {
|
|
3199
|
+
await _commandFinally(state, this);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Verify the page title matches the given title.
|
|
3204
|
+
* @param {string} title - The title to verify.
|
|
3205
|
+
* @param {object} options - Options for verification.
|
|
3206
|
+
* @param {object} world - The world context.
|
|
3207
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3208
|
+
*/
|
|
3209
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
3210
|
+
let error = null;
|
|
3211
|
+
let screenshotId = null;
|
|
3212
|
+
let screenshotPath = null;
|
|
3213
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3214
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
3215
|
+
if (newValue !== title) {
|
|
3216
|
+
this.logger.info(title + "=" + newValue);
|
|
3217
|
+
title = newValue;
|
|
3218
|
+
}
|
|
3219
|
+
const { matcher, queryText } = this._matcher(title);
|
|
3220
|
+
const state = {
|
|
3221
|
+
text_search: queryText,
|
|
3222
|
+
options,
|
|
3223
|
+
world,
|
|
3224
|
+
locate: false,
|
|
3225
|
+
scroll: false,
|
|
3226
|
+
highlight: false,
|
|
3227
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
3228
|
+
text: `Verify the page title is ${queryText}`,
|
|
3229
|
+
_text: `Verify the page title is ${queryText}`,
|
|
3230
|
+
operation: "verifyPageTitle",
|
|
3231
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
3232
|
+
};
|
|
3233
|
+
try {
|
|
3234
|
+
await _preCommand(state, this);
|
|
3235
|
+
state.info.text = queryText;
|
|
3236
|
+
for (let i = 0; i < 30; i++) {
|
|
3237
|
+
const foundTitle = await this.page.title();
|
|
3238
|
+
switch (matcher) {
|
|
3239
|
+
case "exact":
|
|
3240
|
+
if (foundTitle !== queryText) {
|
|
3241
|
+
if (i === 29) {
|
|
3242
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
3243
|
+
}
|
|
3244
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
break;
|
|
3248
|
+
case "contains":
|
|
3249
|
+
if (!foundTitle.includes(queryText)) {
|
|
3250
|
+
if (i === 29) {
|
|
3251
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
3252
|
+
}
|
|
3253
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
break;
|
|
3257
|
+
case "starts-with":
|
|
3258
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
3259
|
+
if (i === 29) {
|
|
3260
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
3261
|
+
}
|
|
3262
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3263
|
+
continue;
|
|
3264
|
+
}
|
|
3265
|
+
break;
|
|
3266
|
+
case "ends-with":
|
|
3267
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
3268
|
+
if (i === 29) {
|
|
3269
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
3270
|
+
}
|
|
3271
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
break;
|
|
3275
|
+
case "regex":
|
|
3276
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3277
|
+
if (!regex.test(foundTitle)) {
|
|
3278
|
+
if (i === 29) {
|
|
3279
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
3280
|
+
}
|
|
3281
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
break;
|
|
3285
|
+
default:
|
|
3286
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3287
|
+
if (!foundTitle.includes(title)) {
|
|
3288
|
+
if (i === 29) {
|
|
3289
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
3290
|
+
}
|
|
3291
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
await _screenshot(state, this);
|
|
3296
|
+
return state.info;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
catch (e) {
|
|
3300
|
+
state.info.failCause.lastError = e.message;
|
|
3301
|
+
state.info.failCause.assertionFailed = true;
|
|
3302
|
+
await _commandError(state, e, this);
|
|
3303
|
+
}
|
|
3304
|
+
finally {
|
|
3305
|
+
await _commandFinally(state, this);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
3309
|
+
const frames = this.page.frames();
|
|
3310
|
+
let results = [];
|
|
3311
|
+
// let ignoreCase = false;
|
|
3312
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3313
|
+
if (dateAlternatives.date) {
|
|
3314
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
3315
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3316
|
+
result.frame = frames[i];
|
|
3317
|
+
results.push(result);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
else if (numberAlternatives.number) {
|
|
3321
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
3322
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3323
|
+
result.frame = frames[i];
|
|
3324
|
+
results.push(result);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
else {
|
|
3328
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3329
|
+
result.frame = frames[i];
|
|
3330
|
+
results.push(result);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
state.info.results = results;
|
|
3334
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3335
|
+
return resultWithElementsFound;
|
|
3336
|
+
}
|
|
3337
|
+
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
3338
|
+
text = unEscapeString(text);
|
|
3339
|
+
const state = {
|
|
3340
|
+
text_search: text,
|
|
3341
|
+
options,
|
|
3342
|
+
world,
|
|
3343
|
+
locate: false,
|
|
3344
|
+
scroll: false,
|
|
3345
|
+
highlight: false,
|
|
3346
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
3347
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
3348
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
3349
|
+
operation: "verifyTextExistInPage",
|
|
3350
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
3351
|
+
};
|
|
3352
|
+
if (testForRegex(text)) {
|
|
3353
|
+
text = text.replace(/\\"/g, '"');
|
|
3354
|
+
}
|
|
3355
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3356
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3357
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
3358
|
+
if (newValue !== text) {
|
|
3359
|
+
this.logger.info(text + "=" + newValue);
|
|
3360
|
+
text = newValue;
|
|
3361
|
+
}
|
|
3362
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
3363
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
3364
|
+
try {
|
|
3365
|
+
await _preCommand(state, this);
|
|
3366
|
+
state.info.text = text;
|
|
3367
|
+
while (true) {
|
|
3368
|
+
let resultWithElementsFound = {
|
|
3369
|
+
length: 0,
|
|
3370
|
+
};
|
|
3371
|
+
try {
|
|
3372
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3373
|
+
}
|
|
3374
|
+
catch (error) {
|
|
3375
|
+
// ignore
|
|
3376
|
+
}
|
|
3377
|
+
if (resultWithElementsFound.length === 0) {
|
|
3378
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3379
|
+
throw new Error(`Text ${text} not found in page`);
|
|
2016
3380
|
}
|
|
2017
3381
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2018
3382
|
continue;
|
|
2019
3383
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
3384
|
+
try {
|
|
3385
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
3386
|
+
const frame = resultWithElementsFound[0].frame;
|
|
3387
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
3388
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3389
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3390
|
+
if (element) {
|
|
3391
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3392
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
await _screenshot(state, this);
|
|
3396
|
+
return state.info;
|
|
3397
|
+
}
|
|
3398
|
+
catch (error) {
|
|
3399
|
+
console.error(error);
|
|
3400
|
+
}
|
|
2022
3401
|
}
|
|
2023
3402
|
}
|
|
2024
3403
|
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;
|
|
3404
|
+
await _commandError(state, e, this);
|
|
2032
3405
|
}
|
|
2033
3406
|
finally {
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
3407
|
+
await _commandFinally(state, this);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
3411
|
+
text = unEscapeString(text);
|
|
3412
|
+
const state = {
|
|
3413
|
+
text_search: text,
|
|
3414
|
+
options,
|
|
3415
|
+
world,
|
|
3416
|
+
locate: false,
|
|
3417
|
+
scroll: false,
|
|
3418
|
+
highlight: false,
|
|
3419
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
3420
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
3421
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
3422
|
+
operation: "verifyTextNotExistInPage",
|
|
3423
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
3424
|
+
};
|
|
3425
|
+
if (testForRegex(text)) {
|
|
3426
|
+
text = text.replace(/\\"/g, '"');
|
|
3427
|
+
}
|
|
3428
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3429
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3430
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
3431
|
+
if (newValue !== text) {
|
|
3432
|
+
this.logger.info(text + "=" + newValue);
|
|
3433
|
+
text = newValue;
|
|
3434
|
+
}
|
|
3435
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
3436
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
3437
|
+
try {
|
|
3438
|
+
await _preCommand(state, this);
|
|
3439
|
+
state.info.text = text;
|
|
3440
|
+
let resultWithElementsFound = {
|
|
3441
|
+
length: null, // initial cannot be 0
|
|
3442
|
+
};
|
|
3443
|
+
while (true) {
|
|
3444
|
+
try {
|
|
3445
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3446
|
+
}
|
|
3447
|
+
catch (error) {
|
|
3448
|
+
// ignore
|
|
3449
|
+
}
|
|
3450
|
+
if (resultWithElementsFound.length === 0) {
|
|
3451
|
+
await _screenshot(state, this);
|
|
3452
|
+
return state.info;
|
|
3453
|
+
}
|
|
3454
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3455
|
+
throw new Error(`Text ${text} found in page`);
|
|
3456
|
+
}
|
|
3457
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
catch (e) {
|
|
3461
|
+
await _commandError(state, e, this);
|
|
3462
|
+
}
|
|
3463
|
+
finally {
|
|
3464
|
+
await _commandFinally(state, this);
|
|
2053
3465
|
}
|
|
2054
3466
|
}
|
|
2055
|
-
async
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
3467
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
3468
|
+
textAnchor = unEscapeString(textAnchor);
|
|
3469
|
+
textToVerify = unEscapeString(textToVerify);
|
|
3470
|
+
const state = {
|
|
3471
|
+
text_search: textToVerify,
|
|
3472
|
+
options,
|
|
3473
|
+
world,
|
|
3474
|
+
locate: false,
|
|
3475
|
+
scroll: false,
|
|
3476
|
+
highlight: false,
|
|
3477
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
3478
|
+
text: `Verify text with relation to another text`,
|
|
3479
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
3480
|
+
operation: "verify_text_with_relation",
|
|
3481
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3482
|
+
};
|
|
3483
|
+
const cmdStartTime = Date.now();
|
|
3484
|
+
let cmdEndTime = null;
|
|
3485
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2062
3486
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
let
|
|
3487
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
3488
|
+
if (newValue !== textAnchor) {
|
|
3489
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
3490
|
+
textAnchor = newValue;
|
|
3491
|
+
}
|
|
3492
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
3493
|
+
if (newValue !== textToVerify) {
|
|
3494
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
3495
|
+
textToVerify = newValue;
|
|
3496
|
+
}
|
|
3497
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
3498
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
3499
|
+
let foundAncore = false;
|
|
2074
3500
|
try {
|
|
3501
|
+
await _preCommand(state, this);
|
|
3502
|
+
state.info.text = textToVerify;
|
|
3503
|
+
let resultWithElementsFound = {
|
|
3504
|
+
length: 0,
|
|
3505
|
+
};
|
|
2075
3506
|
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
|
-
}
|
|
3507
|
+
try {
|
|
3508
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3509
|
+
}
|
|
3510
|
+
catch (error) {
|
|
3511
|
+
// ignore
|
|
2098
3512
|
}
|
|
2099
|
-
info.results = results;
|
|
2100
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2101
3513
|
if (resultWithElementsFound.length === 0) {
|
|
2102
|
-
if (Date.now() - startTime > timeout) {
|
|
2103
|
-
throw new Error(`Text ${
|
|
3514
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3515
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2104
3516
|
}
|
|
2105
3517
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2106
3518
|
continue;
|
|
2107
3519
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
3520
|
+
else {
|
|
3521
|
+
cmdEndTime = Date.now();
|
|
3522
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3523
|
+
if (foundAncore) {
|
|
3524
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3525
|
+
}
|
|
3526
|
+
else {
|
|
3527
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
try {
|
|
3532
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3533
|
+
foundAncore = true;
|
|
3534
|
+
const result = resultWithElementsFound[i];
|
|
3535
|
+
const token = result.randomToken;
|
|
3536
|
+
const frame = result.frame;
|
|
3537
|
+
let css = `[data-blinq-id-${token}]`;
|
|
3538
|
+
const climbArray1 = [];
|
|
3539
|
+
for (let i = 0; i < climb; i++) {
|
|
3540
|
+
climbArray1.push("..");
|
|
3541
|
+
}
|
|
3542
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
3543
|
+
css = css + " >> " + climbXpath;
|
|
3544
|
+
const count = await frame.locator(css).count();
|
|
3545
|
+
for (let j = 0; j < count; j++) {
|
|
3546
|
+
const continer = await frame.locator(css).nth(j);
|
|
3547
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
3548
|
+
if (result.elementCount > 0) {
|
|
3549
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
3550
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3551
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
3552
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
3553
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
3554
|
+
// this._highlightElements(frame, dataAttribute)
|
|
3555
|
+
// .then(async () => {
|
|
3556
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3557
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
3558
|
+
// () => {}
|
|
3559
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
3560
|
+
// );
|
|
3561
|
+
// })
|
|
3562
|
+
// .catch(e);
|
|
3563
|
+
// }
|
|
3564
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
3565
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3566
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3567
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
3568
|
+
if (element) {
|
|
3569
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3570
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3571
|
+
}
|
|
3572
|
+
await _screenshot(state, this);
|
|
3573
|
+
return state.info;
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
2116
3576
|
}
|
|
2117
3577
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
3578
|
+
catch (error) {
|
|
3579
|
+
console.error(error);
|
|
3580
|
+
}
|
|
2120
3581
|
}
|
|
2121
3582
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2122
3583
|
}
|
|
2123
3584
|
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;
|
|
3585
|
+
await _commandError(state, e, this);
|
|
2131
3586
|
}
|
|
2132
3587
|
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
|
-
});
|
|
3588
|
+
await _commandFinally(state, this);
|
|
2152
3589
|
}
|
|
2153
3590
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
3591
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
3592
|
+
const frames = this.page.frames();
|
|
3593
|
+
let results = [];
|
|
3594
|
+
let ignoreCase = false;
|
|
3595
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3596
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
3597
|
+
result.frame = frames[i];
|
|
3598
|
+
const climbArray = [];
|
|
3599
|
+
for (let i = 0; i < climb; i++) {
|
|
3600
|
+
climbArray.push("..");
|
|
3601
|
+
}
|
|
3602
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
3603
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
3604
|
+
const count = await frames[i].locator(newLocator).count();
|
|
3605
|
+
if (count > 0) {
|
|
3606
|
+
result.elementCount = count;
|
|
3607
|
+
result.locator = newLocator;
|
|
3608
|
+
results.push(result);
|
|
3609
|
+
}
|
|
2161
3610
|
}
|
|
2162
|
-
|
|
3611
|
+
// state.info.results = results;
|
|
3612
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3613
|
+
return resultWithElementsFound;
|
|
2163
3614
|
}
|
|
2164
3615
|
async visualVerification(text, options = {}, world = null) {
|
|
2165
3616
|
const startTime = Date.now();
|
|
@@ -2175,14 +3626,17 @@ class StableBrowser {
|
|
|
2175
3626
|
throw new Error("TOKEN is not set");
|
|
2176
3627
|
}
|
|
2177
3628
|
try {
|
|
2178
|
-
let serviceUrl =
|
|
3629
|
+
let serviceUrl = _getServerUrl();
|
|
2179
3630
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2180
3631
|
info.screenshotPath = screenshotPath;
|
|
2181
3632
|
const screenshot = await this.takeScreenshot();
|
|
2182
|
-
|
|
2183
|
-
method: "
|
|
3633
|
+
let request = {
|
|
3634
|
+
method: "post",
|
|
3635
|
+
maxBodyLength: Infinity,
|
|
2184
3636
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2185
3637
|
headers: {
|
|
3638
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3639
|
+
"x-source": "aaa",
|
|
2186
3640
|
"Content-Type": "application/json",
|
|
2187
3641
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2188
3642
|
},
|
|
@@ -2191,7 +3645,7 @@ class StableBrowser {
|
|
|
2191
3645
|
screenshot: screenshot,
|
|
2192
3646
|
}),
|
|
2193
3647
|
};
|
|
2194
|
-
|
|
3648
|
+
const result = await axios.request(request);
|
|
2195
3649
|
if (result.data.status !== true) {
|
|
2196
3650
|
throw new Error("Visual validation failed");
|
|
2197
3651
|
}
|
|
@@ -2211,13 +3665,15 @@ class StableBrowser {
|
|
|
2211
3665
|
info.screenshotPath = screenshotPath;
|
|
2212
3666
|
Object.assign(e, { info: info });
|
|
2213
3667
|
error = e;
|
|
2214
|
-
throw e;
|
|
3668
|
+
// throw e;
|
|
3669
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2215
3670
|
}
|
|
2216
3671
|
finally {
|
|
2217
3672
|
const endTime = Date.now();
|
|
2218
|
-
|
|
3673
|
+
_reportToWorld(world, {
|
|
2219
3674
|
type: Types.VERIFY_VISUAL,
|
|
2220
3675
|
text: "Visual verification",
|
|
3676
|
+
_text: "Visual verification of " + text,
|
|
2221
3677
|
screenshotId,
|
|
2222
3678
|
result: error
|
|
2223
3679
|
? {
|
|
@@ -2263,6 +3719,7 @@ class StableBrowser {
|
|
|
2263
3719
|
let screenshotPath = null;
|
|
2264
3720
|
const info = {};
|
|
2265
3721
|
info.log = "";
|
|
3722
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2266
3723
|
info.operation = "getTableData";
|
|
2267
3724
|
info.selectors = selectors;
|
|
2268
3725
|
try {
|
|
@@ -2278,11 +3735,12 @@ class StableBrowser {
|
|
|
2278
3735
|
info.screenshotPath = screenshotPath;
|
|
2279
3736
|
Object.assign(e, { info: info });
|
|
2280
3737
|
error = e;
|
|
2281
|
-
throw e;
|
|
3738
|
+
// throw e;
|
|
3739
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2282
3740
|
}
|
|
2283
3741
|
finally {
|
|
2284
3742
|
const endTime = Date.now();
|
|
2285
|
-
|
|
3743
|
+
_reportToWorld(world, {
|
|
2286
3744
|
element_name: selectors.element_name,
|
|
2287
3745
|
type: Types.GET_TABLE_DATA,
|
|
2288
3746
|
text: "Get table data",
|
|
@@ -2337,7 +3795,7 @@ class StableBrowser {
|
|
|
2337
3795
|
info.operation = "analyzeTable";
|
|
2338
3796
|
info.selectors = selectors;
|
|
2339
3797
|
info.query = query;
|
|
2340
|
-
query =
|
|
3798
|
+
query = _fixUsingParams(query, _params);
|
|
2341
3799
|
info.query_fixed = query;
|
|
2342
3800
|
info.operator = operator;
|
|
2343
3801
|
info.value = value;
|
|
@@ -2443,11 +3901,12 @@ class StableBrowser {
|
|
|
2443
3901
|
info.screenshotPath = screenshotPath;
|
|
2444
3902
|
Object.assign(e, { info: info });
|
|
2445
3903
|
error = e;
|
|
2446
|
-
throw e;
|
|
3904
|
+
// throw e;
|
|
3905
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2447
3906
|
}
|
|
2448
3907
|
finally {
|
|
2449
3908
|
const endTime = Date.now();
|
|
2450
|
-
|
|
3909
|
+
_reportToWorld(world, {
|
|
2451
3910
|
element_name: selectors.element_name,
|
|
2452
3911
|
type: Types.ANALYZE_TABLE,
|
|
2453
3912
|
text: "Analyze table",
|
|
@@ -2468,28 +3927,51 @@ class StableBrowser {
|
|
|
2468
3927
|
});
|
|
2469
3928
|
}
|
|
2470
3929
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
3930
|
+
/**
|
|
3931
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3932
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3933
|
+
* @param options - Optional configuration object
|
|
3934
|
+
* @param world - Optional world context
|
|
3935
|
+
* @returns Promise that resolves after the specified duration
|
|
3936
|
+
*/
|
|
3937
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3938
|
+
const state = {
|
|
3939
|
+
duration,
|
|
3940
|
+
options,
|
|
3941
|
+
world,
|
|
3942
|
+
locate: false,
|
|
3943
|
+
scroll: false,
|
|
3944
|
+
screenshot: false,
|
|
3945
|
+
highlight: false,
|
|
3946
|
+
type: Types.SLEEP,
|
|
3947
|
+
text: `Sleep for ${duration} ms`,
|
|
3948
|
+
_text: `Sleep for ${duration} ms`,
|
|
3949
|
+
operation: "sleep",
|
|
3950
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3951
|
+
};
|
|
3952
|
+
try {
|
|
3953
|
+
await _preCommand(state, this);
|
|
3954
|
+
if (duration < 0) {
|
|
3955
|
+
throw new Error("Sleep duration cannot be negative");
|
|
2487
3956
|
}
|
|
3957
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3958
|
+
return state.info;
|
|
3959
|
+
}
|
|
3960
|
+
catch (e) {
|
|
3961
|
+
await _commandError(state, e, this);
|
|
3962
|
+
}
|
|
3963
|
+
finally {
|
|
3964
|
+
await _commandFinally(state, this);
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
3968
|
+
try {
|
|
3969
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2488
3970
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
3971
|
+
catch (error) {
|
|
3972
|
+
this.logger.debug(error);
|
|
3973
|
+
throw error;
|
|
2491
3974
|
}
|
|
2492
|
-
return value;
|
|
2493
3975
|
}
|
|
2494
3976
|
_getLoadTimeout(options) {
|
|
2495
3977
|
let timeout = 15000;
|
|
@@ -2501,7 +3983,54 @@ class StableBrowser {
|
|
|
2501
3983
|
}
|
|
2502
3984
|
return timeout;
|
|
2503
3985
|
}
|
|
3986
|
+
_getFindElementTimeout(options) {
|
|
3987
|
+
if (options && options.timeout) {
|
|
3988
|
+
return options.timeout;
|
|
3989
|
+
}
|
|
3990
|
+
if (this.configuration.find_element_timeout) {
|
|
3991
|
+
return this.configuration.find_element_timeout;
|
|
3992
|
+
}
|
|
3993
|
+
return 30000;
|
|
3994
|
+
}
|
|
3995
|
+
async saveStoreState(path = null, world = null) {
|
|
3996
|
+
const storageState = await this.page.context().storageState();
|
|
3997
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
3998
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
3999
|
+
if (path) {
|
|
4000
|
+
// save { storageState: storageState } into the path
|
|
4001
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
4002
|
+
}
|
|
4003
|
+
else {
|
|
4004
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
async restoreSaveState(path = null, world = null) {
|
|
4008
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4009
|
+
await refreshBrowser(this, path, world);
|
|
4010
|
+
this.registerEventListeners(this.context);
|
|
4011
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
4012
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
4013
|
+
if (this.onRestoreSaveState) {
|
|
4014
|
+
this.onRestoreSaveState(path);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
2504
4017
|
async waitForPageLoad(options = {}, world = null) {
|
|
4018
|
+
// try {
|
|
4019
|
+
// let currentPagePath = null;
|
|
4020
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4021
|
+
// if (this.latestPagePath) {
|
|
4022
|
+
// // get the currect page path and compare with the latest page path
|
|
4023
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4024
|
+
// // if the page path is the same, do not wait for page load
|
|
4025
|
+
// console.log("No page change: " + currentPagePath);
|
|
4026
|
+
// return;
|
|
4027
|
+
// }
|
|
4028
|
+
// }
|
|
4029
|
+
// this.latestPagePath = currentPagePath;
|
|
4030
|
+
// } catch (e) {
|
|
4031
|
+
// console.debug("Error getting current page path: ", e);
|
|
4032
|
+
// }
|
|
4033
|
+
//console.log("Waiting for page load");
|
|
2505
4034
|
let timeout = this._getLoadTimeout(options);
|
|
2506
4035
|
const promiseArray = [];
|
|
2507
4036
|
// let waitForNetworkIdle = true;
|
|
@@ -2534,13 +4063,15 @@ class StableBrowser {
|
|
|
2534
4063
|
else if (e.label === "domcontentloaded") {
|
|
2535
4064
|
console.log("waited for the domcontent loaded timeout");
|
|
2536
4065
|
}
|
|
2537
|
-
console.log(".");
|
|
2538
4066
|
}
|
|
2539
4067
|
finally {
|
|
2540
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4068
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4069
|
+
if (options && !options.noSleep) {
|
|
4070
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4071
|
+
}
|
|
2541
4072
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2542
4073
|
const endTime = Date.now();
|
|
2543
|
-
|
|
4074
|
+
_reportToWorld(world, {
|
|
2544
4075
|
type: Types.GET_PAGE_STATUS,
|
|
2545
4076
|
text: "Wait for page load",
|
|
2546
4077
|
screenshotId,
|
|
@@ -2560,41 +4091,133 @@ class StableBrowser {
|
|
|
2560
4091
|
}
|
|
2561
4092
|
}
|
|
2562
4093
|
async closePage(options = {}, world = null) {
|
|
2563
|
-
const
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
4094
|
+
const state = {
|
|
4095
|
+
options,
|
|
4096
|
+
world,
|
|
4097
|
+
locate: false,
|
|
4098
|
+
scroll: false,
|
|
4099
|
+
highlight: false,
|
|
4100
|
+
type: Types.CLOSE_PAGE,
|
|
4101
|
+
text: `Close page`,
|
|
4102
|
+
_text: `Close the page`,
|
|
4103
|
+
operation: "closePage",
|
|
4104
|
+
log: "***** close page *****\n",
|
|
4105
|
+
throwError: false,
|
|
4106
|
+
};
|
|
2568
4107
|
try {
|
|
4108
|
+
await _preCommand(state, this);
|
|
2569
4109
|
await this.page.close();
|
|
2570
4110
|
}
|
|
2571
4111
|
catch (e) {
|
|
2572
|
-
|
|
4112
|
+
await _commandError(state, e, this);
|
|
2573
4113
|
}
|
|
2574
4114
|
finally {
|
|
2575
|
-
await
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
4115
|
+
await _commandFinally(state, this);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
4119
|
+
let operation = null;
|
|
4120
|
+
if (!options || !options.operation) {
|
|
4121
|
+
throw new Error("operation is not defined");
|
|
4122
|
+
}
|
|
4123
|
+
operation = options.operation;
|
|
4124
|
+
// validate operation is one of the supported operations
|
|
4125
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
4126
|
+
throw new Error("operation is not supported");
|
|
4127
|
+
}
|
|
4128
|
+
const state = {
|
|
4129
|
+
options,
|
|
4130
|
+
world,
|
|
4131
|
+
locate: false,
|
|
4132
|
+
scroll: false,
|
|
4133
|
+
highlight: false,
|
|
4134
|
+
type: Types.TABLE_OPERATION,
|
|
4135
|
+
text: `Table operation`,
|
|
4136
|
+
_text: `Table ${operation} operation`,
|
|
4137
|
+
operation: operation,
|
|
4138
|
+
log: "***** Table operation *****\n",
|
|
4139
|
+
};
|
|
4140
|
+
const timeout = this._getFindElementTimeout(options);
|
|
4141
|
+
try {
|
|
4142
|
+
await _preCommand(state, this);
|
|
4143
|
+
const start = Date.now();
|
|
4144
|
+
let cellArea = null;
|
|
4145
|
+
while (true) {
|
|
4146
|
+
try {
|
|
4147
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
4148
|
+
if (cellArea) {
|
|
4149
|
+
break;
|
|
2588
4150
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
4151
|
+
}
|
|
4152
|
+
catch (e) {
|
|
4153
|
+
// ignore
|
|
4154
|
+
}
|
|
4155
|
+
if (Date.now() - start > timeout) {
|
|
4156
|
+
throw new Error(`Cell not found in table`);
|
|
4157
|
+
}
|
|
4158
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
4159
|
+
}
|
|
4160
|
+
switch (operation) {
|
|
4161
|
+
case "click":
|
|
4162
|
+
if (!options.css) {
|
|
4163
|
+
// will click in the center of the cell
|
|
4164
|
+
let xOffset = 0;
|
|
4165
|
+
let yOffset = 0;
|
|
4166
|
+
if (options.xOffset) {
|
|
4167
|
+
xOffset = options.xOffset;
|
|
4168
|
+
}
|
|
4169
|
+
if (options.yOffset) {
|
|
4170
|
+
yOffset = options.yOffset;
|
|
4171
|
+
}
|
|
4172
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
4173
|
+
}
|
|
4174
|
+
else {
|
|
4175
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4176
|
+
if (results.length === 0) {
|
|
4177
|
+
throw new Error(`Element not found in cell area`);
|
|
4178
|
+
}
|
|
4179
|
+
state.element = results[0];
|
|
4180
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
4181
|
+
}
|
|
4182
|
+
break;
|
|
4183
|
+
case "hover+click":
|
|
4184
|
+
if (!options.css) {
|
|
4185
|
+
throw new Error("css is not defined");
|
|
4186
|
+
}
|
|
4187
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4188
|
+
if (results.length === 0) {
|
|
4189
|
+
throw new Error(`Element not found in cell area`);
|
|
4190
|
+
}
|
|
4191
|
+
state.element = results[0];
|
|
4192
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
4193
|
+
break;
|
|
4194
|
+
case "hover":
|
|
4195
|
+
if (!options.css) {
|
|
4196
|
+
throw new Error("css is not defined");
|
|
4197
|
+
}
|
|
4198
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4199
|
+
if (result1.length === 0) {
|
|
4200
|
+
throw new Error(`Element not found in cell area`);
|
|
4201
|
+
}
|
|
4202
|
+
state.element = result1[0];
|
|
4203
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4204
|
+
break;
|
|
4205
|
+
default:
|
|
4206
|
+
throw new Error("operation is not supported");
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
catch (e) {
|
|
4210
|
+
await _commandError(state, e, this);
|
|
4211
|
+
}
|
|
4212
|
+
finally {
|
|
4213
|
+
await _commandFinally(state, this);
|
|
2596
4214
|
}
|
|
2597
4215
|
}
|
|
4216
|
+
saveTestDataAsGlobal(options, world) {
|
|
4217
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
4218
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4219
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
4220
|
+
}
|
|
2598
4221
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2599
4222
|
const startTime = Date.now();
|
|
2600
4223
|
let error = null;
|
|
@@ -2611,15 +4234,16 @@ class StableBrowser {
|
|
|
2611
4234
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2612
4235
|
}
|
|
2613
4236
|
catch (e) {
|
|
2614
|
-
|
|
4237
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2615
4238
|
}
|
|
2616
4239
|
finally {
|
|
2617
4240
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2618
4241
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2619
4242
|
const endTime = Date.now();
|
|
2620
|
-
|
|
4243
|
+
_reportToWorld(world, {
|
|
2621
4244
|
type: Types.SET_VIEWPORT,
|
|
2622
4245
|
text: "set viewport size to " + width + "x" + hight,
|
|
4246
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2623
4247
|
screenshotId,
|
|
2624
4248
|
result: error
|
|
2625
4249
|
? {
|
|
@@ -2647,13 +4271,13 @@ class StableBrowser {
|
|
|
2647
4271
|
await this.page.reload();
|
|
2648
4272
|
}
|
|
2649
4273
|
catch (e) {
|
|
2650
|
-
|
|
4274
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2651
4275
|
}
|
|
2652
4276
|
finally {
|
|
2653
4277
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2654
4278
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2655
4279
|
const endTime = Date.now();
|
|
2656
|
-
|
|
4280
|
+
_reportToWorld(world, {
|
|
2657
4281
|
type: Types.GET_PAGE_STATUS,
|
|
2658
4282
|
text: "page relaod",
|
|
2659
4283
|
screenshotId,
|
|
@@ -2689,11 +4313,239 @@ class StableBrowser {
|
|
|
2689
4313
|
console.log("#-#");
|
|
2690
4314
|
}
|
|
2691
4315
|
}
|
|
2692
|
-
|
|
2693
|
-
if (
|
|
2694
|
-
|
|
4316
|
+
async beforeScenario(world, scenario) {
|
|
4317
|
+
if (world && world.attach) {
|
|
4318
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4319
|
+
}
|
|
4320
|
+
this.context.loadedRoutes = null;
|
|
4321
|
+
this.beforeScenarioCalled = true;
|
|
4322
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4323
|
+
this.scenarioName = scenario.pickle.name;
|
|
4324
|
+
}
|
|
4325
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
4326
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
4327
|
+
}
|
|
4328
|
+
if (this.context) {
|
|
4329
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
4330
|
+
}
|
|
4331
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4332
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4333
|
+
// check if @global_test_data tag is present
|
|
4334
|
+
if (this.tags.includes("@global_test_data")) {
|
|
4335
|
+
this.saveTestDataAsGlobal({}, world);
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
// update test data based on feature/scenario
|
|
4339
|
+
let envName = null;
|
|
4340
|
+
if (this.context && this.context.environment) {
|
|
4341
|
+
envName = this.context.environment.name;
|
|
4342
|
+
}
|
|
4343
|
+
if (!process.env.TEMP_RUN) {
|
|
4344
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4345
|
+
}
|
|
4346
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4347
|
+
}
|
|
4348
|
+
async afterScenario(world, scenario) { }
|
|
4349
|
+
async beforeStep(world, step) {
|
|
4350
|
+
this.stepTags = [];
|
|
4351
|
+
if (!this.beforeScenarioCalled) {
|
|
4352
|
+
this.beforeScenario(world, step);
|
|
4353
|
+
this.context.loadedRoutes = null;
|
|
4354
|
+
}
|
|
4355
|
+
if (this.stepIndex === undefined) {
|
|
4356
|
+
this.stepIndex = 0;
|
|
4357
|
+
}
|
|
4358
|
+
else {
|
|
4359
|
+
this.stepIndex++;
|
|
4360
|
+
}
|
|
4361
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4362
|
+
this.stepName = step.pickleStep.text;
|
|
4363
|
+
let printableStepName = this.stepName;
|
|
4364
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4365
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4366
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4367
|
+
});
|
|
4368
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4369
|
+
}
|
|
4370
|
+
else if (step && step.text) {
|
|
4371
|
+
this.stepName = step.text;
|
|
4372
|
+
}
|
|
4373
|
+
else {
|
|
4374
|
+
this.stepName = "step " + this.stepIndex;
|
|
4375
|
+
}
|
|
4376
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4377
|
+
if (this.context.browserObject.context) {
|
|
4378
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
if (this.initSnapshotTaken === false) {
|
|
4382
|
+
this.initSnapshotTaken = true;
|
|
4383
|
+
if (world &&
|
|
4384
|
+
world.attach &&
|
|
4385
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4386
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4387
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4388
|
+
if (snapshot) {
|
|
4389
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
this.context.routeResults = null;
|
|
4394
|
+
this.context.loadedRoutes = null;
|
|
4395
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4396
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4397
|
+
}
|
|
4398
|
+
setStepTags(tags) {
|
|
4399
|
+
this.stepTags = tags;
|
|
4400
|
+
}
|
|
4401
|
+
async getAriaSnapshot() {
|
|
4402
|
+
try {
|
|
4403
|
+
// find the page url
|
|
4404
|
+
const url = await this.page.url();
|
|
4405
|
+
// extract the path from the url
|
|
4406
|
+
const path = new URL(url).pathname;
|
|
4407
|
+
// get the page title
|
|
4408
|
+
const title = await this.page.title();
|
|
4409
|
+
// go over other frams
|
|
4410
|
+
const frames = this.page.frames();
|
|
4411
|
+
const snapshots = [];
|
|
4412
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4413
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4414
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4415
|
+
const frame = frames[i];
|
|
4416
|
+
try {
|
|
4417
|
+
// Ensure frame is attached and has body
|
|
4418
|
+
const body = frame.locator("body");
|
|
4419
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4420
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4421
|
+
if (!snapshot) {
|
|
4422
|
+
continue;
|
|
4423
|
+
}
|
|
4424
|
+
content.push(`- frame: ${i}`);
|
|
4425
|
+
content.push(snapshot);
|
|
4426
|
+
}
|
|
4427
|
+
catch (innerErr) {
|
|
4428
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4429
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
return content.join("\n");
|
|
4433
|
+
}
|
|
4434
|
+
catch (e) {
|
|
4435
|
+
console.log("Error in getAriaSnapshot");
|
|
4436
|
+
//console.debug(e);
|
|
4437
|
+
}
|
|
4438
|
+
return null;
|
|
4439
|
+
}
|
|
4440
|
+
/**
|
|
4441
|
+
* Sends command with custom payload to report.
|
|
4442
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4443
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4444
|
+
* @param content - Content of the command to be shown in the report.
|
|
4445
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4446
|
+
* @param world - Optional world context.
|
|
4447
|
+
* @public
|
|
4448
|
+
*/
|
|
4449
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4450
|
+
const state = {
|
|
4451
|
+
options,
|
|
4452
|
+
world,
|
|
4453
|
+
locate: false,
|
|
4454
|
+
scroll: false,
|
|
4455
|
+
screenshot: options.screenshot ?? false,
|
|
4456
|
+
highlight: options.highlight ?? false,
|
|
4457
|
+
type: Types.REPORT_COMMAND,
|
|
4458
|
+
text: commandText,
|
|
4459
|
+
_text: commandText,
|
|
4460
|
+
operation: "report_command",
|
|
4461
|
+
log: "***** " + commandText + " *****\n",
|
|
4462
|
+
};
|
|
4463
|
+
try {
|
|
4464
|
+
await _preCommand(state, this);
|
|
4465
|
+
const payload = {
|
|
4466
|
+
type: options.type ?? "text",
|
|
4467
|
+
content: content,
|
|
4468
|
+
screenshotId: null,
|
|
4469
|
+
};
|
|
4470
|
+
state.payload = payload;
|
|
4471
|
+
if (commandStatus === "FAILED") {
|
|
4472
|
+
state.throwError = true;
|
|
4473
|
+
throw new Error("Command failed");
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
catch (e) {
|
|
4477
|
+
await _commandError(state, e, this);
|
|
4478
|
+
}
|
|
4479
|
+
finally {
|
|
4480
|
+
await _commandFinally(state, this);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
async afterStep(world, step) {
|
|
4484
|
+
this.stepName = null;
|
|
4485
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4486
|
+
if (this.context.browserObject.context) {
|
|
4487
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
4488
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4489
|
+
});
|
|
4490
|
+
if (world && world.attach) {
|
|
4491
|
+
await world.attach(JSON.stringify({
|
|
4492
|
+
type: "trace",
|
|
4493
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4494
|
+
}), "application/json+trace");
|
|
4495
|
+
}
|
|
4496
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
if (this.context) {
|
|
4500
|
+
this.context.examplesRow = null;
|
|
4501
|
+
}
|
|
4502
|
+
if (world &&
|
|
4503
|
+
world.attach &&
|
|
4504
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4505
|
+
!this.fastMode &&
|
|
4506
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4507
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4508
|
+
if (snapshot) {
|
|
4509
|
+
const obj = {};
|
|
4510
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4514
|
+
if (this.context.routeResults) {
|
|
4515
|
+
if (world && world.attach) {
|
|
4516
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
if (!process.env.TEMP_RUN) {
|
|
4520
|
+
const state = {
|
|
4521
|
+
world,
|
|
4522
|
+
locate: false,
|
|
4523
|
+
scroll: false,
|
|
4524
|
+
screenshot: true,
|
|
4525
|
+
highlight: true,
|
|
4526
|
+
type: Types.STEP_COMPLETE,
|
|
4527
|
+
text: "end of scenario",
|
|
4528
|
+
_text: "end of scenario",
|
|
4529
|
+
operation: "step_complete",
|
|
4530
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4531
|
+
};
|
|
4532
|
+
try {
|
|
4533
|
+
await _preCommand(state, this);
|
|
4534
|
+
}
|
|
4535
|
+
catch (e) {
|
|
4536
|
+
await _commandError(state, e, this);
|
|
4537
|
+
}
|
|
4538
|
+
finally {
|
|
4539
|
+
await _commandFinally(state, this);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
networkAfterStep(this.stepName, this.context);
|
|
4543
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4544
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4545
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4546
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4547
|
+
}
|
|
2695
4548
|
}
|
|
2696
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2697
4549
|
}
|
|
2698
4550
|
}
|
|
2699
4551
|
function createTimedPromise(promise, label) {
|
|
@@ -2701,156 +4553,5 @@ function createTimedPromise(promise, label) {
|
|
|
2701
4553
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2702
4554
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2703
4555
|
}
|
|
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
4556
|
export { StableBrowser };
|
|
2856
4557
|
//# sourceMappingURL=stable_browser.js.map
|