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