automation_model 1.0.457-dev → 1.0.457
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 +43 -2
- package/lib/api.js +239 -49
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +7 -2
- package/lib/auto_page.js +269 -23
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +6 -3
- package/lib/browser_manager.js +175 -50
- 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/command_common.d.ts +6 -0
- package/lib/command_common.js +206 -0
- package/lib/command_common.js.map +1 -0
- 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 +6 -3
- 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 +61 -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 +3 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +4 -2
- package/lib/init_browser.js +94 -12
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.d.ts +7 -0
- package/lib/locate_element.js +215 -0
- package/lib/locate_element.js.map +1 -0
- 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 +431 -0
- package/lib/network.js.map +1 -0
- package/lib/route.d.ts +21 -0
- package/lib/route.js +392 -0
- package/lib/route.js.map +1 -0
- package/lib/scripts/axe.mini.js +12 -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 +150 -55
- package/lib/stable_browser.js +2541 -1291
- 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 +116 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +6 -0
- package/lib/test_context.js +15 -11
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +36 -2
- package/lib/utils.js +697 -11
- package/lib/utils.js.map +1 -1
- package/package.json +19 -10
package/lib/stable_browser.js
CHANGED
|
@@ -10,26 +10,42 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
10
|
import drawRectangle from "./drawRect.js";
|
|
11
11
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
-
import { decrypt } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
15
14
|
import csv from "csv-parser";
|
|
16
15
|
import { Readable } from "node:stream";
|
|
17
16
|
import readline from "readline";
|
|
18
|
-
import { getContext } from "./init_browser.js";
|
|
19
|
-
|
|
17
|
+
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
18
|
+
import { getTestData } from "./auto_page.js";
|
|
19
|
+
import { locate_element } from "./locate_element.js";
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
22
|
+
import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
23
|
+
import { LocatorLog } from "./locator_log.js";
|
|
24
|
+
import axios from "axios";
|
|
25
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
|
+
import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
27
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
28
|
+
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
29
|
+
export const Types = {
|
|
20
30
|
CLICK: "click_element",
|
|
31
|
+
WAIT_ELEMENT: "wait_element",
|
|
21
32
|
NAVIGATE: "navigate",
|
|
33
|
+
GO_BACK: "go_back",
|
|
34
|
+
GO_FORWARD: "go_forward",
|
|
22
35
|
FILL: "fill_element",
|
|
23
|
-
EXECUTE: "execute_page_method",
|
|
24
|
-
OPEN: "open_environment",
|
|
36
|
+
EXECUTE: "execute_page_method", //
|
|
37
|
+
OPEN: "open_environment", //
|
|
25
38
|
COMPLETE: "step_complete",
|
|
26
39
|
ASK: "information_needed",
|
|
27
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
28
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
40
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
41
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
29
42
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
43
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
44
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
30
45
|
ANALYZE_TABLE: "analyze_table",
|
|
31
|
-
SELECT: "select_combobox",
|
|
46
|
+
SELECT: "select_combobox", //
|
|
32
47
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
48
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
33
49
|
TYPE_PRESS: "type_press",
|
|
34
50
|
PRESS: "press_key",
|
|
35
51
|
HOVER: "hover_element",
|
|
@@ -37,25 +53,50 @@ const Types = {
|
|
|
37
53
|
UNCHECK: "uncheck_element",
|
|
38
54
|
EXTRACT: "extract_attribute",
|
|
39
55
|
CLOSE_PAGE: "close_page",
|
|
56
|
+
TABLE_OPERATION: "table_operation",
|
|
40
57
|
SET_DATE_TIME: "set_date_time",
|
|
41
58
|
SET_VIEWPORT: "set_viewport",
|
|
42
59
|
VERIFY_VISUAL: "verify_visual",
|
|
43
60
|
LOAD_DATA: "load_data",
|
|
44
61
|
SET_INPUT: "set_input",
|
|
62
|
+
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
63
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
64
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
65
|
+
BRUNO: "bruno",
|
|
66
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
67
|
+
SET_INPUT_FILES: "set_input_files",
|
|
68
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
69
|
+
REPORT_COMMAND: "report_command",
|
|
70
|
+
STEP_COMPLETE: "step_complete",
|
|
71
|
+
SLEEP: "sleep",
|
|
72
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
45
73
|
};
|
|
46
74
|
export const apps = {};
|
|
75
|
+
const formatElementName = (elementName) => {
|
|
76
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
77
|
+
};
|
|
47
78
|
class StableBrowser {
|
|
48
|
-
|
|
79
|
+
browser;
|
|
80
|
+
page;
|
|
81
|
+
logger;
|
|
82
|
+
context;
|
|
83
|
+
world;
|
|
84
|
+
fastMode;
|
|
85
|
+
project_path = null;
|
|
86
|
+
webLogFile = null;
|
|
87
|
+
networkLogger = null;
|
|
88
|
+
configuration = null;
|
|
89
|
+
appName = "main";
|
|
90
|
+
tags = null;
|
|
91
|
+
isRecording = false;
|
|
92
|
+
initSnapshotTaken = false;
|
|
93
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
|
|
49
94
|
this.browser = browser;
|
|
50
95
|
this.page = page;
|
|
51
96
|
this.logger = logger;
|
|
52
97
|
this.context = context;
|
|
53
98
|
this.world = world;
|
|
54
|
-
this.
|
|
55
|
-
this.webLogFile = null;
|
|
56
|
-
this.networkLogger = null;
|
|
57
|
-
this.configuration = null;
|
|
58
|
-
this.appName = "main";
|
|
99
|
+
this.fastMode = fastMode;
|
|
59
100
|
if (!this.logger) {
|
|
60
101
|
this.logger = console;
|
|
61
102
|
}
|
|
@@ -84,19 +125,52 @@ class StableBrowser {
|
|
|
84
125
|
context.pages = [this.page];
|
|
85
126
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
86
127
|
this.world = world;
|
|
128
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
129
|
+
this.fastMode = true;
|
|
130
|
+
}
|
|
131
|
+
if (process.env.FAST_MODE === "true") {
|
|
132
|
+
// console.log("Fast mode enabled from environment variable");
|
|
133
|
+
this.fastMode = true;
|
|
134
|
+
}
|
|
135
|
+
if (process.env.FAST_MODE === "false") {
|
|
136
|
+
this.fastMode = false;
|
|
137
|
+
}
|
|
138
|
+
if (this.context) {
|
|
139
|
+
this.context.fastMode = this.fastMode;
|
|
140
|
+
}
|
|
87
141
|
this.registerEventListeners(this.context);
|
|
142
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
143
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
88
144
|
}
|
|
89
145
|
registerEventListeners(context) {
|
|
90
146
|
this.registerConsoleLogListener(this.page, context);
|
|
91
|
-
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
147
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
92
148
|
if (!context.pageLoading) {
|
|
93
149
|
context.pageLoading = { status: false };
|
|
94
150
|
}
|
|
151
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
152
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
153
|
+
}
|
|
95
154
|
context.playContext.on("page", async function (page) {
|
|
155
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
156
|
+
console.log("close unexpected popups");
|
|
157
|
+
await page.close();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
96
160
|
context.pageLoading.status = true;
|
|
97
161
|
this.page = page;
|
|
162
|
+
try {
|
|
163
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
164
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error("Error on dialog accept registration", error);
|
|
169
|
+
}
|
|
98
170
|
context.page = page;
|
|
99
171
|
context.pages.push(page);
|
|
172
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
173
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
100
174
|
page.on("close", async () => {
|
|
101
175
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
102
176
|
this.context.pages.pop();
|
|
@@ -128,7 +202,7 @@ class StableBrowser {
|
|
|
128
202
|
}
|
|
129
203
|
let newContextCreated = false;
|
|
130
204
|
if (!apps[appName]) {
|
|
131
|
-
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this);
|
|
205
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
132
206
|
newContextCreated = true;
|
|
133
207
|
apps[appName] = {
|
|
134
208
|
context: newContext,
|
|
@@ -137,38 +211,47 @@ class StableBrowser {
|
|
|
137
211
|
};
|
|
138
212
|
}
|
|
139
213
|
const tempContext = {};
|
|
140
|
-
|
|
141
|
-
|
|
214
|
+
_copyContext(this, tempContext);
|
|
215
|
+
_copyContext(apps[appName], this);
|
|
142
216
|
apps[this.appName] = tempContext;
|
|
143
217
|
this.appName = appName;
|
|
144
218
|
if (newContextCreated) {
|
|
145
219
|
this.registerEventListeners(this.context);
|
|
146
220
|
await this.goto(this.context.environment.baseUrl);
|
|
147
|
-
|
|
221
|
+
if (!this.fastMode) {
|
|
222
|
+
await this.waitForPageLoad();
|
|
223
|
+
}
|
|
148
224
|
}
|
|
149
225
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
226
|
+
async switchTab(tabTitleOrIndex) {
|
|
227
|
+
// first check if the tabNameOrIndex is a number
|
|
228
|
+
let index = parseInt(tabTitleOrIndex);
|
|
229
|
+
if (!isNaN(index)) {
|
|
230
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
231
|
+
this.page = this.context.pages[index];
|
|
232
|
+
this.context.page = this.page;
|
|
233
|
+
await this.page.bringToFront();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
158
236
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
237
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
238
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
239
|
+
let page = this.context.pages[i];
|
|
240
|
+
let title = await page.title();
|
|
241
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
242
|
+
this.page = page;
|
|
243
|
+
this.context.page = this.page;
|
|
244
|
+
await this.page.bringToFront();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
162
247
|
}
|
|
163
|
-
|
|
164
|
-
return path.join(logFolder, fileName);
|
|
248
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
165
249
|
}
|
|
166
250
|
registerConsoleLogListener(page, context) {
|
|
167
251
|
if (!this.context.webLogger) {
|
|
168
252
|
this.context.webLogger = [];
|
|
169
253
|
}
|
|
170
254
|
page.on("console", async (msg) => {
|
|
171
|
-
var _a;
|
|
172
255
|
const obj = {
|
|
173
256
|
type: msg.type(),
|
|
174
257
|
text: msg.text(),
|
|
@@ -176,7 +259,9 @@ class StableBrowser {
|
|
|
176
259
|
time: new Date().toISOString(),
|
|
177
260
|
};
|
|
178
261
|
this.context.webLogger.push(obj);
|
|
179
|
-
|
|
262
|
+
if (msg.type() === "error") {
|
|
263
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
264
|
+
}
|
|
180
265
|
});
|
|
181
266
|
}
|
|
182
267
|
registerRequestListener(page, context, logFile) {
|
|
@@ -184,7 +269,6 @@ class StableBrowser {
|
|
|
184
269
|
this.context.networkLogger = [];
|
|
185
270
|
}
|
|
186
271
|
page.on("request", async (data) => {
|
|
187
|
-
var _a;
|
|
188
272
|
const startTime = new Date().getTime();
|
|
189
273
|
try {
|
|
190
274
|
const pageUrl = new URL(page.url());
|
|
@@ -209,10 +293,10 @@ class StableBrowser {
|
|
|
209
293
|
startTime,
|
|
210
294
|
};
|
|
211
295
|
context.networkLogger.push(obj);
|
|
212
|
-
|
|
296
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
213
297
|
}
|
|
214
298
|
catch (error) {
|
|
215
|
-
console.error("Error in request listener", error);
|
|
299
|
+
// console.error("Error in request listener", error);
|
|
216
300
|
context.networkLogger.push({
|
|
217
301
|
error: "not able to listen",
|
|
218
302
|
message: error.message,
|
|
@@ -226,69 +310,110 @@ class StableBrowser {
|
|
|
226
310
|
// async closeUnexpectedPopups() {
|
|
227
311
|
// await closeUnexpectedPopups(this.page);
|
|
228
312
|
// }
|
|
229
|
-
async goto(url) {
|
|
313
|
+
async goto(url, world = null) {
|
|
314
|
+
if (!url) {
|
|
315
|
+
throw new Error("url is null, verify that the environment file is correct");
|
|
316
|
+
}
|
|
317
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
230
318
|
if (!url.startsWith("http")) {
|
|
231
319
|
url = "https://" + url;
|
|
232
320
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
321
|
+
const state = {
|
|
322
|
+
value: url,
|
|
323
|
+
world: world,
|
|
324
|
+
type: Types.NAVIGATE,
|
|
325
|
+
text: `Navigate Page to: ${url}`,
|
|
326
|
+
operation: "goto",
|
|
327
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
328
|
+
info: {},
|
|
329
|
+
locate: false,
|
|
330
|
+
scroll: false,
|
|
331
|
+
screenshot: false,
|
|
332
|
+
highlight: false,
|
|
333
|
+
};
|
|
334
|
+
try {
|
|
335
|
+
await _preCommand(state, this);
|
|
336
|
+
await this.page.goto(url, {
|
|
337
|
+
timeout: 60000,
|
|
338
|
+
});
|
|
339
|
+
await _screenshot(state, this);
|
|
243
340
|
}
|
|
244
|
-
|
|
245
|
-
|
|
341
|
+
catch (error) {
|
|
342
|
+
console.error("Error on goto", error);
|
|
343
|
+
_commandError(state, error, this);
|
|
246
344
|
}
|
|
247
|
-
|
|
248
|
-
|
|
345
|
+
finally {
|
|
346
|
+
await _commandFinally(state, this);
|
|
249
347
|
}
|
|
250
348
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
349
|
+
async goBack(options, world = null) {
|
|
350
|
+
const state = {
|
|
351
|
+
value: "",
|
|
352
|
+
world: world,
|
|
353
|
+
type: Types.GO_BACK,
|
|
354
|
+
text: `Browser navigate back`,
|
|
355
|
+
operation: "goBack",
|
|
356
|
+
log: "***** navigate back *****\n",
|
|
357
|
+
info: {},
|
|
358
|
+
locate: false,
|
|
359
|
+
scroll: false,
|
|
360
|
+
screenshot: false,
|
|
361
|
+
highlight: false,
|
|
362
|
+
};
|
|
363
|
+
try {
|
|
364
|
+
await _preCommand(state, this);
|
|
365
|
+
await this.page.goBack({
|
|
366
|
+
waitUntil: "load",
|
|
367
|
+
});
|
|
368
|
+
await _screenshot(state, this);
|
|
254
369
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// remove the _ prefix
|
|
259
|
-
regValue = key.substring(1);
|
|
260
|
-
}
|
|
261
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
370
|
+
catch (error) {
|
|
371
|
+
console.error("Error on goBack", error);
|
|
372
|
+
_commandError(state, error, this);
|
|
262
373
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
266
|
-
// check if not null
|
|
267
|
-
if (!locator) {
|
|
268
|
-
return locator;
|
|
374
|
+
finally {
|
|
375
|
+
await _commandFinally(state, this);
|
|
269
376
|
}
|
|
270
|
-
// clone the locator
|
|
271
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
272
|
-
this.scanAndManipulate(locator, _params);
|
|
273
|
-
return locator;
|
|
274
377
|
}
|
|
275
|
-
|
|
276
|
-
|
|
378
|
+
async goForward(options, world = null) {
|
|
379
|
+
const state = {
|
|
380
|
+
value: "",
|
|
381
|
+
world: world,
|
|
382
|
+
type: Types.GO_FORWARD,
|
|
383
|
+
text: `Browser navigate forward`,
|
|
384
|
+
operation: "goForward",
|
|
385
|
+
log: "***** navigate forward *****\n",
|
|
386
|
+
info: {},
|
|
387
|
+
locate: false,
|
|
388
|
+
scroll: false,
|
|
389
|
+
screenshot: false,
|
|
390
|
+
highlight: false,
|
|
391
|
+
};
|
|
392
|
+
try {
|
|
393
|
+
await _preCommand(state, this);
|
|
394
|
+
await this.page.goForward({
|
|
395
|
+
waitUntil: "load",
|
|
396
|
+
});
|
|
397
|
+
await _screenshot(state, this);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
console.error("Error on goForward", error);
|
|
401
|
+
_commandError(state, error, this);
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
await _commandFinally(state, this);
|
|
405
|
+
}
|
|
277
406
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
407
|
+
async _getLocator(locator, scope, _params) {
|
|
408
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
409
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
410
|
+
for (let key in locator) {
|
|
411
|
+
if (typeof locator[key] !== "string")
|
|
412
|
+
continue;
|
|
413
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
414
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
287
415
|
}
|
|
288
416
|
}
|
|
289
|
-
}
|
|
290
|
-
_getLocator(locator, scope, _params) {
|
|
291
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
292
417
|
let locatorReturn;
|
|
293
418
|
if (locator.role) {
|
|
294
419
|
if (locator.role[1].nameReg) {
|
|
@@ -296,7 +421,7 @@ class StableBrowser {
|
|
|
296
421
|
delete locator.role[1].nameReg;
|
|
297
422
|
}
|
|
298
423
|
// if (locator.role[1].name) {
|
|
299
|
-
// locator.role[1].name =
|
|
424
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
300
425
|
// }
|
|
301
426
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
302
427
|
}
|
|
@@ -315,7 +440,7 @@ class StableBrowser {
|
|
|
315
440
|
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
316
441
|
}
|
|
317
442
|
}
|
|
318
|
-
if (locator
|
|
443
|
+
if (locator?.engine) {
|
|
319
444
|
if (locator.engine === "css") {
|
|
320
445
|
locatorReturn = scope.locator(locator.selector);
|
|
321
446
|
}
|
|
@@ -339,192 +464,177 @@ class StableBrowser {
|
|
|
339
464
|
if (css && css.locator) {
|
|
340
465
|
css = css.locator;
|
|
341
466
|
}
|
|
342
|
-
let result = await this._locateElementByText(scope,
|
|
467
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
343
468
|
if (result.elementCount === 0) {
|
|
344
469
|
return;
|
|
345
470
|
}
|
|
346
|
-
let textElementCss = "[data-blinq-id
|
|
471
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
347
472
|
// css climb to parent element
|
|
348
473
|
const climbArray = [];
|
|
349
474
|
for (let i = 0; i < climb; i++) {
|
|
350
475
|
climbArray.push("..");
|
|
351
476
|
}
|
|
352
477
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
// Iterate over child nodes
|
|
377
|
-
element.childNodes.forEach((child) => {
|
|
378
|
-
// Recursively call the function for each child node
|
|
379
|
-
document.collectAllShadowDomElements(child, result);
|
|
380
|
-
});
|
|
381
|
-
return result;
|
|
382
|
-
}
|
|
383
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
384
|
-
if (!tag) {
|
|
385
|
-
tag = "*";
|
|
386
|
-
}
|
|
387
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
388
|
-
let shadowHosts = [];
|
|
389
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
390
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
391
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
392
|
-
if (!shadowElement) {
|
|
393
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
397
|
-
elements = elements.concat(shadowElements);
|
|
398
|
-
}
|
|
399
|
-
let randomToken = null;
|
|
400
|
-
const foundElements = [];
|
|
401
|
-
if (regex) {
|
|
402
|
-
let regexpSearch = new RegExp(text, "im");
|
|
403
|
-
for (let i = 0; i < elements.length; i++) {
|
|
404
|
-
const element = elements[i];
|
|
405
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
406
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
407
|
-
foundElements.push(element);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
else {
|
|
412
|
-
text = text.trim();
|
|
413
|
-
for (let i = 0; i < elements.length; i++) {
|
|
414
|
-
const element = elements[i];
|
|
415
|
-
if (partial) {
|
|
416
|
-
if ((element.innerText && element.innerText.trim().includes(text)) ||
|
|
417
|
-
(element.value && element.value.includes(text))) {
|
|
418
|
-
foundElements.push(element);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
423
|
-
(element.value && element.value === text)) {
|
|
424
|
-
foundElements.push(element);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
let noChildElements = [];
|
|
430
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
431
|
-
let element = foundElements[i];
|
|
432
|
-
let hasChild = false;
|
|
433
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
434
|
-
if (i === j) {
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
if (isParent(element, foundElements[j])) {
|
|
438
|
-
hasChild = true;
|
|
439
|
-
break;
|
|
478
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
479
|
+
if (css) {
|
|
480
|
+
resultCss = resultCss + " >> " + css;
|
|
481
|
+
}
|
|
482
|
+
return resultCss;
|
|
483
|
+
}
|
|
484
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
485
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
486
|
+
const locator = scope.locator(query);
|
|
487
|
+
const count = await locator.count();
|
|
488
|
+
if (!tag1) {
|
|
489
|
+
tag1 = "*";
|
|
490
|
+
}
|
|
491
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
492
|
+
let tagCount = 0;
|
|
493
|
+
for (let i = 0; i < count; i++) {
|
|
494
|
+
const element = locator.nth(i);
|
|
495
|
+
// check if the tag matches
|
|
496
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
497
|
+
if (!tag.startsWith("*")) {
|
|
498
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
499
|
+
return false;
|
|
440
500
|
}
|
|
441
501
|
}
|
|
442
|
-
if (!
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
let elementCount = 0;
|
|
447
|
-
if (noChildElements.length > 0) {
|
|
448
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
449
|
-
if (randomToken === null) {
|
|
450
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
451
|
-
}
|
|
452
|
-
let element = noChildElements[i];
|
|
453
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
454
|
-
elementCount++;
|
|
502
|
+
if (!el.setAttribute) {
|
|
503
|
+
el = el.parentElement;
|
|
455
504
|
}
|
|
505
|
+
// remove any attributes start with data-blinq-id
|
|
506
|
+
// for (let i = 0; i < el.attributes.length; i++) {
|
|
507
|
+
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
508
|
+
// el.removeAttribute(el.attributes[i].name);
|
|
509
|
+
// }
|
|
510
|
+
// }
|
|
511
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
512
|
+
return true;
|
|
513
|
+
}, [tag1, randomToken]))) {
|
|
514
|
+
continue;
|
|
456
515
|
}
|
|
457
|
-
|
|
458
|
-
}
|
|
516
|
+
tagCount++;
|
|
517
|
+
}
|
|
518
|
+
return { elementCount: tagCount, randomToken };
|
|
459
519
|
}
|
|
460
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
520
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
521
|
+
if (!info) {
|
|
522
|
+
info = {};
|
|
523
|
+
}
|
|
524
|
+
if (!info.failCause) {
|
|
525
|
+
info.failCause = {};
|
|
526
|
+
}
|
|
527
|
+
if (!info.log) {
|
|
528
|
+
info.log = "";
|
|
529
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
530
|
+
}
|
|
461
531
|
let locatorSearch = selectorHierarchy[index];
|
|
532
|
+
try {
|
|
533
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
534
|
+
}
|
|
535
|
+
catch (e) {
|
|
536
|
+
console.error(e);
|
|
537
|
+
}
|
|
538
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
462
539
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
463
540
|
let locator = null;
|
|
464
541
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
465
|
-
|
|
542
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
543
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
466
544
|
if (!locatorString) {
|
|
545
|
+
info.failCause.textNotFound = true;
|
|
546
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
467
547
|
return;
|
|
468
548
|
}
|
|
469
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
549
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
470
550
|
}
|
|
471
551
|
else if (locatorSearch.text) {
|
|
472
|
-
let
|
|
552
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
553
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
473
554
|
if (result.elementCount === 0) {
|
|
555
|
+
info.failCause.textNotFound = true;
|
|
556
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
474
557
|
return;
|
|
475
558
|
}
|
|
476
|
-
locatorSearch.css = "[data-blinq-id
|
|
559
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
477
560
|
if (locatorSearch.childCss) {
|
|
478
561
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
479
562
|
}
|
|
480
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
563
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
481
564
|
}
|
|
482
565
|
else {
|
|
483
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
566
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
484
567
|
}
|
|
485
568
|
// let cssHref = false;
|
|
486
569
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
487
570
|
// cssHref = true;
|
|
488
571
|
// }
|
|
489
572
|
let count = await locator.count();
|
|
573
|
+
if (count > 0 && !info.failCause.count) {
|
|
574
|
+
info.failCause.count = count;
|
|
575
|
+
}
|
|
490
576
|
//info.log += "total elements found " + count + "\n";
|
|
491
577
|
//let visibleCount = 0;
|
|
492
578
|
let visibleLocator = null;
|
|
493
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
579
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
494
580
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
581
|
+
if (info.locatorLog) {
|
|
582
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
583
|
+
}
|
|
495
584
|
return;
|
|
496
585
|
}
|
|
586
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
587
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
588
|
+
}
|
|
497
589
|
for (let j = 0; j < count; j++) {
|
|
498
590
|
let visible = await locator.nth(j).isVisible();
|
|
499
591
|
const enabled = await locator.nth(j).isEnabled();
|
|
500
592
|
if (!visibleOnly) {
|
|
501
593
|
visible = true;
|
|
502
594
|
}
|
|
503
|
-
if (visible && enabled) {
|
|
595
|
+
if (visible && (allowDisabled || enabled)) {
|
|
504
596
|
foundLocators.push(locator.nth(j));
|
|
597
|
+
if (info.locatorLog) {
|
|
598
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
599
|
+
}
|
|
505
600
|
}
|
|
506
|
-
else {
|
|
601
|
+
else if (logErrors) {
|
|
602
|
+
info.failCause.visible = visible;
|
|
603
|
+
info.failCause.enabled = enabled;
|
|
507
604
|
if (!info.printMessages) {
|
|
508
605
|
info.printMessages = {};
|
|
509
606
|
}
|
|
607
|
+
if (info.locatorLog && !visible) {
|
|
608
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
609
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
610
|
+
}
|
|
611
|
+
if (info.locatorLog && !enabled) {
|
|
612
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
613
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
614
|
+
}
|
|
510
615
|
if (!info.printMessages[j.toString()]) {
|
|
511
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
616
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
512
617
|
info.printMessages[j.toString()] = true;
|
|
513
618
|
}
|
|
514
619
|
}
|
|
515
620
|
}
|
|
516
621
|
}
|
|
517
622
|
async closeUnexpectedPopups(info, _params) {
|
|
623
|
+
if (!info) {
|
|
624
|
+
info = {};
|
|
625
|
+
info.failCause = {};
|
|
626
|
+
info.log = "";
|
|
627
|
+
}
|
|
518
628
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
519
629
|
if (!info) {
|
|
520
630
|
info = {};
|
|
521
631
|
}
|
|
522
|
-
info.log += "scan for popup handlers" + "\n";
|
|
632
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
523
633
|
const handlerGroup = [];
|
|
524
634
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
525
635
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
526
636
|
}
|
|
527
|
-
const scopes =
|
|
637
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
528
638
|
let result = null;
|
|
529
639
|
let scope = null;
|
|
530
640
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -546,58 +656,108 @@ class StableBrowser {
|
|
|
546
656
|
}
|
|
547
657
|
if (result.foundElements.length > 0) {
|
|
548
658
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
659
|
+
try {
|
|
660
|
+
await scope?.evaluate(() => {
|
|
661
|
+
window.__isClosingPopups = true;
|
|
662
|
+
});
|
|
663
|
+
await dialogCloseLocator.click();
|
|
664
|
+
// wait for the dialog to close
|
|
665
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
666
|
+
}
|
|
667
|
+
catch (e) {
|
|
668
|
+
}
|
|
669
|
+
finally {
|
|
670
|
+
await scope?.evaluate(() => {
|
|
671
|
+
window.__isClosingPopups = false;
|
|
672
|
+
});
|
|
673
|
+
}
|
|
552
674
|
return { rerun: true };
|
|
553
675
|
}
|
|
554
676
|
}
|
|
555
677
|
}
|
|
556
678
|
return { rerun: false };
|
|
557
679
|
}
|
|
558
|
-
async _locate(selectors, info, _params, timeout =
|
|
680
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
681
|
+
if (!timeout) {
|
|
682
|
+
timeout = 30000;
|
|
683
|
+
}
|
|
559
684
|
for (let i = 0; i < 3; i++) {
|
|
560
685
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
561
686
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
562
687
|
let selector = selectors.locators[j];
|
|
563
688
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
564
689
|
}
|
|
565
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
690
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
566
691
|
if (!element.rerun) {
|
|
567
|
-
|
|
692
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
693
|
+
await element.evaluate((el, randomToken) => {
|
|
694
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
695
|
+
}, randomToken);
|
|
696
|
+
// if (element._frame) {
|
|
697
|
+
// return element;
|
|
698
|
+
// }
|
|
699
|
+
const scope = element._frame ?? element.page();
|
|
700
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
701
|
+
let prefixSelector = "";
|
|
702
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
703
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
704
|
+
if (frameSelectorIndex !== -1) {
|
|
705
|
+
// remove everything after the >> internal:control=enter-frame
|
|
706
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
707
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
708
|
+
}
|
|
709
|
+
// if (element?._frame?._selector) {
|
|
710
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
711
|
+
// }
|
|
712
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
713
|
+
return scope.locator(newSelector);
|
|
568
714
|
}
|
|
569
715
|
}
|
|
570
716
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
571
717
|
}
|
|
572
|
-
async
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
718
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
719
|
+
if (!info) {
|
|
720
|
+
info = {};
|
|
721
|
+
info.failCause = {};
|
|
722
|
+
info.log = "";
|
|
723
|
+
}
|
|
724
|
+
let startTime = Date.now();
|
|
578
725
|
let scope = this.page;
|
|
726
|
+
if (selectors.frame) {
|
|
727
|
+
return selectors.frame;
|
|
728
|
+
}
|
|
579
729
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
580
|
-
const findFrame = (frame, framescope) => {
|
|
730
|
+
const findFrame = async (frame, framescope) => {
|
|
581
731
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
582
732
|
let frameLocator = frame.selectors[i];
|
|
583
733
|
if (frameLocator.css) {
|
|
584
|
-
|
|
734
|
+
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
585
735
|
if (frameLocator.index) {
|
|
586
|
-
|
|
736
|
+
testframescope = framescope.nth(frameLocator.index);
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
await testframescope.owner().evaluateHandle(() => true, null, {
|
|
740
|
+
timeout: 5000,
|
|
741
|
+
});
|
|
742
|
+
framescope = testframescope;
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
// console.error("frame not found " + frameLocator.css);
|
|
587
747
|
}
|
|
588
|
-
break;
|
|
589
748
|
}
|
|
590
749
|
}
|
|
591
750
|
if (frame.children) {
|
|
592
|
-
return findFrame(frame.children, framescope);
|
|
751
|
+
return await findFrame(frame.children, framescope);
|
|
593
752
|
}
|
|
594
753
|
return framescope;
|
|
595
754
|
};
|
|
596
|
-
|
|
755
|
+
let fLocator = null;
|
|
597
756
|
while (true) {
|
|
598
757
|
let frameFound = false;
|
|
599
758
|
if (selectors.nestFrmLoc) {
|
|
600
|
-
|
|
759
|
+
fLocator = selectors.nestFrmLoc;
|
|
760
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
601
761
|
frameFound = true;
|
|
602
762
|
break;
|
|
603
763
|
}
|
|
@@ -605,6 +765,7 @@ class StableBrowser {
|
|
|
605
765
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
606
766
|
let frameLocator = selectors.frameLocators[i];
|
|
607
767
|
if (frameLocator.css) {
|
|
768
|
+
fLocator = frameLocator.css;
|
|
608
769
|
scope = scope.frameLocator(frameLocator.css);
|
|
609
770
|
frameFound = true;
|
|
610
771
|
break;
|
|
@@ -612,20 +773,54 @@ class StableBrowser {
|
|
|
612
773
|
}
|
|
613
774
|
}
|
|
614
775
|
if (!frameFound && selectors.iframe_src) {
|
|
776
|
+
fLocator = selectors.iframe_src;
|
|
615
777
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
616
778
|
}
|
|
617
779
|
if (!scope) {
|
|
618
|
-
info
|
|
619
|
-
|
|
780
|
+
if (info && info.locatorLog) {
|
|
781
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
782
|
+
}
|
|
783
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
784
|
+
if (Date.now() - startTime > timeout) {
|
|
785
|
+
info.failCause.iframeNotFound = true;
|
|
786
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
620
787
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
621
788
|
}
|
|
622
789
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
623
790
|
}
|
|
624
791
|
else {
|
|
792
|
+
if (info && info.locatorLog) {
|
|
793
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
794
|
+
}
|
|
625
795
|
break;
|
|
626
796
|
}
|
|
627
797
|
}
|
|
628
798
|
}
|
|
799
|
+
if (!scope) {
|
|
800
|
+
scope = this.page;
|
|
801
|
+
}
|
|
802
|
+
return scope;
|
|
803
|
+
}
|
|
804
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
805
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
806
|
+
return scope.evaluate(() => {
|
|
807
|
+
var bodyContent = document.body.innerHTML;
|
|
808
|
+
return bodyContent;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
812
|
+
if (!info) {
|
|
813
|
+
info = {};
|
|
814
|
+
info.failCause = {};
|
|
815
|
+
info.log = "";
|
|
816
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
817
|
+
}
|
|
818
|
+
let highPriorityTimeout = 5000;
|
|
819
|
+
let visibleOnlyTimeout = 6000;
|
|
820
|
+
let startTime = Date.now();
|
|
821
|
+
let locatorsCount = 0;
|
|
822
|
+
let lazy_scroll = false;
|
|
823
|
+
//let arrayMode = Array.isArray(selectors);
|
|
629
824
|
let selectorsLocators = null;
|
|
630
825
|
selectorsLocators = selectors.locators;
|
|
631
826
|
// group selectors by priority
|
|
@@ -653,6 +848,7 @@ class StableBrowser {
|
|
|
653
848
|
let highPriorityOnly = true;
|
|
654
849
|
let visibleOnly = true;
|
|
655
850
|
while (true) {
|
|
851
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
656
852
|
locatorsCount = 0;
|
|
657
853
|
let result = [];
|
|
658
854
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -661,18 +857,13 @@ class StableBrowser {
|
|
|
661
857
|
}
|
|
662
858
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
663
859
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
664
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
860
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
665
861
|
if (result.foundElements.length === 0) {
|
|
666
862
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
667
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
863
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
668
864
|
}
|
|
669
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
670
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
674
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
675
|
-
}
|
|
865
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
866
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
676
867
|
}
|
|
677
868
|
let foundElements = result.foundElements;
|
|
678
869
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -712,24 +903,43 @@ class StableBrowser {
|
|
|
712
903
|
return maxCountElement.locator;
|
|
713
904
|
}
|
|
714
905
|
}
|
|
715
|
-
if (
|
|
906
|
+
if (Date.now() - startTime > timeout) {
|
|
716
907
|
break;
|
|
717
908
|
}
|
|
718
|
-
if (
|
|
719
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
909
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
910
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
720
911
|
highPriorityOnly = false;
|
|
912
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
913
|
+
lazy_scroll = true;
|
|
914
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
915
|
+
}
|
|
721
916
|
}
|
|
722
|
-
if (
|
|
723
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
917
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
918
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
724
919
|
visibleOnly = false;
|
|
725
920
|
}
|
|
726
921
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
922
|
+
// sheck of more of half of the timeout has passed
|
|
923
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
924
|
+
highPriorityOnly = false;
|
|
925
|
+
visibleOnly = false;
|
|
926
|
+
}
|
|
727
927
|
}
|
|
728
928
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
729
|
-
info.
|
|
929
|
+
// if (info.locatorLog) {
|
|
930
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
931
|
+
// for (let line of lines) {
|
|
932
|
+
// this.logger.debug(line);
|
|
933
|
+
// }
|
|
934
|
+
// }
|
|
935
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
936
|
+
info.failCause.locatorNotFound = true;
|
|
937
|
+
if (!info?.failCause?.lastError) {
|
|
938
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
939
|
+
}
|
|
730
940
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
731
941
|
}
|
|
732
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
942
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
733
943
|
let foundElements = [];
|
|
734
944
|
const result = {
|
|
735
945
|
foundElements: foundElements,
|
|
@@ -737,17 +947,20 @@ class StableBrowser {
|
|
|
737
947
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
738
948
|
let foundLocators = [];
|
|
739
949
|
try {
|
|
740
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
950
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
741
951
|
}
|
|
742
952
|
catch (e) {
|
|
743
|
-
this
|
|
744
|
-
this.logger.debug(
|
|
953
|
+
// this call can fail it the browser is navigating
|
|
954
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
955
|
+
// this.logger.debug(e);
|
|
745
956
|
foundLocators = [];
|
|
746
957
|
try {
|
|
747
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
958
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
748
959
|
}
|
|
749
960
|
catch (e) {
|
|
750
|
-
|
|
961
|
+
if (logErrors) {
|
|
962
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
963
|
+
}
|
|
751
964
|
}
|
|
752
965
|
}
|
|
753
966
|
if (foundLocators.length === 1) {
|
|
@@ -758,270 +971,350 @@ class StableBrowser {
|
|
|
758
971
|
});
|
|
759
972
|
result.locatorIndex = i;
|
|
760
973
|
}
|
|
974
|
+
if (foundLocators.length > 1) {
|
|
975
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
976
|
+
const boxes = [];
|
|
977
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
978
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
979
|
+
}
|
|
980
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
981
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
982
|
+
if (j === k) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
986
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
987
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
988
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
989
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
990
|
+
// as the element is not unique, will remove it
|
|
991
|
+
boxes.splice(k, 1);
|
|
992
|
+
k--;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (boxes.length === 1) {
|
|
997
|
+
result.foundElements.push({
|
|
998
|
+
locator: boxes[0].locator.first(),
|
|
999
|
+
box: boxes[0].box,
|
|
1000
|
+
unique: true,
|
|
1001
|
+
});
|
|
1002
|
+
result.locatorIndex = i;
|
|
1003
|
+
}
|
|
1004
|
+
else if (logErrors) {
|
|
1005
|
+
info.failCause.foundMultiple = true;
|
|
1006
|
+
if (info.locatorLog) {
|
|
1007
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
761
1011
|
}
|
|
762
1012
|
return result;
|
|
763
1013
|
}
|
|
764
|
-
async
|
|
765
|
-
|
|
1014
|
+
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
1015
|
+
const state = {
|
|
1016
|
+
locate: false,
|
|
1017
|
+
scroll: false,
|
|
1018
|
+
highlight: false,
|
|
1019
|
+
_params,
|
|
1020
|
+
options,
|
|
1021
|
+
world,
|
|
1022
|
+
type: Types.CLICK,
|
|
1023
|
+
text: "Click element",
|
|
1024
|
+
operation: "simpleClick",
|
|
1025
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
1026
|
+
};
|
|
1027
|
+
_preCommand(state, this);
|
|
766
1028
|
const startTime = Date.now();
|
|
767
|
-
|
|
768
|
-
|
|
1029
|
+
let timeout = 30000;
|
|
1030
|
+
if (options && options.timeout) {
|
|
1031
|
+
timeout = options.timeout;
|
|
769
1032
|
}
|
|
770
|
-
|
|
771
|
-
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
772
|
-
info.operation = "click";
|
|
773
|
-
info.selectors = selectors;
|
|
774
|
-
let error = null;
|
|
775
|
-
let screenshotId = null;
|
|
776
|
-
let screenshotPath = null;
|
|
777
|
-
try {
|
|
778
|
-
let element = await this._locate(selectors, info, _params);
|
|
779
|
-
await this.scrollIfNeeded(element, info);
|
|
780
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1033
|
+
while (true) {
|
|
781
1034
|
try {
|
|
782
|
-
await this.
|
|
783
|
-
|
|
784
|
-
|
|
1035
|
+
const result = await locate_element(this.context, elementDescription, "click");
|
|
1036
|
+
if (result?.elementNumber >= 0) {
|
|
1037
|
+
const selectors = {
|
|
1038
|
+
frame: result?.frame,
|
|
1039
|
+
locators: [
|
|
1040
|
+
{
|
|
1041
|
+
css: result?.css,
|
|
1042
|
+
},
|
|
1043
|
+
],
|
|
1044
|
+
};
|
|
1045
|
+
await this.click(selectors, _params, options, world);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
785
1048
|
}
|
|
786
1049
|
catch (e) {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1050
|
+
if (performance.now() - startTime > timeout) {
|
|
1051
|
+
// throw e;
|
|
1052
|
+
try {
|
|
1053
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1054
|
+
}
|
|
1055
|
+
finally {
|
|
1056
|
+
await _commandFinally(state, this);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
792
1059
|
}
|
|
793
|
-
await
|
|
794
|
-
|
|
1060
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
1064
|
+
const state = {
|
|
1065
|
+
locate: false,
|
|
1066
|
+
scroll: false,
|
|
1067
|
+
highlight: false,
|
|
1068
|
+
_params,
|
|
1069
|
+
options,
|
|
1070
|
+
world,
|
|
1071
|
+
type: Types.FILL,
|
|
1072
|
+
text: "Fill element",
|
|
1073
|
+
operation: "simpleClickType",
|
|
1074
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
1075
|
+
};
|
|
1076
|
+
_preCommand(state, this);
|
|
1077
|
+
const startTime = Date.now();
|
|
1078
|
+
let timeout = 30000;
|
|
1079
|
+
if (options && options.timeout) {
|
|
1080
|
+
timeout = options.timeout;
|
|
1081
|
+
}
|
|
1082
|
+
while (true) {
|
|
1083
|
+
try {
|
|
1084
|
+
const result = await locate_element(this.context, elementDescription, "fill", value);
|
|
1085
|
+
if (result?.elementNumber >= 0) {
|
|
1086
|
+
const selectors = {
|
|
1087
|
+
frame: result?.frame,
|
|
1088
|
+
locators: [
|
|
1089
|
+
{
|
|
1090
|
+
css: result?.css,
|
|
1091
|
+
},
|
|
1092
|
+
],
|
|
1093
|
+
};
|
|
1094
|
+
await this.clickType(selectors, value, false, _params, options, world);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
catch (e) {
|
|
1099
|
+
if (performance.now() - startTime > timeout) {
|
|
1100
|
+
// throw e;
|
|
1101
|
+
try {
|
|
1102
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1103
|
+
}
|
|
1104
|
+
finally {
|
|
1105
|
+
await _commandFinally(state, this);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async click(selectors, _params, options = {}, world = null) {
|
|
1113
|
+
const state = {
|
|
1114
|
+
selectors,
|
|
1115
|
+
_params,
|
|
1116
|
+
options,
|
|
1117
|
+
world,
|
|
1118
|
+
text: "Click element",
|
|
1119
|
+
_text: "Click on " + selectors.element_name,
|
|
1120
|
+
type: Types.CLICK,
|
|
1121
|
+
operation: "click",
|
|
1122
|
+
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1123
|
+
};
|
|
1124
|
+
try {
|
|
1125
|
+
await _preCommand(state, this);
|
|
1126
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1127
|
+
if (!this.fastMode) {
|
|
1128
|
+
await this.waitForPageLoad();
|
|
1129
|
+
}
|
|
1130
|
+
return state.info;
|
|
795
1131
|
}
|
|
796
1132
|
catch (e) {
|
|
797
|
-
|
|
798
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
799
|
-
info.screenshotPath = screenshotPath;
|
|
800
|
-
Object.assign(e, { info: info });
|
|
801
|
-
error = e;
|
|
802
|
-
throw e;
|
|
1133
|
+
await _commandError(state, e, this);
|
|
803
1134
|
}
|
|
804
1135
|
finally {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1136
|
+
await _commandFinally(state, this);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1140
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1141
|
+
const state = {
|
|
1142
|
+
selectors,
|
|
1143
|
+
_params,
|
|
1144
|
+
options,
|
|
1145
|
+
world,
|
|
1146
|
+
text: "Wait for element",
|
|
1147
|
+
_text: "Wait for " + selectors.element_name,
|
|
1148
|
+
type: Types.WAIT_ELEMENT,
|
|
1149
|
+
operation: "waitForElement",
|
|
1150
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1151
|
+
};
|
|
1152
|
+
let found = false;
|
|
1153
|
+
try {
|
|
1154
|
+
await _preCommand(state, this);
|
|
1155
|
+
// if (state.options && state.options.context) {
|
|
1156
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1157
|
+
// }
|
|
1158
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1159
|
+
found = true;
|
|
1160
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
825
1161
|
}
|
|
1162
|
+
catch (e) {
|
|
1163
|
+
console.error("Error on waitForElement", e);
|
|
1164
|
+
// await _commandError(state, e, this);
|
|
1165
|
+
}
|
|
1166
|
+
finally {
|
|
1167
|
+
await _commandFinally(state, this);
|
|
1168
|
+
}
|
|
1169
|
+
return found;
|
|
826
1170
|
}
|
|
827
1171
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1172
|
+
const state = {
|
|
1173
|
+
selectors,
|
|
1174
|
+
_params,
|
|
1175
|
+
options,
|
|
1176
|
+
world,
|
|
1177
|
+
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
1178
|
+
text: checked ? `Check element` : `Uncheck element`,
|
|
1179
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
1180
|
+
operation: "setCheck",
|
|
1181
|
+
log: "***** check " + selectors.element_name + " *****\n",
|
|
1182
|
+
};
|
|
838
1183
|
try {
|
|
839
|
-
|
|
840
|
-
|
|
1184
|
+
await _preCommand(state, this);
|
|
1185
|
+
state.info.checked = checked;
|
|
1186
|
+
// let element = await this._locate(selectors, info, _params);
|
|
1187
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
841
1188
|
try {
|
|
842
|
-
|
|
843
|
-
|
|
1189
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1190
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1191
|
+
await this._highlightElements(state.element);
|
|
1192
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
844
1193
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1194
|
+
// await this._unHighlightElements(element);
|
|
1195
|
+
// }
|
|
1196
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1197
|
+
// await this._unHighlightElements(element);
|
|
845
1198
|
}
|
|
846
1199
|
catch (e) {
|
|
847
1200
|
if (e.message && e.message.includes("did not change its state")) {
|
|
848
1201
|
this.logger.info("element did not change its state, ignoring...");
|
|
849
1202
|
}
|
|
850
1203
|
else {
|
|
851
|
-
//await this.closeUnexpectedPopups();
|
|
852
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
853
|
-
element = await this._locate(selectors, info, _params);
|
|
854
|
-
await element.setChecked(checked, { timeout: 5000, force: true });
|
|
855
1204
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1205
|
+
//await this.closeUnexpectedPopups();
|
|
1206
|
+
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1207
|
+
state.element_found = false;
|
|
1208
|
+
try {
|
|
1209
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1210
|
+
state.element_found = true;
|
|
1211
|
+
// check the check state
|
|
1212
|
+
}
|
|
1213
|
+
catch (error) {
|
|
1214
|
+
// element dismissed
|
|
1215
|
+
}
|
|
1216
|
+
if (state.element_found) {
|
|
1217
|
+
const isChecked = await state.element.isChecked();
|
|
1218
|
+
if (isChecked !== checked) {
|
|
1219
|
+
// perform click
|
|
1220
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
856
1226
|
}
|
|
857
1227
|
}
|
|
858
1228
|
await this.waitForPageLoad();
|
|
859
|
-
return info;
|
|
1229
|
+
return state.info;
|
|
860
1230
|
}
|
|
861
1231
|
catch (e) {
|
|
862
|
-
|
|
863
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
864
|
-
info.screenshotPath = screenshotPath;
|
|
865
|
-
Object.assign(e, { info: info });
|
|
866
|
-
error = e;
|
|
867
|
-
throw e;
|
|
1232
|
+
await _commandError(state, e, this);
|
|
868
1233
|
}
|
|
869
1234
|
finally {
|
|
870
|
-
|
|
871
|
-
this._reportToWorld(world, {
|
|
872
|
-
element_name: selectors.element_name,
|
|
873
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
874
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
875
|
-
screenshotId,
|
|
876
|
-
result: error
|
|
877
|
-
? {
|
|
878
|
-
status: "FAILED",
|
|
879
|
-
startTime,
|
|
880
|
-
endTime,
|
|
881
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
882
|
-
}
|
|
883
|
-
: {
|
|
884
|
-
status: "PASSED",
|
|
885
|
-
startTime,
|
|
886
|
-
endTime,
|
|
887
|
-
},
|
|
888
|
-
info: info,
|
|
889
|
-
});
|
|
1235
|
+
await _commandFinally(state, this);
|
|
890
1236
|
}
|
|
891
1237
|
}
|
|
892
1238
|
async hover(selectors, _params, options = {}, world = null) {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1239
|
+
const state = {
|
|
1240
|
+
selectors,
|
|
1241
|
+
_params,
|
|
1242
|
+
options,
|
|
1243
|
+
world,
|
|
1244
|
+
type: Types.HOVER,
|
|
1245
|
+
text: `Hover element`,
|
|
1246
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1247
|
+
operation: "hover",
|
|
1248
|
+
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1249
|
+
};
|
|
902
1250
|
try {
|
|
903
|
-
|
|
904
|
-
(
|
|
905
|
-
|
|
906
|
-
await this._highlightElements(element);
|
|
907
|
-
await element.hover();
|
|
908
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
909
|
-
}
|
|
910
|
-
catch (e) {
|
|
911
|
-
//await this.closeUnexpectedPopups();
|
|
912
|
-
info.log += "hover failed, will try again" + "\n";
|
|
913
|
-
element = await this._locate(selectors, info, _params);
|
|
914
|
-
await element.hover({ timeout: 10000 });
|
|
915
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
916
|
-
}
|
|
1251
|
+
await _preCommand(state, this);
|
|
1252
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1253
|
+
await _screenshot(state, this);
|
|
917
1254
|
await this.waitForPageLoad();
|
|
918
|
-
return info;
|
|
1255
|
+
return state.info;
|
|
919
1256
|
}
|
|
920
1257
|
catch (e) {
|
|
921
|
-
|
|
922
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
923
|
-
info.screenshotPath = screenshotPath;
|
|
924
|
-
Object.assign(e, { info: info });
|
|
925
|
-
error = e;
|
|
926
|
-
throw e;
|
|
1258
|
+
await _commandError(state, e, this);
|
|
927
1259
|
}
|
|
928
1260
|
finally {
|
|
929
|
-
|
|
930
|
-
this._reportToWorld(world, {
|
|
931
|
-
element_name: selectors.element_name,
|
|
932
|
-
type: Types.HOVER,
|
|
933
|
-
text: `Hover element`,
|
|
934
|
-
screenshotId,
|
|
935
|
-
result: error
|
|
936
|
-
? {
|
|
937
|
-
status: "FAILED",
|
|
938
|
-
startTime,
|
|
939
|
-
endTime,
|
|
940
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
941
|
-
}
|
|
942
|
-
: {
|
|
943
|
-
status: "PASSED",
|
|
944
|
-
startTime,
|
|
945
|
-
endTime,
|
|
946
|
-
},
|
|
947
|
-
info: info,
|
|
948
|
-
});
|
|
1261
|
+
await _commandFinally(state, this);
|
|
949
1262
|
}
|
|
950
1263
|
}
|
|
951
1264
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
952
|
-
this._validateSelectors(selectors);
|
|
953
1265
|
if (!values) {
|
|
954
1266
|
throw new Error("values is null");
|
|
955
1267
|
}
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1268
|
+
const state = {
|
|
1269
|
+
selectors,
|
|
1270
|
+
_params,
|
|
1271
|
+
options,
|
|
1272
|
+
world,
|
|
1273
|
+
value: values.toString(),
|
|
1274
|
+
type: Types.SELECT,
|
|
1275
|
+
text: `Select option: ${values}`,
|
|
1276
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1277
|
+
operation: "selectOption",
|
|
1278
|
+
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1279
|
+
};
|
|
964
1280
|
try {
|
|
965
|
-
|
|
966
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1281
|
+
await _preCommand(state, this);
|
|
967
1282
|
try {
|
|
968
|
-
await
|
|
969
|
-
await element.selectOption(values);
|
|
1283
|
+
await state.element.selectOption(values);
|
|
970
1284
|
}
|
|
971
1285
|
catch (e) {
|
|
972
1286
|
//await this.closeUnexpectedPopups();
|
|
973
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
974
|
-
await element.selectOption(values, { timeout: 10000, force: true });
|
|
1287
|
+
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1288
|
+
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
975
1289
|
}
|
|
976
1290
|
await this.waitForPageLoad();
|
|
977
|
-
return info;
|
|
1291
|
+
return state.info;
|
|
978
1292
|
}
|
|
979
1293
|
catch (e) {
|
|
980
|
-
|
|
981
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
982
|
-
info.screenshotPath = screenshotPath;
|
|
983
|
-
Object.assign(e, { info: info });
|
|
984
|
-
this.logger.info("click failed, will try next selector");
|
|
985
|
-
error = e;
|
|
986
|
-
throw e;
|
|
1294
|
+
await _commandError(state, e, this);
|
|
987
1295
|
}
|
|
988
1296
|
finally {
|
|
989
|
-
|
|
990
|
-
this._reportToWorld(world, {
|
|
991
|
-
element_name: selectors.element_name,
|
|
992
|
-
type: Types.SELECT,
|
|
993
|
-
text: `Select option: ${values}`,
|
|
994
|
-
value: values.toString(),
|
|
995
|
-
screenshotId,
|
|
996
|
-
result: error
|
|
997
|
-
? {
|
|
998
|
-
status: "FAILED",
|
|
999
|
-
startTime,
|
|
1000
|
-
endTime,
|
|
1001
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1002
|
-
}
|
|
1003
|
-
: {
|
|
1004
|
-
status: "PASSED",
|
|
1005
|
-
startTime,
|
|
1006
|
-
endTime,
|
|
1007
|
-
},
|
|
1008
|
-
info: info,
|
|
1009
|
-
});
|
|
1297
|
+
await _commandFinally(state, this);
|
|
1010
1298
|
}
|
|
1011
1299
|
}
|
|
1012
1300
|
async type(_value, _params = null, options = {}, world = null) {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1301
|
+
const state = {
|
|
1302
|
+
value: _value,
|
|
1303
|
+
_params,
|
|
1304
|
+
options,
|
|
1305
|
+
world,
|
|
1306
|
+
locate: false,
|
|
1307
|
+
scroll: false,
|
|
1308
|
+
highlight: false,
|
|
1309
|
+
type: Types.TYPE_PRESS,
|
|
1310
|
+
text: `Type value: ${_value}`,
|
|
1311
|
+
_text: `Type value: ${_value}`,
|
|
1312
|
+
operation: "type",
|
|
1313
|
+
log: "",
|
|
1314
|
+
};
|
|
1022
1315
|
try {
|
|
1023
|
-
|
|
1024
|
-
const valueSegment =
|
|
1316
|
+
await _preCommand(state, this);
|
|
1317
|
+
const valueSegment = state.value.split("&&");
|
|
1025
1318
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1026
1319
|
if (i > 0) {
|
|
1027
1320
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1041,134 +1334,77 @@ class StableBrowser {
|
|
|
1041
1334
|
await this.page.keyboard.type(value);
|
|
1042
1335
|
}
|
|
1043
1336
|
}
|
|
1044
|
-
return info;
|
|
1337
|
+
return state.info;
|
|
1045
1338
|
}
|
|
1046
1339
|
catch (e) {
|
|
1047
|
-
|
|
1048
|
-
this.logger.error("type failed " + info.log);
|
|
1049
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1050
|
-
info.screenshotPath = screenshotPath;
|
|
1051
|
-
Object.assign(e, { info: info });
|
|
1052
|
-
error = e;
|
|
1053
|
-
throw e;
|
|
1340
|
+
await _commandError(state, e, this);
|
|
1054
1341
|
}
|
|
1055
1342
|
finally {
|
|
1056
|
-
|
|
1057
|
-
this._reportToWorld(world, {
|
|
1058
|
-
type: Types.TYPE_PRESS,
|
|
1059
|
-
screenshotId,
|
|
1060
|
-
value: _value,
|
|
1061
|
-
text: `type value: ${_value}`,
|
|
1062
|
-
result: error
|
|
1063
|
-
? {
|
|
1064
|
-
status: "FAILED",
|
|
1065
|
-
startTime,
|
|
1066
|
-
endTime,
|
|
1067
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1068
|
-
}
|
|
1069
|
-
: {
|
|
1070
|
-
status: "PASSED",
|
|
1071
|
-
startTime,
|
|
1072
|
-
endTime,
|
|
1073
|
-
},
|
|
1074
|
-
info: info,
|
|
1075
|
-
});
|
|
1343
|
+
await _commandFinally(state, this);
|
|
1076
1344
|
}
|
|
1077
1345
|
}
|
|
1078
1346
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
let screenshotPath = null;
|
|
1347
|
+
const state = {
|
|
1348
|
+
selectors,
|
|
1349
|
+
_params,
|
|
1350
|
+
value,
|
|
1351
|
+
options,
|
|
1352
|
+
world,
|
|
1353
|
+
type: Types.SET_INPUT,
|
|
1354
|
+
text: `Set input value`,
|
|
1355
|
+
operation: "setInputValue",
|
|
1356
|
+
log: "***** set input value " + selectors.element_name + " *****\n",
|
|
1357
|
+
};
|
|
1091
1358
|
try {
|
|
1092
|
-
|
|
1093
|
-
let
|
|
1094
|
-
await this.scrollIfNeeded(element, info);
|
|
1095
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1096
|
-
await this._highlightElements(element);
|
|
1359
|
+
await _preCommand(state, this);
|
|
1360
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1097
1361
|
try {
|
|
1098
|
-
await element.evaluateHandle((el, value) => {
|
|
1362
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1099
1363
|
el.value = value;
|
|
1100
1364
|
}, value);
|
|
1101
1365
|
}
|
|
1102
1366
|
catch (error) {
|
|
1103
1367
|
this.logger.error("setInputValue failed, will try again");
|
|
1104
|
-
|
|
1105
|
-
info.
|
|
1106
|
-
|
|
1107
|
-
await element.evaluateHandle((el, value) => {
|
|
1368
|
+
await _screenshot(state, this);
|
|
1369
|
+
Object.assign(error, { info: state.info });
|
|
1370
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1108
1371
|
el.value = value;
|
|
1109
1372
|
});
|
|
1110
1373
|
}
|
|
1111
1374
|
}
|
|
1112
1375
|
catch (e) {
|
|
1113
|
-
|
|
1114
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1115
|
-
info.screenshotPath = screenshotPath;
|
|
1116
|
-
Object.assign(e, { info: info });
|
|
1117
|
-
error = e;
|
|
1118
|
-
throw e;
|
|
1376
|
+
await _commandError(state, e, this);
|
|
1119
1377
|
}
|
|
1120
1378
|
finally {
|
|
1121
|
-
|
|
1122
|
-
this._reportToWorld(world, {
|
|
1123
|
-
element_name: selectors.element_name,
|
|
1124
|
-
type: Types.SET_INPUT,
|
|
1125
|
-
text: `Set input value`,
|
|
1126
|
-
value: value,
|
|
1127
|
-
screenshotId,
|
|
1128
|
-
result: error
|
|
1129
|
-
? {
|
|
1130
|
-
status: "FAILED",
|
|
1131
|
-
startTime,
|
|
1132
|
-
endTime,
|
|
1133
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1134
|
-
}
|
|
1135
|
-
: {
|
|
1136
|
-
status: "PASSED",
|
|
1137
|
-
startTime,
|
|
1138
|
-
endTime,
|
|
1139
|
-
},
|
|
1140
|
-
info: info,
|
|
1141
|
-
});
|
|
1379
|
+
await _commandFinally(state, this);
|
|
1142
1380
|
}
|
|
1143
1381
|
}
|
|
1144
1382
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1383
|
+
const state = {
|
|
1384
|
+
selectors,
|
|
1385
|
+
_params,
|
|
1386
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1387
|
+
options,
|
|
1388
|
+
world,
|
|
1389
|
+
type: Types.SET_DATE_TIME,
|
|
1390
|
+
text: `Set date time value: ${value}`,
|
|
1391
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1392
|
+
operation: "setDateTime",
|
|
1393
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1394
|
+
throwError: false,
|
|
1395
|
+
};
|
|
1155
1396
|
try {
|
|
1156
|
-
|
|
1157
|
-
let element = await this._locate(selectors, info, _params);
|
|
1158
|
-
//insert red border around the element
|
|
1159
|
-
await this.scrollIfNeeded(element, info);
|
|
1160
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1161
|
-
await this._highlightElements(element);
|
|
1397
|
+
await _preCommand(state, this);
|
|
1162
1398
|
try {
|
|
1163
|
-
await element
|
|
1399
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1164
1400
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1165
1401
|
if (format) {
|
|
1166
|
-
value = dayjs(value).format(format);
|
|
1167
|
-
await element.fill(value);
|
|
1402
|
+
state.value = dayjs(state.value).format(format);
|
|
1403
|
+
await state.element.fill(state.value);
|
|
1168
1404
|
}
|
|
1169
1405
|
else {
|
|
1170
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1171
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1406
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1407
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1172
1408
|
el.value = ""; // clear input
|
|
1173
1409
|
el.value = dateTimeValue;
|
|
1174
1410
|
}, dateTimeValue);
|
|
@@ -1181,20 +1417,19 @@ class StableBrowser {
|
|
|
1181
1417
|
}
|
|
1182
1418
|
catch (err) {
|
|
1183
1419
|
//await this.closeUnexpectedPopups();
|
|
1184
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1420
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1185
1421
|
this.logger.info("Trying again");
|
|
1186
|
-
|
|
1187
|
-
info.
|
|
1188
|
-
Object.assign(err, { info: info });
|
|
1422
|
+
await _screenshot(state, this);
|
|
1423
|
+
Object.assign(err, { info: state.info });
|
|
1189
1424
|
await element.click();
|
|
1190
1425
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1191
1426
|
if (format) {
|
|
1192
|
-
value = dayjs(value).format(format);
|
|
1193
|
-
await element.fill(value);
|
|
1427
|
+
state.value = dayjs(state.value).format(format);
|
|
1428
|
+
await state.element.fill(state.value);
|
|
1194
1429
|
}
|
|
1195
1430
|
else {
|
|
1196
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1197
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1431
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1432
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1198
1433
|
el.value = ""; // clear input
|
|
1199
1434
|
el.value = dateTimeValue;
|
|
1200
1435
|
}, dateTimeValue);
|
|
@@ -1207,85 +1442,63 @@ class StableBrowser {
|
|
|
1207
1442
|
}
|
|
1208
1443
|
}
|
|
1209
1444
|
catch (e) {
|
|
1210
|
-
|
|
1211
|
-
throw e;
|
|
1445
|
+
await _commandError(state, e, this);
|
|
1212
1446
|
}
|
|
1213
1447
|
finally {
|
|
1214
|
-
|
|
1215
|
-
this._reportToWorld(world, {
|
|
1216
|
-
element_name: selectors.element_name,
|
|
1217
|
-
type: Types.SET_DATE_TIME,
|
|
1218
|
-
screenshotId,
|
|
1219
|
-
value: value,
|
|
1220
|
-
text: `setDateTime input with value: ${value}`,
|
|
1221
|
-
result: error
|
|
1222
|
-
? {
|
|
1223
|
-
status: "FAILED",
|
|
1224
|
-
startTime,
|
|
1225
|
-
endTime,
|
|
1226
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1227
|
-
}
|
|
1228
|
-
: {
|
|
1229
|
-
status: "PASSED",
|
|
1230
|
-
startTime,
|
|
1231
|
-
endTime,
|
|
1232
|
-
},
|
|
1233
|
-
info: info,
|
|
1234
|
-
});
|
|
1448
|
+
await _commandFinally(state, this);
|
|
1235
1449
|
}
|
|
1236
1450
|
}
|
|
1237
1451
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1238
1452
|
_value = unEscapeString(_value);
|
|
1239
|
-
this._validateSelectors(selectors);
|
|
1240
|
-
const startTime = Date.now();
|
|
1241
|
-
let error = null;
|
|
1242
|
-
let screenshotId = null;
|
|
1243
|
-
let screenshotPath = null;
|
|
1244
|
-
const info = {};
|
|
1245
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1246
|
-
info.operation = "clickType";
|
|
1247
|
-
info.selectors = selectors;
|
|
1248
1453
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1454
|
+
const state = {
|
|
1455
|
+
selectors,
|
|
1456
|
+
_params,
|
|
1457
|
+
value: newValue,
|
|
1458
|
+
originalValue: _value,
|
|
1459
|
+
options,
|
|
1460
|
+
world,
|
|
1461
|
+
type: Types.FILL,
|
|
1462
|
+
text: `Click type input with value: ${_value}`,
|
|
1463
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1464
|
+
operation: "clickType",
|
|
1465
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1466
|
+
};
|
|
1467
|
+
if (!options) {
|
|
1468
|
+
options = {};
|
|
1469
|
+
}
|
|
1249
1470
|
if (newValue !== _value) {
|
|
1250
1471
|
//this.logger.info(_value + "=" + newValue);
|
|
1251
1472
|
_value = newValue;
|
|
1252
1473
|
}
|
|
1253
|
-
info.value = _value;
|
|
1254
1474
|
try {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1259
|
-
await this._highlightElements(element);
|
|
1260
|
-
if (options === null || options === undefined || !options.press) {
|
|
1475
|
+
await _preCommand(state, this);
|
|
1476
|
+
state.info.value = _value;
|
|
1477
|
+
if (!options.press) {
|
|
1261
1478
|
try {
|
|
1262
|
-
let currentValue = await element.inputValue();
|
|
1479
|
+
let currentValue = await state.element.inputValue();
|
|
1263
1480
|
if (currentValue) {
|
|
1264
|
-
await element.fill("");
|
|
1481
|
+
await state.element.fill("");
|
|
1265
1482
|
}
|
|
1266
1483
|
}
|
|
1267
1484
|
catch (e) {
|
|
1268
1485
|
this.logger.info("unable to clear input value");
|
|
1269
1486
|
}
|
|
1270
1487
|
}
|
|
1271
|
-
if (options
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
catch (e) {
|
|
1276
|
-
await element.dispatchEvent("click");
|
|
1277
|
-
}
|
|
1488
|
+
if (options.press) {
|
|
1489
|
+
options.timeout = 5000;
|
|
1490
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1278
1491
|
}
|
|
1279
1492
|
else {
|
|
1280
1493
|
try {
|
|
1281
|
-
await element.focus();
|
|
1494
|
+
await state.element.focus();
|
|
1282
1495
|
}
|
|
1283
1496
|
catch (e) {
|
|
1284
|
-
await element.dispatchEvent("focus");
|
|
1497
|
+
await state.element.dispatchEvent("focus");
|
|
1285
1498
|
}
|
|
1286
1499
|
}
|
|
1287
1500
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1288
|
-
const valueSegment =
|
|
1501
|
+
const valueSegment = state.value.split("&&");
|
|
1289
1502
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1290
1503
|
if (i > 0) {
|
|
1291
1504
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1305,13 +1518,21 @@ class StableBrowser {
|
|
|
1305
1518
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1306
1519
|
}
|
|
1307
1520
|
}
|
|
1521
|
+
//if (!this.fastMode) {
|
|
1522
|
+
await _screenshot(state, this);
|
|
1523
|
+
//}
|
|
1308
1524
|
if (enter === true) {
|
|
1309
1525
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1310
1526
|
await this.page.keyboard.press("Enter");
|
|
1311
1527
|
await this.waitForPageLoad();
|
|
1312
1528
|
}
|
|
1313
1529
|
else if (enter === false) {
|
|
1314
|
-
|
|
1530
|
+
try {
|
|
1531
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1532
|
+
}
|
|
1533
|
+
catch (e) {
|
|
1534
|
+
// ignore
|
|
1535
|
+
}
|
|
1315
1536
|
//await this.page.keyboard.press("Tab");
|
|
1316
1537
|
}
|
|
1317
1538
|
else {
|
|
@@ -1320,112 +1541,95 @@ class StableBrowser {
|
|
|
1320
1541
|
await this.waitForPageLoad();
|
|
1321
1542
|
}
|
|
1322
1543
|
}
|
|
1323
|
-
return info;
|
|
1544
|
+
return state.info;
|
|
1324
1545
|
}
|
|
1325
1546
|
catch (e) {
|
|
1326
|
-
|
|
1327
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1328
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1329
|
-
info.screenshotPath = screenshotPath;
|
|
1330
|
-
Object.assign(e, { info: info });
|
|
1331
|
-
error = e;
|
|
1332
|
-
throw e;
|
|
1547
|
+
await _commandError(state, e, this);
|
|
1333
1548
|
}
|
|
1334
1549
|
finally {
|
|
1335
|
-
|
|
1336
|
-
this._reportToWorld(world, {
|
|
1337
|
-
element_name: selectors.element_name,
|
|
1338
|
-
type: Types.FILL,
|
|
1339
|
-
screenshotId,
|
|
1340
|
-
value: _value,
|
|
1341
|
-
text: `clickType input with value: ${_value}`,
|
|
1342
|
-
result: error
|
|
1343
|
-
? {
|
|
1344
|
-
status: "FAILED",
|
|
1345
|
-
startTime,
|
|
1346
|
-
endTime,
|
|
1347
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1348
|
-
}
|
|
1349
|
-
: {
|
|
1350
|
-
status: "PASSED",
|
|
1351
|
-
startTime,
|
|
1352
|
-
endTime,
|
|
1353
|
-
},
|
|
1354
|
-
info: info,
|
|
1355
|
-
});
|
|
1550
|
+
await _commandFinally(state, this);
|
|
1356
1551
|
}
|
|
1357
1552
|
}
|
|
1358
1553
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1554
|
+
const state = {
|
|
1555
|
+
selectors,
|
|
1556
|
+
_params,
|
|
1557
|
+
value: unEscapeString(value),
|
|
1558
|
+
options,
|
|
1559
|
+
world,
|
|
1560
|
+
type: Types.FILL,
|
|
1561
|
+
text: `Fill input with value: ${value}`,
|
|
1562
|
+
operation: "fill",
|
|
1563
|
+
log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
|
|
1564
|
+
};
|
|
1370
1565
|
try {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
await
|
|
1374
|
-
await element.fill(value);
|
|
1375
|
-
await element.dispatchEvent("change");
|
|
1566
|
+
await _preCommand(state, this);
|
|
1567
|
+
await state.element.fill(value);
|
|
1568
|
+
await state.element.dispatchEvent("change");
|
|
1376
1569
|
if (enter) {
|
|
1377
1570
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1378
1571
|
await this.page.keyboard.press("Enter");
|
|
1379
1572
|
}
|
|
1380
1573
|
await this.waitForPageLoad();
|
|
1381
|
-
return info;
|
|
1574
|
+
return state.info;
|
|
1382
1575
|
}
|
|
1383
1576
|
catch (e) {
|
|
1384
|
-
|
|
1385
|
-
this.logger.error("fill failed " + info.log);
|
|
1386
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1387
|
-
info.screenshotPath = screenshotPath;
|
|
1388
|
-
Object.assign(e, { info: info });
|
|
1389
|
-
error = e;
|
|
1390
|
-
throw e;
|
|
1577
|
+
await _commandError(state, e, this);
|
|
1391
1578
|
}
|
|
1392
1579
|
finally {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1580
|
+
await _commandFinally(state, this);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1584
|
+
const state = {
|
|
1585
|
+
selectors,
|
|
1586
|
+
_params,
|
|
1587
|
+
files,
|
|
1588
|
+
value: '"' + files.join('", "') + '"',
|
|
1589
|
+
options,
|
|
1590
|
+
world,
|
|
1591
|
+
type: Types.SET_INPUT_FILES,
|
|
1592
|
+
text: `Set input files`,
|
|
1593
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1594
|
+
operation: "setInputFiles",
|
|
1595
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1596
|
+
};
|
|
1597
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1598
|
+
try {
|
|
1599
|
+
await _preCommand(state, this);
|
|
1600
|
+
for (let i = 0; i < files.length; i++) {
|
|
1601
|
+
const file = files[i];
|
|
1602
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1603
|
+
if (!fs.existsSync(filePath)) {
|
|
1604
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1605
|
+
}
|
|
1606
|
+
state.files[i] = filePath;
|
|
1607
|
+
}
|
|
1608
|
+
await state.element.setInputFiles(files);
|
|
1609
|
+
return state.info;
|
|
1610
|
+
}
|
|
1611
|
+
catch (e) {
|
|
1612
|
+
await _commandError(state, e, this);
|
|
1613
|
+
}
|
|
1614
|
+
finally {
|
|
1615
|
+
await _commandFinally(state, this);
|
|
1414
1616
|
}
|
|
1415
1617
|
}
|
|
1416
1618
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1417
1619
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1418
1620
|
}
|
|
1419
1621
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1420
|
-
this.
|
|
1622
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1623
|
+
_validateSelectors(selectors);
|
|
1421
1624
|
let screenshotId = null;
|
|
1422
1625
|
let screenshotPath = null;
|
|
1423
1626
|
if (!info.log) {
|
|
1424
1627
|
info.log = "";
|
|
1628
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1425
1629
|
}
|
|
1426
1630
|
info.operation = "getText";
|
|
1427
1631
|
info.selectors = selectors;
|
|
1428
|
-
let element = await this._locate(selectors, info, _params);
|
|
1632
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1429
1633
|
if (climb > 0) {
|
|
1430
1634
|
const climbArray = [];
|
|
1431
1635
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1444,6 +1648,18 @@ class StableBrowser {
|
|
|
1444
1648
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1445
1649
|
try {
|
|
1446
1650
|
await this._highlightElements(element);
|
|
1651
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1652
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1653
|
+
// this._highlightElements(element)
|
|
1654
|
+
// .then(async () => {
|
|
1655
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1656
|
+
// this._unhighlightElements(element).then(
|
|
1657
|
+
// () => {}
|
|
1658
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1659
|
+
// );
|
|
1660
|
+
// })
|
|
1661
|
+
// .catch(e);
|
|
1662
|
+
// }
|
|
1447
1663
|
const elementText = await element.innerText();
|
|
1448
1664
|
return {
|
|
1449
1665
|
text: elementText,
|
|
@@ -1455,189 +1671,219 @@ class StableBrowser {
|
|
|
1455
1671
|
}
|
|
1456
1672
|
catch (e) {
|
|
1457
1673
|
//await this.closeUnexpectedPopups();
|
|
1458
|
-
this.logger.info("no innerText will use textContent");
|
|
1674
|
+
this.logger.info("no innerText, will use textContent");
|
|
1459
1675
|
const elementText = await element.textContent();
|
|
1460
1676
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1461
1677
|
}
|
|
1462
1678
|
}
|
|
1463
1679
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1464
|
-
var _a;
|
|
1465
|
-
this._validateSelectors(selectors);
|
|
1466
1680
|
if (!pattern) {
|
|
1467
1681
|
throw new Error("pattern is null");
|
|
1468
1682
|
}
|
|
1469
1683
|
if (!text) {
|
|
1470
1684
|
throw new Error("text is null");
|
|
1471
1685
|
}
|
|
1686
|
+
const state = {
|
|
1687
|
+
selectors,
|
|
1688
|
+
_params,
|
|
1689
|
+
pattern,
|
|
1690
|
+
value: pattern,
|
|
1691
|
+
options,
|
|
1692
|
+
world,
|
|
1693
|
+
locate: false,
|
|
1694
|
+
scroll: false,
|
|
1695
|
+
screenshot: false,
|
|
1696
|
+
highlight: false,
|
|
1697
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1698
|
+
text: `Verify element contains pattern: ${pattern}`,
|
|
1699
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1700
|
+
operation: "containsPattern",
|
|
1701
|
+
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1702
|
+
};
|
|
1472
1703
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1473
1704
|
if (newValue !== text) {
|
|
1474
1705
|
this.logger.info(text + "=" + newValue);
|
|
1475
1706
|
text = newValue;
|
|
1476
1707
|
}
|
|
1477
|
-
const startTime = Date.now();
|
|
1478
|
-
let error = null;
|
|
1479
|
-
let screenshotId = null;
|
|
1480
|
-
let screenshotPath = null;
|
|
1481
|
-
const info = {};
|
|
1482
|
-
info.log =
|
|
1483
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1484
|
-
info.operation = "containsPattern";
|
|
1485
|
-
info.selectors = selectors;
|
|
1486
|
-
info.value = text;
|
|
1487
|
-
info.pattern = pattern;
|
|
1488
1708
|
let foundObj = null;
|
|
1489
1709
|
try {
|
|
1490
|
-
|
|
1710
|
+
await _preCommand(state, this);
|
|
1711
|
+
state.info.pattern = pattern;
|
|
1712
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1491
1713
|
if (foundObj && foundObj.element) {
|
|
1492
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1714
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1493
1715
|
}
|
|
1494
|
-
|
|
1716
|
+
await _screenshot(state, this);
|
|
1495
1717
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1496
1718
|
pattern = pattern.replace("{text}", escapedText);
|
|
1497
1719
|
let regex = new RegExp(pattern, "im");
|
|
1498
|
-
if (!regex.test(foundObj
|
|
1499
|
-
info.foundText = foundObj
|
|
1720
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1721
|
+
state.info.foundText = foundObj?.text;
|
|
1500
1722
|
throw new Error("element doesn't contain text " + text);
|
|
1501
1723
|
}
|
|
1502
|
-
return info;
|
|
1724
|
+
return state.info;
|
|
1503
1725
|
}
|
|
1504
1726
|
catch (e) {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1508
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1509
|
-
info.screenshotPath = screenshotPath;
|
|
1510
|
-
Object.assign(e, { info: info });
|
|
1511
|
-
error = e;
|
|
1512
|
-
throw e;
|
|
1727
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1728
|
+
await _commandError(state, e, this);
|
|
1513
1729
|
}
|
|
1514
1730
|
finally {
|
|
1515
|
-
|
|
1516
|
-
this._reportToWorld(world, {
|
|
1517
|
-
element_name: selectors.element_name,
|
|
1518
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1519
|
-
value: pattern,
|
|
1520
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1521
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1522
|
-
result: error
|
|
1523
|
-
? {
|
|
1524
|
-
status: "FAILED",
|
|
1525
|
-
startTime,
|
|
1526
|
-
endTime,
|
|
1527
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1528
|
-
}
|
|
1529
|
-
: {
|
|
1530
|
-
status: "PASSED",
|
|
1531
|
-
startTime,
|
|
1532
|
-
endTime,
|
|
1533
|
-
},
|
|
1534
|
-
info: info,
|
|
1535
|
-
});
|
|
1731
|
+
await _commandFinally(state, this);
|
|
1536
1732
|
}
|
|
1537
1733
|
}
|
|
1538
1734
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1735
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1736
|
+
const startTime = Date.now();
|
|
1737
|
+
const state = {
|
|
1738
|
+
selectors,
|
|
1739
|
+
_params,
|
|
1740
|
+
value: text,
|
|
1741
|
+
options,
|
|
1742
|
+
world,
|
|
1743
|
+
locate: false,
|
|
1744
|
+
scroll: false,
|
|
1745
|
+
screenshot: false,
|
|
1746
|
+
highlight: false,
|
|
1747
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1748
|
+
text: `Verify element contains text: ${text}`,
|
|
1749
|
+
operation: "containsText",
|
|
1750
|
+
log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
|
|
1751
|
+
};
|
|
1542
1752
|
if (!text) {
|
|
1543
1753
|
throw new Error("text is null");
|
|
1544
1754
|
}
|
|
1545
|
-
|
|
1546
|
-
let error = null;
|
|
1547
|
-
let screenshotId = null;
|
|
1548
|
-
let screenshotPath = null;
|
|
1549
|
-
const info = {};
|
|
1550
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1551
|
-
info.operation = "containsText";
|
|
1552
|
-
info.selectors = selectors;
|
|
1755
|
+
text = unEscapeString(text);
|
|
1553
1756
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1554
1757
|
if (newValue !== text) {
|
|
1555
1758
|
this.logger.info(text + "=" + newValue);
|
|
1556
1759
|
text = newValue;
|
|
1557
1760
|
}
|
|
1558
|
-
info.value = text;
|
|
1559
1761
|
let foundObj = null;
|
|
1560
1762
|
try {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1568
|
-
if (dateAlternatives.date) {
|
|
1569
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1570
|
-
if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
|
|
1571
|
-
((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
|
|
1572
|
-
return info;
|
|
1763
|
+
while (Date.now() - startTime < timeout) {
|
|
1764
|
+
try {
|
|
1765
|
+
await _preCommand(state, this);
|
|
1766
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1767
|
+
if (foundObj && foundObj.element) {
|
|
1768
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1573
1769
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1770
|
+
await _screenshot(state, this);
|
|
1771
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1772
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1773
|
+
if (dateAlternatives.date) {
|
|
1774
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1775
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1776
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1777
|
+
return state.info;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
else if (numberAlternatives.number) {
|
|
1782
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1783
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1784
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1785
|
+
return state.info;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1790
|
+
return state.info;
|
|
1582
1791
|
}
|
|
1583
1792
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
throw new Error("element doesn't contain text " + text);
|
|
1793
|
+
catch (e) {
|
|
1794
|
+
// Log error but continue retrying until timeout is reached
|
|
1795
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1796
|
+
}
|
|
1797
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1590
1798
|
}
|
|
1591
|
-
|
|
1799
|
+
state.info.foundText = foundObj?.text;
|
|
1800
|
+
state.info.value = foundObj?.value;
|
|
1801
|
+
throw new Error("element doesn't contain text " + text);
|
|
1592
1802
|
}
|
|
1593
1803
|
catch (e) {
|
|
1594
|
-
|
|
1595
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1596
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1597
|
-
info.screenshotPath = screenshotPath;
|
|
1598
|
-
Object.assign(e, { info: info });
|
|
1599
|
-
error = e;
|
|
1804
|
+
await _commandError(state, e, this);
|
|
1600
1805
|
throw e;
|
|
1601
1806
|
}
|
|
1602
1807
|
finally {
|
|
1603
|
-
|
|
1604
|
-
this._reportToWorld(world, {
|
|
1605
|
-
element_name: selectors.element_name,
|
|
1606
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1607
|
-
text: `Verify element contains text: ${text}`,
|
|
1608
|
-
value: text,
|
|
1609
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1610
|
-
result: error
|
|
1611
|
-
? {
|
|
1612
|
-
status: "FAILED",
|
|
1613
|
-
startTime,
|
|
1614
|
-
endTime,
|
|
1615
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1616
|
-
}
|
|
1617
|
-
: {
|
|
1618
|
-
status: "PASSED",
|
|
1619
|
-
startTime,
|
|
1620
|
-
endTime,
|
|
1621
|
-
},
|
|
1622
|
-
info: info,
|
|
1623
|
-
});
|
|
1808
|
+
await _commandFinally(state, this);
|
|
1624
1809
|
}
|
|
1625
1810
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1811
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1812
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1813
|
+
const startTime = Date.now();
|
|
1814
|
+
const state = {
|
|
1815
|
+
_params,
|
|
1816
|
+
value: referanceSnapshot,
|
|
1817
|
+
options,
|
|
1818
|
+
world,
|
|
1819
|
+
locate: false,
|
|
1820
|
+
scroll: false,
|
|
1821
|
+
screenshot: true,
|
|
1822
|
+
highlight: false,
|
|
1823
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1824
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1825
|
+
operation: "snapshotValidation",
|
|
1826
|
+
log: "***** verify snapshot *****\n",
|
|
1827
|
+
};
|
|
1828
|
+
if (!referanceSnapshot) {
|
|
1829
|
+
throw new Error("referanceSnapshot is null");
|
|
1830
|
+
}
|
|
1831
|
+
let text = null;
|
|
1832
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1833
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1630
1834
|
}
|
|
1631
|
-
else if (this.
|
|
1632
|
-
|
|
1835
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1836
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1633
1837
|
}
|
|
1634
|
-
else if (
|
|
1635
|
-
|
|
1838
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1839
|
+
text = referanceSnapshot.substring(5);
|
|
1636
1840
|
}
|
|
1637
1841
|
else {
|
|
1638
|
-
|
|
1842
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1843
|
+
}
|
|
1844
|
+
state.text = text;
|
|
1845
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1846
|
+
await _preCommand(state, this);
|
|
1847
|
+
let foundObj = null;
|
|
1848
|
+
try {
|
|
1849
|
+
let matchResult = null;
|
|
1850
|
+
while (Date.now() - startTime < timeout) {
|
|
1851
|
+
try {
|
|
1852
|
+
let scope = null;
|
|
1853
|
+
if (!frameSelectors) {
|
|
1854
|
+
scope = this.page;
|
|
1855
|
+
}
|
|
1856
|
+
else {
|
|
1857
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1858
|
+
}
|
|
1859
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1860
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1861
|
+
if (matchResult.errorLine !== -1) {
|
|
1862
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1863
|
+
}
|
|
1864
|
+
// highlight and screenshot
|
|
1865
|
+
try {
|
|
1866
|
+
await await highlightSnapshot(newValue, scope);
|
|
1867
|
+
await _screenshot(state, this);
|
|
1868
|
+
}
|
|
1869
|
+
catch (e) { }
|
|
1870
|
+
return state.info;
|
|
1871
|
+
}
|
|
1872
|
+
catch (e) {
|
|
1873
|
+
// Log error but continue retrying until timeout is reached
|
|
1874
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1875
|
+
}
|
|
1876
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1877
|
+
}
|
|
1878
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1879
|
+
}
|
|
1880
|
+
catch (e) {
|
|
1881
|
+
await _commandError(state, e, this);
|
|
1882
|
+
throw e;
|
|
1883
|
+
}
|
|
1884
|
+
finally {
|
|
1885
|
+
await _commandFinally(state, this);
|
|
1639
1886
|
}
|
|
1640
|
-
return dataFile;
|
|
1641
1887
|
}
|
|
1642
1888
|
async waitForUserInput(message, world = null) {
|
|
1643
1889
|
if (!message) {
|
|
@@ -1667,13 +1913,22 @@ class StableBrowser {
|
|
|
1667
1913
|
return;
|
|
1668
1914
|
}
|
|
1669
1915
|
// if data file exists, load it
|
|
1670
|
-
const dataFile =
|
|
1916
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1671
1917
|
let data = this.getTestData(world);
|
|
1672
1918
|
// merge the testData with the existing data
|
|
1673
1919
|
Object.assign(data, testData);
|
|
1674
1920
|
// save the data to the file
|
|
1675
1921
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1676
1922
|
}
|
|
1923
|
+
overwriteTestData(testData, world = null) {
|
|
1924
|
+
if (!testData) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
// if data file exists, load it
|
|
1928
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1929
|
+
// save the data to the file
|
|
1930
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1931
|
+
}
|
|
1677
1932
|
_getDataFilePath(fileName) {
|
|
1678
1933
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1679
1934
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1770,12 +2025,7 @@ class StableBrowser {
|
|
|
1770
2025
|
}
|
|
1771
2026
|
}
|
|
1772
2027
|
getTestData(world = null) {
|
|
1773
|
-
|
|
1774
|
-
let data = {};
|
|
1775
|
-
if (fs.existsSync(dataFile)) {
|
|
1776
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1777
|
-
}
|
|
1778
|
-
return data;
|
|
2028
|
+
return _getTestData(world, this.context, this);
|
|
1779
2029
|
}
|
|
1780
2030
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1781
2031
|
// collect url/path/title
|
|
@@ -1802,11 +2052,9 @@ class StableBrowser {
|
|
|
1802
2052
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1803
2053
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1804
2054
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
2055
|
+
// to make sure the path doesn't start with -
|
|
2056
|
+
const uuidStr = "id_" + randomUUID();
|
|
2057
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1810
2058
|
try {
|
|
1811
2059
|
await this.takeScreenshot(screenshotPath);
|
|
1812
2060
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1816,15 +2064,15 @@ class StableBrowser {
|
|
|
1816
2064
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1817
2065
|
// }
|
|
1818
2066
|
// });
|
|
2067
|
+
result.screenshotId = uuidStr;
|
|
2068
|
+
result.screenshotPath = screenshotPath;
|
|
2069
|
+
if (info && info.box) {
|
|
2070
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
2071
|
+
}
|
|
1819
2072
|
}
|
|
1820
2073
|
catch (e) {
|
|
1821
2074
|
this.logger.info("unable to take screenshot, ignored");
|
|
1822
2075
|
}
|
|
1823
|
-
result.screenshotId = nextIndex;
|
|
1824
|
-
result.screenshotPath = screenshotPath;
|
|
1825
|
-
if (info && info.box) {
|
|
1826
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1827
|
-
}
|
|
1828
2076
|
}
|
|
1829
2077
|
else if (options && options.screenshot) {
|
|
1830
2078
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1849,7 +2097,6 @@ class StableBrowser {
|
|
|
1849
2097
|
}
|
|
1850
2098
|
async takeScreenshot(screenshotPath) {
|
|
1851
2099
|
const playContext = this.context.playContext;
|
|
1852
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1853
2100
|
// Using CDP to capture the screenshot
|
|
1854
2101
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1855
2102
|
document.body.scrollWidth,
|
|
@@ -1859,164 +2106,560 @@ class StableBrowser {
|
|
|
1859
2106
|
document.body.clientWidth,
|
|
1860
2107
|
document.documentElement.clientWidth,
|
|
1861
2108
|
])));
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2109
|
+
let screenshotBuffer = null;
|
|
2110
|
+
// if (focusedElement) {
|
|
2111
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2112
|
+
// await this._unhighlightElements(focusedElement);
|
|
2113
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2114
|
+
// console.log(`Unhighlighted previous element`);
|
|
2115
|
+
// }
|
|
2116
|
+
// if (focusedElement) {
|
|
2117
|
+
// await this._highlightElements(focusedElement);
|
|
2118
|
+
// }
|
|
2119
|
+
if (this.context.browserName === "chromium") {
|
|
2120
|
+
const client = await playContext.newCDPSession(this.page);
|
|
2121
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
2122
|
+
format: "png",
|
|
2123
|
+
// clip: {
|
|
2124
|
+
// x: 0,
|
|
2125
|
+
// y: 0,
|
|
2126
|
+
// width: viewportWidth,
|
|
2127
|
+
// height: viewportHeight,
|
|
2128
|
+
// scale: 1,
|
|
2129
|
+
// },
|
|
2130
|
+
});
|
|
2131
|
+
await client.detach();
|
|
2132
|
+
if (!screenshotPath) {
|
|
2133
|
+
return data;
|
|
2134
|
+
}
|
|
2135
|
+
screenshotBuffer = Buffer.from(data, "base64");
|
|
2136
|
+
}
|
|
2137
|
+
else {
|
|
2138
|
+
screenshotBuffer = await this.page.screenshot();
|
|
1882
2139
|
}
|
|
1883
|
-
|
|
2140
|
+
// if (focusedElement) {
|
|
2141
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2142
|
+
// await this._unhighlightElements(focusedElement);
|
|
2143
|
+
// }
|
|
1884
2144
|
let image = await Jimp.read(screenshotBuffer);
|
|
1885
2145
|
// Get the image dimensions
|
|
1886
2146
|
const { width, height } = image.bitmap;
|
|
2147
|
+
const resizeRatio = viewportWidth / width;
|
|
1887
2148
|
// Resize the image to fit within the viewport dimensions without enlarging
|
|
1888
|
-
if (width > viewportWidth
|
|
1889
|
-
image = image.resize({ w: viewportWidth, h:
|
|
2149
|
+
if (width > viewportWidth) {
|
|
2150
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
1890
2151
|
await image.write(screenshotPath);
|
|
1891
2152
|
}
|
|
1892
2153
|
else {
|
|
1893
2154
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1894
2155
|
}
|
|
1895
|
-
|
|
2156
|
+
return screenshotBuffer;
|
|
1896
2157
|
}
|
|
1897
2158
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2159
|
+
const state = {
|
|
2160
|
+
selectors,
|
|
2161
|
+
_params,
|
|
2162
|
+
options,
|
|
2163
|
+
world,
|
|
2164
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2165
|
+
text: `Verify element exists in page`,
|
|
2166
|
+
operation: "verifyElementExistInPage",
|
|
2167
|
+
log: "***** verify element " + selectors.element_name + " exists in page *****\n",
|
|
2168
|
+
};
|
|
1903
2169
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1904
|
-
const info = {};
|
|
1905
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1906
|
-
info.operation = "verify";
|
|
1907
|
-
info.selectors = selectors;
|
|
1908
2170
|
try {
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
}
|
|
1913
|
-
await this._highlightElements(element);
|
|
1914
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1915
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1916
|
-
return info;
|
|
2171
|
+
await _preCommand(state, this);
|
|
2172
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
2173
|
+
return state.info;
|
|
1917
2174
|
}
|
|
1918
2175
|
catch (e) {
|
|
1919
|
-
|
|
1920
|
-
this.logger.error("verify failed " + info.log);
|
|
1921
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1922
|
-
info.screenshotPath = screenshotPath;
|
|
1923
|
-
Object.assign(e, { info: info });
|
|
1924
|
-
error = e;
|
|
1925
|
-
throw e;
|
|
2176
|
+
await _commandError(state, e, this);
|
|
1926
2177
|
}
|
|
1927
2178
|
finally {
|
|
1928
|
-
|
|
1929
|
-
this._reportToWorld(world, {
|
|
1930
|
-
element_name: selectors.element_name,
|
|
1931
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1932
|
-
text: "Verify element exists in page",
|
|
1933
|
-
screenshotId,
|
|
1934
|
-
result: error
|
|
1935
|
-
? {
|
|
1936
|
-
status: "FAILED",
|
|
1937
|
-
startTime,
|
|
1938
|
-
endTime,
|
|
1939
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1940
|
-
}
|
|
1941
|
-
: {
|
|
1942
|
-
status: "PASSED",
|
|
1943
|
-
startTime,
|
|
1944
|
-
endTime,
|
|
1945
|
-
},
|
|
1946
|
-
info: info,
|
|
1947
|
-
});
|
|
2179
|
+
await _commandFinally(state, this);
|
|
1948
2180
|
}
|
|
1949
2181
|
}
|
|
1950
2182
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2183
|
+
const state = {
|
|
2184
|
+
selectors,
|
|
2185
|
+
_params,
|
|
2186
|
+
attribute,
|
|
2187
|
+
variable,
|
|
2188
|
+
options,
|
|
2189
|
+
world,
|
|
2190
|
+
type: Types.EXTRACT,
|
|
2191
|
+
text: `Extract attribute from element`,
|
|
2192
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
2193
|
+
operation: "extractAttribute",
|
|
2194
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2195
|
+
allowDisabled: true,
|
|
2196
|
+
};
|
|
1956
2197
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1957
|
-
const info = {};
|
|
1958
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1959
|
-
info.operation = "extract";
|
|
1960
|
-
info.selectors = selectors;
|
|
1961
2198
|
try {
|
|
1962
|
-
|
|
1963
|
-
await this._highlightElements(element);
|
|
1964
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2199
|
+
await _preCommand(state, this);
|
|
1965
2200
|
switch (attribute) {
|
|
1966
2201
|
case "inner_text":
|
|
1967
|
-
|
|
2202
|
+
state.value = await state.element.innerText();
|
|
1968
2203
|
break;
|
|
1969
2204
|
case "href":
|
|
1970
|
-
|
|
2205
|
+
state.value = await state.element.getAttribute("href");
|
|
1971
2206
|
break;
|
|
1972
2207
|
case "value":
|
|
1973
|
-
|
|
2208
|
+
state.value = await state.element.inputValue();
|
|
2209
|
+
break;
|
|
2210
|
+
case "text":
|
|
2211
|
+
state.value = await state.element.textContent();
|
|
1974
2212
|
break;
|
|
1975
2213
|
default:
|
|
1976
|
-
|
|
2214
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1977
2215
|
break;
|
|
1978
2216
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2217
|
+
if (options !== null) {
|
|
2218
|
+
if (options.regex && options.regex !== "") {
|
|
2219
|
+
// Construct a regex pattern from the provided string
|
|
2220
|
+
const regex = options.regex.slice(1, -1);
|
|
2221
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2222
|
+
const matches = state.value.match(regexPattern);
|
|
2223
|
+
if (matches) {
|
|
2224
|
+
let newValue = "";
|
|
2225
|
+
for (const match of matches) {
|
|
2226
|
+
newValue += match;
|
|
2227
|
+
}
|
|
2228
|
+
state.value = newValue;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2232
|
+
state.value = state.value.trim();
|
|
2233
|
+
}
|
|
1982
2234
|
}
|
|
1983
|
-
|
|
1984
|
-
this.
|
|
1985
|
-
|
|
2235
|
+
state.info.value = state.value;
|
|
2236
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2237
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2238
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2239
|
+
return state.info;
|
|
1986
2240
|
}
|
|
1987
2241
|
catch (e) {
|
|
1988
|
-
|
|
1989
|
-
this.logger.error("extract failed " + info.log);
|
|
1990
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1991
|
-
info.screenshotPath = screenshotPath;
|
|
1992
|
-
Object.assign(e, { info: info });
|
|
1993
|
-
error = e;
|
|
1994
|
-
throw e;
|
|
2242
|
+
await _commandError(state, e, this);
|
|
1995
2243
|
}
|
|
1996
2244
|
finally {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2245
|
+
await _commandFinally(state, this);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2249
|
+
const state = {
|
|
2250
|
+
selectors,
|
|
2251
|
+
_params,
|
|
2252
|
+
property,
|
|
2253
|
+
variable,
|
|
2254
|
+
options,
|
|
2255
|
+
world,
|
|
2256
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2257
|
+
text: `Extract property from element`,
|
|
2258
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2259
|
+
operation: "extractProperty",
|
|
2260
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2261
|
+
allowDisabled: true,
|
|
2262
|
+
};
|
|
2263
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2264
|
+
try {
|
|
2265
|
+
await _preCommand(state, this);
|
|
2266
|
+
switch (property) {
|
|
2267
|
+
case "inner_text":
|
|
2268
|
+
state.value = await state.element.innerText();
|
|
2269
|
+
break;
|
|
2270
|
+
case "href":
|
|
2271
|
+
state.value = await state.element.getAttribute("href");
|
|
2272
|
+
break;
|
|
2273
|
+
case "value":
|
|
2274
|
+
state.value = await state.element.inputValue();
|
|
2275
|
+
break;
|
|
2276
|
+
case "text":
|
|
2277
|
+
state.value = await state.element.textContent();
|
|
2278
|
+
break;
|
|
2279
|
+
default:
|
|
2280
|
+
if (property.startsWith("dataset.")) {
|
|
2281
|
+
const dataAttribute = property.substring(8);
|
|
2282
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2011
2283
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2284
|
+
else {
|
|
2285
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
if (options !== null) {
|
|
2289
|
+
if (options.regex && options.regex !== "") {
|
|
2290
|
+
// Construct a regex pattern from the provided string
|
|
2291
|
+
const regex = options.regex.slice(1, -1);
|
|
2292
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2293
|
+
const matches = state.value.match(regexPattern);
|
|
2294
|
+
if (matches) {
|
|
2295
|
+
let newValue = "";
|
|
2296
|
+
for (const match of matches) {
|
|
2297
|
+
newValue += match;
|
|
2298
|
+
}
|
|
2299
|
+
state.value = newValue;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2303
|
+
state.value = state.value.trim();
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
state.info.value = state.value;
|
|
2307
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2308
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2309
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2310
|
+
return state.info;
|
|
2311
|
+
}
|
|
2312
|
+
catch (e) {
|
|
2313
|
+
await _commandError(state, e, this);
|
|
2314
|
+
}
|
|
2315
|
+
finally {
|
|
2316
|
+
await _commandFinally(state, this);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2320
|
+
const state = {
|
|
2321
|
+
selectors,
|
|
2322
|
+
_params,
|
|
2323
|
+
attribute,
|
|
2324
|
+
value,
|
|
2325
|
+
options,
|
|
2326
|
+
world,
|
|
2327
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2328
|
+
highlight: true,
|
|
2329
|
+
screenshot: true,
|
|
2330
|
+
text: `Verify element attribute`,
|
|
2331
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2332
|
+
operation: "verifyAttribute",
|
|
2333
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2334
|
+
allowDisabled: true,
|
|
2335
|
+
};
|
|
2336
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2337
|
+
let val;
|
|
2338
|
+
let expectedValue;
|
|
2339
|
+
try {
|
|
2340
|
+
await _preCommand(state, this);
|
|
2341
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2342
|
+
state.info.expectedValue = expectedValue;
|
|
2343
|
+
switch (attribute) {
|
|
2344
|
+
case "innerText":
|
|
2345
|
+
val = String(await state.element.innerText());
|
|
2346
|
+
break;
|
|
2347
|
+
case "text":
|
|
2348
|
+
val = String(await state.element.textContent());
|
|
2349
|
+
break;
|
|
2350
|
+
case "value":
|
|
2351
|
+
val = String(await state.element.inputValue());
|
|
2352
|
+
break;
|
|
2353
|
+
case "checked":
|
|
2354
|
+
val = String(await state.element.isChecked());
|
|
2355
|
+
break;
|
|
2356
|
+
case "disabled":
|
|
2357
|
+
val = String(await state.element.isDisabled());
|
|
2358
|
+
break;
|
|
2359
|
+
case "readOnly":
|
|
2360
|
+
const isEditable = await state.element.isEditable();
|
|
2361
|
+
val = String(!isEditable);
|
|
2362
|
+
break;
|
|
2363
|
+
default:
|
|
2364
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2365
|
+
break;
|
|
2366
|
+
}
|
|
2367
|
+
state.info.value = val;
|
|
2368
|
+
let regex;
|
|
2369
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2370
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2371
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2372
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2373
|
+
state.info.regex = true;
|
|
2374
|
+
}
|
|
2375
|
+
else {
|
|
2376
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2377
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2378
|
+
}
|
|
2379
|
+
if (attribute === "innerText") {
|
|
2380
|
+
if (state.info.regex) {
|
|
2381
|
+
if (!regex.test(val)) {
|
|
2382
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2383
|
+
state.info.failCause.assertionFailed = true;
|
|
2384
|
+
state.info.failCause.lastError = errorMessage;
|
|
2385
|
+
throw new Error(errorMessage);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
else {
|
|
2389
|
+
const valLines = val.split("\n");
|
|
2390
|
+
const expectedLines = expectedValue.split("\n");
|
|
2391
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2392
|
+
if (!isPart) {
|
|
2393
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2394
|
+
state.info.failCause.assertionFailed = true;
|
|
2395
|
+
state.info.failCause.lastError = errorMessage;
|
|
2396
|
+
throw new Error(errorMessage);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
else {
|
|
2401
|
+
if (!val.match(regex)) {
|
|
2402
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2403
|
+
state.info.failCause.assertionFailed = true;
|
|
2404
|
+
state.info.failCause.lastError = errorMessage;
|
|
2405
|
+
throw new Error(errorMessage);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return state.info;
|
|
2409
|
+
}
|
|
2410
|
+
catch (e) {
|
|
2411
|
+
await _commandError(state, e, this);
|
|
2412
|
+
}
|
|
2413
|
+
finally {
|
|
2414
|
+
await _commandFinally(state, this);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2418
|
+
const state = {
|
|
2419
|
+
selectors,
|
|
2420
|
+
_params,
|
|
2421
|
+
property,
|
|
2422
|
+
value,
|
|
2423
|
+
options,
|
|
2424
|
+
world,
|
|
2425
|
+
type: Types.VERIFY_PROPERTY,
|
|
2426
|
+
highlight: true,
|
|
2427
|
+
screenshot: true,
|
|
2428
|
+
text: `Verify element property`,
|
|
2429
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2430
|
+
operation: "verifyProperty",
|
|
2431
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2432
|
+
allowDisabled: true,
|
|
2433
|
+
};
|
|
2434
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2435
|
+
let val;
|
|
2436
|
+
let expectedValue;
|
|
2437
|
+
try {
|
|
2438
|
+
await _preCommand(state, this);
|
|
2439
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2440
|
+
state.info.expectedValue = expectedValue;
|
|
2441
|
+
switch (property) {
|
|
2442
|
+
case "innerText":
|
|
2443
|
+
val = String(await state.element.innerText());
|
|
2444
|
+
break;
|
|
2445
|
+
case "text":
|
|
2446
|
+
val = String(await state.element.textContent());
|
|
2447
|
+
break;
|
|
2448
|
+
case "value":
|
|
2449
|
+
val = String(await state.element.inputValue());
|
|
2450
|
+
break;
|
|
2451
|
+
case "checked":
|
|
2452
|
+
val = String(await state.element.isChecked());
|
|
2453
|
+
break;
|
|
2454
|
+
case "disabled":
|
|
2455
|
+
val = String(await state.element.isDisabled());
|
|
2456
|
+
break;
|
|
2457
|
+
case "readOnly":
|
|
2458
|
+
const isEditable = await state.element.isEditable();
|
|
2459
|
+
val = String(!isEditable);
|
|
2460
|
+
break;
|
|
2461
|
+
case "innerHTML":
|
|
2462
|
+
val = String(await state.element.innerHTML());
|
|
2463
|
+
break;
|
|
2464
|
+
case "outerHTML":
|
|
2465
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2466
|
+
break;
|
|
2467
|
+
default:
|
|
2468
|
+
if (property.startsWith("dataset.")) {
|
|
2469
|
+
const dataAttribute = property.substring(8);
|
|
2470
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2471
|
+
}
|
|
2472
|
+
else {
|
|
2473
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
// Helper function to remove all style="" attributes
|
|
2477
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2478
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2479
|
+
};
|
|
2480
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2481
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2482
|
+
val = removeStyleAttributes(val);
|
|
2483
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2484
|
+
}
|
|
2485
|
+
state.info.value = val;
|
|
2486
|
+
let regex;
|
|
2487
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2488
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2489
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2490
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2491
|
+
state.info.regex = true;
|
|
2492
|
+
}
|
|
2493
|
+
else {
|
|
2494
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2495
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2496
|
+
}
|
|
2497
|
+
if (property === "innerText") {
|
|
2498
|
+
if (state.info.regex) {
|
|
2499
|
+
if (!regex.test(val)) {
|
|
2500
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2501
|
+
state.info.failCause.assertionFailed = true;
|
|
2502
|
+
state.info.failCause.lastError = errorMessage;
|
|
2503
|
+
throw new Error(errorMessage);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
else {
|
|
2507
|
+
// Fix: Replace escaped newlines with actual newlines before splitting
|
|
2508
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2509
|
+
const valLines = val.split("\n");
|
|
2510
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2511
|
+
// Check if all expected lines are present in the actual lines
|
|
2512
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2513
|
+
if (!isPart) {
|
|
2514
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2515
|
+
state.info.failCause.assertionFailed = true;
|
|
2516
|
+
state.info.failCause.lastError = errorMessage;
|
|
2517
|
+
throw new Error(errorMessage);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
else {
|
|
2522
|
+
if (!val.match(regex)) {
|
|
2523
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2524
|
+
state.info.failCause.assertionFailed = true;
|
|
2525
|
+
state.info.failCause.lastError = errorMessage;
|
|
2526
|
+
throw new Error(errorMessage);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return state.info;
|
|
2530
|
+
}
|
|
2531
|
+
catch (e) {
|
|
2532
|
+
await _commandError(state, e, this);
|
|
2533
|
+
}
|
|
2534
|
+
finally {
|
|
2535
|
+
await _commandFinally(state, this);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2539
|
+
// Convert timeout from seconds to milliseconds
|
|
2540
|
+
const timeoutMs = timeout * 1000;
|
|
2541
|
+
const state = {
|
|
2542
|
+
selectors,
|
|
2543
|
+
_params,
|
|
2544
|
+
condition,
|
|
2545
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2546
|
+
options,
|
|
2547
|
+
world,
|
|
2548
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2549
|
+
highlight: true,
|
|
2550
|
+
screenshot: true,
|
|
2551
|
+
text: `Conditional wait for element`,
|
|
2552
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2553
|
+
operation: "conditionalWait",
|
|
2554
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2555
|
+
allowDisabled: true,
|
|
2556
|
+
info: {},
|
|
2557
|
+
};
|
|
2558
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2559
|
+
const startTime = Date.now();
|
|
2560
|
+
let conditionMet = false;
|
|
2561
|
+
let currentValue = null;
|
|
2562
|
+
let lastError = null;
|
|
2563
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2564
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2565
|
+
const elapsedTime = Date.now() - startTime;
|
|
2566
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2567
|
+
try {
|
|
2568
|
+
// Try to execute _preCommand (element location)
|
|
2569
|
+
await _preCommand(state, this);
|
|
2570
|
+
// If _preCommand succeeds, start condition checking
|
|
2571
|
+
const checkCondition = async () => {
|
|
2572
|
+
try {
|
|
2573
|
+
switch (condition.toLowerCase()) {
|
|
2574
|
+
case "checked":
|
|
2575
|
+
currentValue = await state.element.isChecked();
|
|
2576
|
+
return currentValue === true;
|
|
2577
|
+
case "unchecked":
|
|
2578
|
+
currentValue = await state.element.isChecked();
|
|
2579
|
+
return currentValue === false;
|
|
2580
|
+
case "visible":
|
|
2581
|
+
currentValue = await state.element.isVisible();
|
|
2582
|
+
return currentValue === true;
|
|
2583
|
+
case "hidden":
|
|
2584
|
+
currentValue = await state.element.isVisible();
|
|
2585
|
+
return currentValue === false;
|
|
2586
|
+
case "enabled":
|
|
2587
|
+
currentValue = await state.element.isDisabled();
|
|
2588
|
+
return currentValue === false;
|
|
2589
|
+
case "disabled":
|
|
2590
|
+
currentValue = await state.element.isDisabled();
|
|
2591
|
+
return currentValue === true;
|
|
2592
|
+
case "editable":
|
|
2593
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2594
|
+
currentValue = await state.element.isContentEditable();
|
|
2595
|
+
return currentValue === true;
|
|
2596
|
+
default:
|
|
2597
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2598
|
+
state.info.success = false;
|
|
2599
|
+
return false;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
catch (error) {
|
|
2603
|
+
// Don't throw here, just return false to continue retrying
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
// Inner loop for condition checking (once element is located)
|
|
2608
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2609
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2610
|
+
conditionMet = await checkCondition();
|
|
2611
|
+
if (conditionMet) {
|
|
2612
|
+
break;
|
|
2613
|
+
}
|
|
2614
|
+
// Check if we still have time for another attempt
|
|
2615
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2616
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
break;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
// If we got here and condition is met, break out of main loop
|
|
2623
|
+
if (conditionMet) {
|
|
2624
|
+
break;
|
|
2625
|
+
}
|
|
2626
|
+
// If condition not met but no exception, we've timed out
|
|
2627
|
+
break;
|
|
2628
|
+
}
|
|
2629
|
+
catch (e) {
|
|
2630
|
+
lastError = e;
|
|
2631
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2632
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2633
|
+
// Check if we have enough time left to retry
|
|
2634
|
+
if (timeLeft > 100) {
|
|
2635
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2636
|
+
}
|
|
2637
|
+
else {
|
|
2638
|
+
break;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2019
2641
|
}
|
|
2642
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2643
|
+
state.info = {
|
|
2644
|
+
success: conditionMet,
|
|
2645
|
+
conditionMet,
|
|
2646
|
+
actualWaitTime,
|
|
2647
|
+
currentValue,
|
|
2648
|
+
lastError: lastError?.message || null,
|
|
2649
|
+
message: conditionMet
|
|
2650
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2651
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2652
|
+
};
|
|
2653
|
+
if (lastError) {
|
|
2654
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2655
|
+
}
|
|
2656
|
+
try {
|
|
2657
|
+
await _commandFinally(state, this);
|
|
2658
|
+
}
|
|
2659
|
+
catch (finallyError) {
|
|
2660
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2661
|
+
}
|
|
2662
|
+
return state.info;
|
|
2020
2663
|
}
|
|
2021
2664
|
async extractEmailData(emailAddress, options, world) {
|
|
2022
2665
|
if (!emailAddress) {
|
|
@@ -2036,7 +2679,7 @@ class StableBrowser {
|
|
|
2036
2679
|
if (options && options.timeout) {
|
|
2037
2680
|
timeout = options.timeout;
|
|
2038
2681
|
}
|
|
2039
|
-
const serviceUrl =
|
|
2682
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
2040
2683
|
const request = {
|
|
2041
2684
|
method: "POST",
|
|
2042
2685
|
url: serviceUrl,
|
|
@@ -2092,7 +2735,8 @@ class StableBrowser {
|
|
|
2092
2735
|
catch (e) {
|
|
2093
2736
|
errorCount++;
|
|
2094
2737
|
if (errorCount > 3) {
|
|
2095
|
-
throw e;
|
|
2738
|
+
// throw e;
|
|
2739
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2096
2740
|
}
|
|
2097
2741
|
// ignore
|
|
2098
2742
|
}
|
|
@@ -2106,27 +2750,32 @@ class StableBrowser {
|
|
|
2106
2750
|
async _highlightElements(scope, css) {
|
|
2107
2751
|
try {
|
|
2108
2752
|
if (!scope) {
|
|
2753
|
+
// console.log(`Scope is not defined`);
|
|
2109
2754
|
return;
|
|
2110
2755
|
}
|
|
2111
2756
|
if (!css) {
|
|
2112
2757
|
scope
|
|
2113
2758
|
.evaluate((node) => {
|
|
2114
2759
|
if (node && node.style) {
|
|
2115
|
-
let
|
|
2116
|
-
|
|
2760
|
+
let originalOutline = node.style.outline;
|
|
2761
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
2762
|
+
// node.__previousOutline = originalOutline;
|
|
2763
|
+
node.style.outline = "2px solid red";
|
|
2764
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
2117
2765
|
if (window) {
|
|
2118
2766
|
window.addEventListener("beforeunload", function (e) {
|
|
2119
|
-
node.style.
|
|
2767
|
+
node.style.outline = originalOutline;
|
|
2120
2768
|
});
|
|
2121
2769
|
}
|
|
2122
2770
|
setTimeout(function () {
|
|
2123
|
-
node.style.
|
|
2771
|
+
node.style.outline = originalOutline;
|
|
2124
2772
|
}, 2000);
|
|
2125
2773
|
}
|
|
2126
2774
|
})
|
|
2127
2775
|
.then(() => { })
|
|
2128
2776
|
.catch((e) => {
|
|
2129
2777
|
// ignore
|
|
2778
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2130
2779
|
});
|
|
2131
2780
|
}
|
|
2132
2781
|
else {
|
|
@@ -2142,17 +2791,18 @@ class StableBrowser {
|
|
|
2142
2791
|
if (!element.style) {
|
|
2143
2792
|
return;
|
|
2144
2793
|
}
|
|
2145
|
-
|
|
2794
|
+
let originalOutline = element.style.outline;
|
|
2795
|
+
element.__previousOutline = originalOutline;
|
|
2146
2796
|
// Set the new border to be red and 2px solid
|
|
2147
|
-
element.style.
|
|
2797
|
+
element.style.outline = "2px solid red";
|
|
2148
2798
|
if (window) {
|
|
2149
2799
|
window.addEventListener("beforeunload", function (e) {
|
|
2150
|
-
element.style.
|
|
2800
|
+
element.style.outline = originalOutline;
|
|
2151
2801
|
});
|
|
2152
2802
|
}
|
|
2153
2803
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2154
2804
|
setTimeout(function () {
|
|
2155
|
-
element.style.
|
|
2805
|
+
element.style.outline = originalOutline;
|
|
2156
2806
|
}, 2000);
|
|
2157
2807
|
}
|
|
2158
2808
|
return;
|
|
@@ -2160,6 +2810,7 @@ class StableBrowser {
|
|
|
2160
2810
|
.then(() => { })
|
|
2161
2811
|
.catch((e) => {
|
|
2162
2812
|
// ignore
|
|
2813
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2163
2814
|
});
|
|
2164
2815
|
}
|
|
2165
2816
|
}
|
|
@@ -2167,8 +2818,49 @@ class StableBrowser {
|
|
|
2167
2818
|
console.debug(error);
|
|
2168
2819
|
}
|
|
2169
2820
|
}
|
|
2821
|
+
_matcher(text) {
|
|
2822
|
+
if (!text) {
|
|
2823
|
+
return { matcher: "contains", queryText: "" };
|
|
2824
|
+
}
|
|
2825
|
+
if (text.length < 2) {
|
|
2826
|
+
return { matcher: "contains", queryText: text };
|
|
2827
|
+
}
|
|
2828
|
+
const split = text.split(":");
|
|
2829
|
+
const matcher = split[0].toLowerCase();
|
|
2830
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2831
|
+
return { matcher, queryText };
|
|
2832
|
+
}
|
|
2833
|
+
_getDomain(url) {
|
|
2834
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2835
|
+
return "";
|
|
2836
|
+
}
|
|
2837
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2838
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2839
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2840
|
+
}
|
|
2841
|
+
let n = hostnameFragments.length;
|
|
2842
|
+
let fragments = [...hostnameFragments];
|
|
2843
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2844
|
+
hostnameFragments.pop();
|
|
2845
|
+
n = hostnameFragments.length;
|
|
2846
|
+
}
|
|
2847
|
+
if (n == 0) {
|
|
2848
|
+
if (fragments[0] === "www")
|
|
2849
|
+
fragments = fragments.slice(1);
|
|
2850
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2851
|
+
}
|
|
2852
|
+
if (hostnameFragments[0] === "www")
|
|
2853
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2854
|
+
return hostnameFragments.join(".");
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Verify the page path matches the given path.
|
|
2858
|
+
* @param {string} pathPart - The path to verify.
|
|
2859
|
+
* @param {object} options - Options for verification.
|
|
2860
|
+
* @param {object} world - The world context.
|
|
2861
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2862
|
+
*/
|
|
2170
2863
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2171
|
-
const startTime = Date.now();
|
|
2172
2864
|
let error = null;
|
|
2173
2865
|
let screenshotId = null;
|
|
2174
2866
|
let screenshotPath = null;
|
|
@@ -2182,159 +2874,520 @@ class StableBrowser {
|
|
|
2182
2874
|
pathPart = newValue;
|
|
2183
2875
|
}
|
|
2184
2876
|
info.pathPart = pathPart;
|
|
2877
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2878
|
+
const state = {
|
|
2879
|
+
text_search: queryText,
|
|
2880
|
+
options,
|
|
2881
|
+
world,
|
|
2882
|
+
locate: false,
|
|
2883
|
+
scroll: false,
|
|
2884
|
+
highlight: false,
|
|
2885
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2886
|
+
text: `Verify the page url is ${queryText}`,
|
|
2887
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2888
|
+
operation: "verifyPagePath",
|
|
2889
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2890
|
+
};
|
|
2185
2891
|
try {
|
|
2892
|
+
await _preCommand(state, this);
|
|
2893
|
+
state.info.text = queryText;
|
|
2186
2894
|
for (let i = 0; i < 30; i++) {
|
|
2187
2895
|
const url = await this.page.url();
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2896
|
+
switch (matcher) {
|
|
2897
|
+
case "exact":
|
|
2898
|
+
if (url !== queryText) {
|
|
2899
|
+
if (i === 29) {
|
|
2900
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2901
|
+
}
|
|
2902
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
break;
|
|
2906
|
+
case "contains":
|
|
2907
|
+
if (!url.includes(queryText)) {
|
|
2908
|
+
if (i === 29) {
|
|
2909
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2910
|
+
}
|
|
2911
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2912
|
+
continue;
|
|
2913
|
+
}
|
|
2914
|
+
break;
|
|
2915
|
+
case "starts-with":
|
|
2916
|
+
{
|
|
2917
|
+
const domain = this._getDomain(url);
|
|
2918
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2919
|
+
if (i === 29) {
|
|
2920
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2921
|
+
}
|
|
2922
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
break;
|
|
2927
|
+
case "ends-with":
|
|
2928
|
+
{
|
|
2929
|
+
const urlObj = new URL(url);
|
|
2930
|
+
let route = "/";
|
|
2931
|
+
if (urlObj.pathname !== "/") {
|
|
2932
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2933
|
+
}
|
|
2934
|
+
else {
|
|
2935
|
+
route = "/";
|
|
2936
|
+
}
|
|
2937
|
+
if (route !== queryText) {
|
|
2938
|
+
if (i === 29) {
|
|
2939
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2940
|
+
}
|
|
2941
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2942
|
+
continue;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
break;
|
|
2946
|
+
case "regex":
|
|
2947
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2948
|
+
if (!regex.test(url)) {
|
|
2949
|
+
if (i === 29) {
|
|
2950
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2951
|
+
}
|
|
2952
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
break;
|
|
2956
|
+
default:
|
|
2957
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2958
|
+
if (!url.includes(pathPart)) {
|
|
2959
|
+
if (i === 29) {
|
|
2960
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2961
|
+
}
|
|
2962
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
await _screenshot(state, this);
|
|
2967
|
+
return state.info;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
catch (e) {
|
|
2971
|
+
state.info.failCause.lastError = e.message;
|
|
2972
|
+
state.info.failCause.assertionFailed = true;
|
|
2973
|
+
await _commandError(state, e, this);
|
|
2974
|
+
}
|
|
2975
|
+
finally {
|
|
2976
|
+
await _commandFinally(state, this);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Verify the page title matches the given title.
|
|
2981
|
+
* @param {string} title - The title to verify.
|
|
2982
|
+
* @param {object} options - Options for verification.
|
|
2983
|
+
* @param {object} world - The world context.
|
|
2984
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2985
|
+
*/
|
|
2986
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2987
|
+
let error = null;
|
|
2988
|
+
let screenshotId = null;
|
|
2989
|
+
let screenshotPath = null;
|
|
2990
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2991
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2992
|
+
if (newValue !== title) {
|
|
2993
|
+
this.logger.info(title + "=" + newValue);
|
|
2994
|
+
title = newValue;
|
|
2995
|
+
}
|
|
2996
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2997
|
+
const state = {
|
|
2998
|
+
text_search: queryText,
|
|
2999
|
+
options,
|
|
3000
|
+
world,
|
|
3001
|
+
locate: false,
|
|
3002
|
+
scroll: false,
|
|
3003
|
+
highlight: false,
|
|
3004
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
3005
|
+
text: `Verify the page title is ${queryText}`,
|
|
3006
|
+
_text: `Verify the page title is ${queryText}`,
|
|
3007
|
+
operation: "verifyPageTitle",
|
|
3008
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
3009
|
+
};
|
|
3010
|
+
try {
|
|
3011
|
+
await _preCommand(state, this);
|
|
3012
|
+
state.info.text = queryText;
|
|
3013
|
+
for (let i = 0; i < 30; i++) {
|
|
3014
|
+
const foundTitle = await this.page.title();
|
|
3015
|
+
switch (matcher) {
|
|
3016
|
+
case "exact":
|
|
3017
|
+
if (foundTitle !== queryText) {
|
|
3018
|
+
if (i === 29) {
|
|
3019
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
3020
|
+
}
|
|
3021
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
3024
|
+
break;
|
|
3025
|
+
case "contains":
|
|
3026
|
+
if (!foundTitle.includes(queryText)) {
|
|
3027
|
+
if (i === 29) {
|
|
3028
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
3029
|
+
}
|
|
3030
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3031
|
+
continue;
|
|
3032
|
+
}
|
|
3033
|
+
break;
|
|
3034
|
+
case "starts-with":
|
|
3035
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
3036
|
+
if (i === 29) {
|
|
3037
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
3038
|
+
}
|
|
3039
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
break;
|
|
3043
|
+
case "ends-with":
|
|
3044
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
3045
|
+
if (i === 29) {
|
|
3046
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
3047
|
+
}
|
|
3048
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
break;
|
|
3052
|
+
case "regex":
|
|
3053
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3054
|
+
if (!regex.test(foundTitle)) {
|
|
3055
|
+
if (i === 29) {
|
|
3056
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
3057
|
+
}
|
|
3058
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
break;
|
|
3062
|
+
default:
|
|
3063
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3064
|
+
if (!foundTitle.includes(title)) {
|
|
3065
|
+
if (i === 29) {
|
|
3066
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
3067
|
+
}
|
|
3068
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
await _screenshot(state, this);
|
|
3073
|
+
return state.info;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
catch (e) {
|
|
3077
|
+
state.info.failCause.lastError = e.message;
|
|
3078
|
+
state.info.failCause.assertionFailed = true;
|
|
3079
|
+
await _commandError(state, e, this);
|
|
3080
|
+
}
|
|
3081
|
+
finally {
|
|
3082
|
+
await _commandFinally(state, this);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
3086
|
+
const frames = this.page.frames();
|
|
3087
|
+
let results = [];
|
|
3088
|
+
// let ignoreCase = false;
|
|
3089
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3090
|
+
if (dateAlternatives.date) {
|
|
3091
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
3092
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3093
|
+
result.frame = frames[i];
|
|
3094
|
+
results.push(result);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
else if (numberAlternatives.number) {
|
|
3098
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
3099
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3100
|
+
result.frame = frames[i];
|
|
3101
|
+
results.push(result);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
else {
|
|
3105
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3106
|
+
result.frame = frames[i];
|
|
3107
|
+
results.push(result);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
state.info.results = results;
|
|
3111
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3112
|
+
return resultWithElementsFound;
|
|
3113
|
+
}
|
|
3114
|
+
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
3115
|
+
text = unEscapeString(text);
|
|
3116
|
+
const state = {
|
|
3117
|
+
text_search: text,
|
|
3118
|
+
options,
|
|
3119
|
+
world,
|
|
3120
|
+
locate: false,
|
|
3121
|
+
scroll: false,
|
|
3122
|
+
highlight: false,
|
|
3123
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
3124
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
3125
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
3126
|
+
operation: "verifyTextExistInPage",
|
|
3127
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
3128
|
+
};
|
|
3129
|
+
if (testForRegex(text)) {
|
|
3130
|
+
text = text.replace(/\\"/g, '"');
|
|
3131
|
+
}
|
|
3132
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3133
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3134
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
3135
|
+
if (newValue !== text) {
|
|
3136
|
+
this.logger.info(text + "=" + newValue);
|
|
3137
|
+
text = newValue;
|
|
3138
|
+
}
|
|
3139
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
3140
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
3141
|
+
try {
|
|
3142
|
+
await _preCommand(state, this);
|
|
3143
|
+
state.info.text = text;
|
|
3144
|
+
while (true) {
|
|
3145
|
+
let resultWithElementsFound = {
|
|
3146
|
+
length: 0,
|
|
3147
|
+
};
|
|
3148
|
+
try {
|
|
3149
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3150
|
+
}
|
|
3151
|
+
catch (error) {
|
|
3152
|
+
// ignore
|
|
3153
|
+
}
|
|
3154
|
+
if (resultWithElementsFound.length === 0) {
|
|
3155
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3156
|
+
throw new Error(`Text ${text} not found in page`);
|
|
2191
3157
|
}
|
|
2192
3158
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2193
3159
|
continue;
|
|
2194
3160
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
3161
|
+
try {
|
|
3162
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
3163
|
+
const frame = resultWithElementsFound[0].frame;
|
|
3164
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
3165
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3166
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3167
|
+
if (element) {
|
|
3168
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3169
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
await _screenshot(state, this);
|
|
3173
|
+
return state.info;
|
|
3174
|
+
}
|
|
3175
|
+
catch (error) {
|
|
3176
|
+
console.error(error);
|
|
3177
|
+
}
|
|
2197
3178
|
}
|
|
2198
3179
|
}
|
|
2199
3180
|
catch (e) {
|
|
2200
|
-
|
|
2201
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2202
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2203
|
-
info.screenshotPath = screenshotPath;
|
|
2204
|
-
Object.assign(e, { info: info });
|
|
2205
|
-
error = e;
|
|
2206
|
-
throw e;
|
|
3181
|
+
await _commandError(state, e, this);
|
|
2207
3182
|
}
|
|
2208
3183
|
finally {
|
|
2209
|
-
|
|
2210
|
-
this._reportToWorld(world, {
|
|
2211
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2212
|
-
text: "Verify page path",
|
|
2213
|
-
screenshotId,
|
|
2214
|
-
result: error
|
|
2215
|
-
? {
|
|
2216
|
-
status: "FAILED",
|
|
2217
|
-
startTime,
|
|
2218
|
-
endTime,
|
|
2219
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2220
|
-
}
|
|
2221
|
-
: {
|
|
2222
|
-
status: "PASSED",
|
|
2223
|
-
startTime,
|
|
2224
|
-
endTime,
|
|
2225
|
-
},
|
|
2226
|
-
info: info,
|
|
2227
|
-
});
|
|
3184
|
+
await _commandFinally(state, this);
|
|
2228
3185
|
}
|
|
2229
3186
|
}
|
|
2230
|
-
async
|
|
3187
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2231
3188
|
text = unEscapeString(text);
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
3189
|
+
const state = {
|
|
3190
|
+
text_search: text,
|
|
3191
|
+
options,
|
|
3192
|
+
world,
|
|
3193
|
+
locate: false,
|
|
3194
|
+
scroll: false,
|
|
3195
|
+
highlight: false,
|
|
3196
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
3197
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
3198
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
3199
|
+
operation: "verifyTextNotExistInPage",
|
|
3200
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
3201
|
+
};
|
|
3202
|
+
if (testForRegex(text)) {
|
|
3203
|
+
text = text.replace(/\\"/g, '"');
|
|
3204
|
+
}
|
|
3205
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2237
3206
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2238
|
-
const info = {};
|
|
2239
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2240
|
-
info.operation = "verifyTextExistInPage";
|
|
2241
3207
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2242
3208
|
if (newValue !== text) {
|
|
2243
3209
|
this.logger.info(text + "=" + newValue);
|
|
2244
3210
|
text = newValue;
|
|
2245
3211
|
}
|
|
2246
|
-
info.text = text;
|
|
2247
3212
|
let dateAlternatives = findDateAlternatives(text);
|
|
2248
3213
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2249
3214
|
try {
|
|
3215
|
+
await _preCommand(state, this);
|
|
3216
|
+
state.info.text = text;
|
|
3217
|
+
let resultWithElementsFound = {
|
|
3218
|
+
length: null, // initial cannot be 0
|
|
3219
|
+
};
|
|
2250
3220
|
while (true) {
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
|
|
2257
|
-
result.frame = frames[i];
|
|
2258
|
-
results.push(result);
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
else if (numberAlternatives.number) {
|
|
2262
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2263
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
|
|
2264
|
-
result.frame = frames[i];
|
|
2265
|
-
results.push(result);
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
else {
|
|
2269
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, {});
|
|
2270
|
-
result.frame = frames[i];
|
|
2271
|
-
results.push(result);
|
|
2272
|
-
}
|
|
3221
|
+
try {
|
|
3222
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3223
|
+
}
|
|
3224
|
+
catch (error) {
|
|
3225
|
+
// ignore
|
|
2273
3226
|
}
|
|
2274
|
-
info.results = results;
|
|
2275
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2276
3227
|
if (resultWithElementsFound.length === 0) {
|
|
2277
|
-
|
|
2278
|
-
|
|
3228
|
+
await _screenshot(state, this);
|
|
3229
|
+
return state.info;
|
|
3230
|
+
}
|
|
3231
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3232
|
+
throw new Error(`Text ${text} found in page`);
|
|
3233
|
+
}
|
|
3234
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
catch (e) {
|
|
3238
|
+
await _commandError(state, e, this);
|
|
3239
|
+
}
|
|
3240
|
+
finally {
|
|
3241
|
+
await _commandFinally(state, this);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
3245
|
+
textAnchor = unEscapeString(textAnchor);
|
|
3246
|
+
textToVerify = unEscapeString(textToVerify);
|
|
3247
|
+
const state = {
|
|
3248
|
+
text_search: textToVerify,
|
|
3249
|
+
options,
|
|
3250
|
+
world,
|
|
3251
|
+
locate: false,
|
|
3252
|
+
scroll: false,
|
|
3253
|
+
highlight: false,
|
|
3254
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
3255
|
+
text: `Verify text with relation to another text`,
|
|
3256
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
3257
|
+
operation: "verify_text_with_relation",
|
|
3258
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3259
|
+
};
|
|
3260
|
+
const cmdStartTime = Date.now();
|
|
3261
|
+
let cmdEndTime = null;
|
|
3262
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3263
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3264
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
3265
|
+
if (newValue !== textAnchor) {
|
|
3266
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
3267
|
+
textAnchor = newValue;
|
|
3268
|
+
}
|
|
3269
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
3270
|
+
if (newValue !== textToVerify) {
|
|
3271
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
3272
|
+
textToVerify = newValue;
|
|
3273
|
+
}
|
|
3274
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
3275
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
3276
|
+
let foundAncore = false;
|
|
3277
|
+
try {
|
|
3278
|
+
await _preCommand(state, this);
|
|
3279
|
+
state.info.text = textToVerify;
|
|
3280
|
+
let resultWithElementsFound = {
|
|
3281
|
+
length: 0,
|
|
3282
|
+
};
|
|
3283
|
+
while (true) {
|
|
3284
|
+
try {
|
|
3285
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3286
|
+
}
|
|
3287
|
+
catch (error) {
|
|
3288
|
+
// ignore
|
|
3289
|
+
}
|
|
3290
|
+
if (resultWithElementsFound.length === 0) {
|
|
3291
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3292
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2279
3293
|
}
|
|
2280
3294
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2281
3295
|
continue;
|
|
2282
3296
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
3297
|
+
else {
|
|
3298
|
+
cmdEndTime = Date.now();
|
|
3299
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3300
|
+
if (foundAncore) {
|
|
3301
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3302
|
+
}
|
|
3303
|
+
else {
|
|
3304
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
try {
|
|
3309
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3310
|
+
foundAncore = true;
|
|
3311
|
+
const result = resultWithElementsFound[i];
|
|
3312
|
+
const token = result.randomToken;
|
|
3313
|
+
const frame = result.frame;
|
|
3314
|
+
let css = `[data-blinq-id-${token}]`;
|
|
3315
|
+
const climbArray1 = [];
|
|
3316
|
+
for (let i = 0; i < climb; i++) {
|
|
3317
|
+
climbArray1.push("..");
|
|
3318
|
+
}
|
|
3319
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
3320
|
+
css = css + " >> " + climbXpath;
|
|
3321
|
+
const count = await frame.locator(css).count();
|
|
3322
|
+
for (let j = 0; j < count; j++) {
|
|
3323
|
+
const continer = await frame.locator(css).nth(j);
|
|
3324
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
3325
|
+
if (result.elementCount > 0) {
|
|
3326
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
3327
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3328
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
3329
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
3330
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
3331
|
+
// this._highlightElements(frame, dataAttribute)
|
|
3332
|
+
// .then(async () => {
|
|
3333
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3334
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
3335
|
+
// () => {}
|
|
3336
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
3337
|
+
// );
|
|
3338
|
+
// })
|
|
3339
|
+
// .catch(e);
|
|
3340
|
+
// }
|
|
3341
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
3342
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3343
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3344
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
3345
|
+
if (element) {
|
|
3346
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3347
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3348
|
+
}
|
|
3349
|
+
await _screenshot(state, this);
|
|
3350
|
+
return state.info;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
2291
3353
|
}
|
|
2292
3354
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
3355
|
+
catch (error) {
|
|
3356
|
+
console.error(error);
|
|
3357
|
+
}
|
|
2295
3358
|
}
|
|
2296
3359
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2297
3360
|
}
|
|
2298
3361
|
catch (e) {
|
|
2299
|
-
|
|
2300
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2301
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2302
|
-
info.screenshotPath = screenshotPath;
|
|
2303
|
-
Object.assign(e, { info: info });
|
|
2304
|
-
error = e;
|
|
2305
|
-
throw e;
|
|
3362
|
+
await _commandError(state, e, this);
|
|
2306
3363
|
}
|
|
2307
3364
|
finally {
|
|
2308
|
-
|
|
2309
|
-
this._reportToWorld(world, {
|
|
2310
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2311
|
-
text: "Verify text exists in page",
|
|
2312
|
-
screenshotId,
|
|
2313
|
-
result: error
|
|
2314
|
-
? {
|
|
2315
|
-
status: "FAILED",
|
|
2316
|
-
startTime,
|
|
2317
|
-
endTime,
|
|
2318
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2319
|
-
}
|
|
2320
|
-
: {
|
|
2321
|
-
status: "PASSED",
|
|
2322
|
-
startTime,
|
|
2323
|
-
endTime,
|
|
2324
|
-
},
|
|
2325
|
-
info: info,
|
|
2326
|
-
});
|
|
3365
|
+
await _commandFinally(state, this);
|
|
2327
3366
|
}
|
|
2328
3367
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
3368
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
3369
|
+
const frames = this.page.frames();
|
|
3370
|
+
let results = [];
|
|
3371
|
+
let ignoreCase = false;
|
|
3372
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3373
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
3374
|
+
result.frame = frames[i];
|
|
3375
|
+
const climbArray = [];
|
|
3376
|
+
for (let i = 0; i < climb; i++) {
|
|
3377
|
+
climbArray.push("..");
|
|
3378
|
+
}
|
|
3379
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
3380
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
3381
|
+
const count = await frames[i].locator(newLocator).count();
|
|
3382
|
+
if (count > 0) {
|
|
3383
|
+
result.elementCount = count;
|
|
3384
|
+
result.locator = newLocator;
|
|
3385
|
+
results.push(result);
|
|
3386
|
+
}
|
|
2336
3387
|
}
|
|
2337
|
-
|
|
3388
|
+
// state.info.results = results;
|
|
3389
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3390
|
+
return resultWithElementsFound;
|
|
2338
3391
|
}
|
|
2339
3392
|
async visualVerification(text, options = {}, world = null) {
|
|
2340
3393
|
const startTime = Date.now();
|
|
@@ -2350,14 +3403,17 @@ class StableBrowser {
|
|
|
2350
3403
|
throw new Error("TOKEN is not set");
|
|
2351
3404
|
}
|
|
2352
3405
|
try {
|
|
2353
|
-
let serviceUrl =
|
|
3406
|
+
let serviceUrl = _getServerUrl();
|
|
2354
3407
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2355
3408
|
info.screenshotPath = screenshotPath;
|
|
2356
3409
|
const screenshot = await this.takeScreenshot();
|
|
2357
|
-
|
|
2358
|
-
method: "
|
|
3410
|
+
let request = {
|
|
3411
|
+
method: "post",
|
|
3412
|
+
maxBodyLength: Infinity,
|
|
2359
3413
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2360
3414
|
headers: {
|
|
3415
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3416
|
+
"x-source": "aaa",
|
|
2361
3417
|
"Content-Type": "application/json",
|
|
2362
3418
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2363
3419
|
},
|
|
@@ -2366,7 +3422,7 @@ class StableBrowser {
|
|
|
2366
3422
|
screenshot: screenshot,
|
|
2367
3423
|
}),
|
|
2368
3424
|
};
|
|
2369
|
-
|
|
3425
|
+
const result = await axios.request(request);
|
|
2370
3426
|
if (result.data.status !== true) {
|
|
2371
3427
|
throw new Error("Visual validation failed");
|
|
2372
3428
|
}
|
|
@@ -2386,20 +3442,22 @@ class StableBrowser {
|
|
|
2386
3442
|
info.screenshotPath = screenshotPath;
|
|
2387
3443
|
Object.assign(e, { info: info });
|
|
2388
3444
|
error = e;
|
|
2389
|
-
throw e;
|
|
3445
|
+
// throw e;
|
|
3446
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2390
3447
|
}
|
|
2391
3448
|
finally {
|
|
2392
3449
|
const endTime = Date.now();
|
|
2393
|
-
|
|
3450
|
+
_reportToWorld(world, {
|
|
2394
3451
|
type: Types.VERIFY_VISUAL,
|
|
2395
3452
|
text: "Visual verification",
|
|
3453
|
+
_text: "Visual verification of " + text,
|
|
2396
3454
|
screenshotId,
|
|
2397
3455
|
result: error
|
|
2398
3456
|
? {
|
|
2399
3457
|
status: "FAILED",
|
|
2400
3458
|
startTime,
|
|
2401
3459
|
endTime,
|
|
2402
|
-
message: error
|
|
3460
|
+
message: error?.message,
|
|
2403
3461
|
}
|
|
2404
3462
|
: {
|
|
2405
3463
|
status: "PASSED",
|
|
@@ -2431,13 +3489,14 @@ class StableBrowser {
|
|
|
2431
3489
|
this.logger.info("Table data verified");
|
|
2432
3490
|
}
|
|
2433
3491
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2434
|
-
|
|
3492
|
+
_validateSelectors(selectors);
|
|
2435
3493
|
const startTime = Date.now();
|
|
2436
3494
|
let error = null;
|
|
2437
3495
|
let screenshotId = null;
|
|
2438
3496
|
let screenshotPath = null;
|
|
2439
3497
|
const info = {};
|
|
2440
3498
|
info.log = "";
|
|
3499
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2441
3500
|
info.operation = "getTableData";
|
|
2442
3501
|
info.selectors = selectors;
|
|
2443
3502
|
try {
|
|
@@ -2453,11 +3512,12 @@ class StableBrowser {
|
|
|
2453
3512
|
info.screenshotPath = screenshotPath;
|
|
2454
3513
|
Object.assign(e, { info: info });
|
|
2455
3514
|
error = e;
|
|
2456
|
-
throw e;
|
|
3515
|
+
// throw e;
|
|
3516
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2457
3517
|
}
|
|
2458
3518
|
finally {
|
|
2459
3519
|
const endTime = Date.now();
|
|
2460
|
-
|
|
3520
|
+
_reportToWorld(world, {
|
|
2461
3521
|
element_name: selectors.element_name,
|
|
2462
3522
|
type: Types.GET_TABLE_DATA,
|
|
2463
3523
|
text: "Get table data",
|
|
@@ -2467,7 +3527,7 @@ class StableBrowser {
|
|
|
2467
3527
|
status: "FAILED",
|
|
2468
3528
|
startTime,
|
|
2469
3529
|
endTime,
|
|
2470
|
-
message: error
|
|
3530
|
+
message: error?.message,
|
|
2471
3531
|
}
|
|
2472
3532
|
: {
|
|
2473
3533
|
status: "PASSED",
|
|
@@ -2479,7 +3539,7 @@ class StableBrowser {
|
|
|
2479
3539
|
}
|
|
2480
3540
|
}
|
|
2481
3541
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2482
|
-
|
|
3542
|
+
_validateSelectors(selectors);
|
|
2483
3543
|
if (!query) {
|
|
2484
3544
|
throw new Error("query is null");
|
|
2485
3545
|
}
|
|
@@ -2512,7 +3572,7 @@ class StableBrowser {
|
|
|
2512
3572
|
info.operation = "analyzeTable";
|
|
2513
3573
|
info.selectors = selectors;
|
|
2514
3574
|
info.query = query;
|
|
2515
|
-
query =
|
|
3575
|
+
query = _fixUsingParams(query, _params);
|
|
2516
3576
|
info.query_fixed = query;
|
|
2517
3577
|
info.operator = operator;
|
|
2518
3578
|
info.value = value;
|
|
@@ -2618,11 +3678,12 @@ class StableBrowser {
|
|
|
2618
3678
|
info.screenshotPath = screenshotPath;
|
|
2619
3679
|
Object.assign(e, { info: info });
|
|
2620
3680
|
error = e;
|
|
2621
|
-
throw e;
|
|
3681
|
+
// throw e;
|
|
3682
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2622
3683
|
}
|
|
2623
3684
|
finally {
|
|
2624
3685
|
const endTime = Date.now();
|
|
2625
|
-
|
|
3686
|
+
_reportToWorld(world, {
|
|
2626
3687
|
element_name: selectors.element_name,
|
|
2627
3688
|
type: Types.ANALYZE_TABLE,
|
|
2628
3689
|
text: "Analyze table",
|
|
@@ -2632,7 +3693,7 @@ class StableBrowser {
|
|
|
2632
3693
|
status: "FAILED",
|
|
2633
3694
|
startTime,
|
|
2634
3695
|
endTime,
|
|
2635
|
-
message: error
|
|
3696
|
+
message: error?.message,
|
|
2636
3697
|
}
|
|
2637
3698
|
: {
|
|
2638
3699
|
status: "PASSED",
|
|
@@ -2643,28 +3704,51 @@ class StableBrowser {
|
|
|
2643
3704
|
});
|
|
2644
3705
|
}
|
|
2645
3706
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
3707
|
+
/**
|
|
3708
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3709
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3710
|
+
* @param options - Optional configuration object
|
|
3711
|
+
* @param world - Optional world context
|
|
3712
|
+
* @returns Promise that resolves after the specified duration
|
|
3713
|
+
*/
|
|
3714
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3715
|
+
const state = {
|
|
3716
|
+
duration,
|
|
3717
|
+
options,
|
|
3718
|
+
world,
|
|
3719
|
+
locate: false,
|
|
3720
|
+
scroll: false,
|
|
3721
|
+
screenshot: false,
|
|
3722
|
+
highlight: false,
|
|
3723
|
+
type: Types.SLEEP,
|
|
3724
|
+
text: `Sleep for ${duration} ms`,
|
|
3725
|
+
_text: `Sleep for ${duration} ms`,
|
|
3726
|
+
operation: "sleep",
|
|
3727
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3728
|
+
};
|
|
3729
|
+
try {
|
|
3730
|
+
await _preCommand(state, this);
|
|
3731
|
+
if (duration < 0) {
|
|
3732
|
+
throw new Error("Sleep duration cannot be negative");
|
|
2662
3733
|
}
|
|
3734
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3735
|
+
return state.info;
|
|
3736
|
+
}
|
|
3737
|
+
catch (e) {
|
|
3738
|
+
await _commandError(state, e, this);
|
|
2663
3739
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
3740
|
+
finally {
|
|
3741
|
+
await _commandFinally(state, this);
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
3745
|
+
try {
|
|
3746
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3747
|
+
}
|
|
3748
|
+
catch (error) {
|
|
3749
|
+
this.logger.debug(error);
|
|
3750
|
+
throw error;
|
|
2666
3751
|
}
|
|
2667
|
-
return value;
|
|
2668
3752
|
}
|
|
2669
3753
|
_getLoadTimeout(options) {
|
|
2670
3754
|
let timeout = 15000;
|
|
@@ -2676,6 +3760,37 @@ class StableBrowser {
|
|
|
2676
3760
|
}
|
|
2677
3761
|
return timeout;
|
|
2678
3762
|
}
|
|
3763
|
+
_getFindElementTimeout(options) {
|
|
3764
|
+
if (options && options.timeout) {
|
|
3765
|
+
return options.timeout;
|
|
3766
|
+
}
|
|
3767
|
+
if (this.configuration.find_element_timeout) {
|
|
3768
|
+
return this.configuration.find_element_timeout;
|
|
3769
|
+
}
|
|
3770
|
+
return 30000;
|
|
3771
|
+
}
|
|
3772
|
+
async saveStoreState(path = null, world = null) {
|
|
3773
|
+
const storageState = await this.page.context().storageState();
|
|
3774
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
3775
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
3776
|
+
if (path) {
|
|
3777
|
+
// save { storageState: storageState } into the path
|
|
3778
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
3779
|
+
}
|
|
3780
|
+
else {
|
|
3781
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
async restoreSaveState(path = null, world = null) {
|
|
3785
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
3786
|
+
await refreshBrowser(this, path, world);
|
|
3787
|
+
this.registerEventListeners(this.context);
|
|
3788
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
3789
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
3790
|
+
if (this.onRestoreSaveState) {
|
|
3791
|
+
this.onRestoreSaveState(path);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
2679
3794
|
async waitForPageLoad(options = {}, world = null) {
|
|
2680
3795
|
let timeout = this._getLoadTimeout(options);
|
|
2681
3796
|
const promiseArray = [];
|
|
@@ -2709,13 +3824,12 @@ class StableBrowser {
|
|
|
2709
3824
|
else if (e.label === "domcontentloaded") {
|
|
2710
3825
|
console.log("waited for the domcontent loaded timeout");
|
|
2711
3826
|
}
|
|
2712
|
-
console.log(".");
|
|
2713
3827
|
}
|
|
2714
3828
|
finally {
|
|
2715
3829
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2716
3830
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2717
3831
|
const endTime = Date.now();
|
|
2718
|
-
|
|
3832
|
+
_reportToWorld(world, {
|
|
2719
3833
|
type: Types.GET_PAGE_STATUS,
|
|
2720
3834
|
text: "Wait for page load",
|
|
2721
3835
|
screenshotId,
|
|
@@ -2724,7 +3838,7 @@ class StableBrowser {
|
|
|
2724
3838
|
status: "FAILED",
|
|
2725
3839
|
startTime,
|
|
2726
3840
|
endTime,
|
|
2727
|
-
message: error
|
|
3841
|
+
message: error?.message,
|
|
2728
3842
|
}
|
|
2729
3843
|
: {
|
|
2730
3844
|
status: "PASSED",
|
|
@@ -2735,41 +3849,122 @@ class StableBrowser {
|
|
|
2735
3849
|
}
|
|
2736
3850
|
}
|
|
2737
3851
|
async closePage(options = {}, world = null) {
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
3852
|
+
const state = {
|
|
3853
|
+
options,
|
|
3854
|
+
world,
|
|
3855
|
+
locate: false,
|
|
3856
|
+
scroll: false,
|
|
3857
|
+
highlight: false,
|
|
3858
|
+
type: Types.CLOSE_PAGE,
|
|
3859
|
+
text: `Close page`,
|
|
3860
|
+
_text: `Close the page`,
|
|
3861
|
+
operation: "closePage",
|
|
3862
|
+
log: "***** close page *****\n",
|
|
3863
|
+
throwError: false,
|
|
3864
|
+
};
|
|
2743
3865
|
try {
|
|
3866
|
+
await _preCommand(state, this);
|
|
2744
3867
|
await this.page.close();
|
|
2745
3868
|
}
|
|
2746
3869
|
catch (e) {
|
|
2747
|
-
|
|
3870
|
+
await _commandError(state, e, this);
|
|
2748
3871
|
}
|
|
2749
3872
|
finally {
|
|
2750
|
-
await
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
3873
|
+
await _commandFinally(state, this);
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3877
|
+
let operation = null;
|
|
3878
|
+
if (!options || !options.operation) {
|
|
3879
|
+
throw new Error("operation is not defined");
|
|
3880
|
+
}
|
|
3881
|
+
operation = options.operation;
|
|
3882
|
+
// validate operation is one of the supported operations
|
|
3883
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3884
|
+
throw new Error("operation is not supported");
|
|
3885
|
+
}
|
|
3886
|
+
const state = {
|
|
3887
|
+
options,
|
|
3888
|
+
world,
|
|
3889
|
+
locate: false,
|
|
3890
|
+
scroll: false,
|
|
3891
|
+
highlight: false,
|
|
3892
|
+
type: Types.TABLE_OPERATION,
|
|
3893
|
+
text: `Table operation`,
|
|
3894
|
+
_text: `Table ${operation} operation`,
|
|
3895
|
+
operation: operation,
|
|
3896
|
+
log: "***** Table operation *****\n",
|
|
3897
|
+
};
|
|
3898
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3899
|
+
try {
|
|
3900
|
+
await _preCommand(state, this);
|
|
3901
|
+
const start = Date.now();
|
|
3902
|
+
let cellArea = null;
|
|
3903
|
+
while (true) {
|
|
3904
|
+
try {
|
|
3905
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3906
|
+
if (cellArea) {
|
|
3907
|
+
break;
|
|
2763
3908
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
3909
|
+
}
|
|
3910
|
+
catch (e) {
|
|
3911
|
+
// ignore
|
|
3912
|
+
}
|
|
3913
|
+
if (Date.now() - start > timeout) {
|
|
3914
|
+
throw new Error(`Cell not found in table`);
|
|
3915
|
+
}
|
|
3916
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3917
|
+
}
|
|
3918
|
+
switch (operation) {
|
|
3919
|
+
case "click":
|
|
3920
|
+
if (!options.css) {
|
|
3921
|
+
// will click in the center of the cell
|
|
3922
|
+
let xOffset = 0;
|
|
3923
|
+
let yOffset = 0;
|
|
3924
|
+
if (options.xOffset) {
|
|
3925
|
+
xOffset = options.xOffset;
|
|
3926
|
+
}
|
|
3927
|
+
if (options.yOffset) {
|
|
3928
|
+
yOffset = options.yOffset;
|
|
3929
|
+
}
|
|
3930
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3931
|
+
}
|
|
3932
|
+
else {
|
|
3933
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3934
|
+
if (results.length === 0) {
|
|
3935
|
+
throw new Error(`Element not found in cell area`);
|
|
3936
|
+
}
|
|
3937
|
+
state.element = results[0];
|
|
3938
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3939
|
+
}
|
|
3940
|
+
break;
|
|
3941
|
+
case "hover+click":
|
|
3942
|
+
if (!options.css) {
|
|
3943
|
+
throw new Error("css is not defined");
|
|
3944
|
+
}
|
|
3945
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3946
|
+
if (results.length === 0) {
|
|
3947
|
+
throw new Error(`Element not found in cell area`);
|
|
3948
|
+
}
|
|
3949
|
+
state.element = results[0];
|
|
3950
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3951
|
+
break;
|
|
3952
|
+
default:
|
|
3953
|
+
throw new Error("operation is not supported");
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
catch (e) {
|
|
3957
|
+
await _commandError(state, e, this);
|
|
3958
|
+
}
|
|
3959
|
+
finally {
|
|
3960
|
+
await _commandFinally(state, this);
|
|
2771
3961
|
}
|
|
2772
3962
|
}
|
|
3963
|
+
saveTestDataAsGlobal(options, world) {
|
|
3964
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
3965
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
3966
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
3967
|
+
}
|
|
2773
3968
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2774
3969
|
const startTime = Date.now();
|
|
2775
3970
|
let error = null;
|
|
@@ -2786,22 +3981,23 @@ class StableBrowser {
|
|
|
2786
3981
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2787
3982
|
}
|
|
2788
3983
|
catch (e) {
|
|
2789
|
-
|
|
3984
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2790
3985
|
}
|
|
2791
3986
|
finally {
|
|
2792
3987
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2793
3988
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2794
3989
|
const endTime = Date.now();
|
|
2795
|
-
|
|
3990
|
+
_reportToWorld(world, {
|
|
2796
3991
|
type: Types.SET_VIEWPORT,
|
|
2797
3992
|
text: "set viewport size to " + width + "x" + hight,
|
|
3993
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2798
3994
|
screenshotId,
|
|
2799
3995
|
result: error
|
|
2800
3996
|
? {
|
|
2801
3997
|
status: "FAILED",
|
|
2802
3998
|
startTime,
|
|
2803
3999
|
endTime,
|
|
2804
|
-
message: error
|
|
4000
|
+
message: error?.message,
|
|
2805
4001
|
}
|
|
2806
4002
|
: {
|
|
2807
4003
|
status: "PASSED",
|
|
@@ -2822,13 +4018,13 @@ class StableBrowser {
|
|
|
2822
4018
|
await this.page.reload();
|
|
2823
4019
|
}
|
|
2824
4020
|
catch (e) {
|
|
2825
|
-
|
|
4021
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2826
4022
|
}
|
|
2827
4023
|
finally {
|
|
2828
4024
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2829
4025
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2830
4026
|
const endTime = Date.now();
|
|
2831
|
-
|
|
4027
|
+
_reportToWorld(world, {
|
|
2832
4028
|
type: Types.GET_PAGE_STATUS,
|
|
2833
4029
|
text: "page relaod",
|
|
2834
4030
|
screenshotId,
|
|
@@ -2837,7 +4033,7 @@ class StableBrowser {
|
|
|
2837
4033
|
status: "FAILED",
|
|
2838
4034
|
startTime,
|
|
2839
4035
|
endTime,
|
|
2840
|
-
message: error
|
|
4036
|
+
message: error?.message,
|
|
2841
4037
|
}
|
|
2842
4038
|
: {
|
|
2843
4039
|
status: "PASSED",
|
|
@@ -2864,11 +4060,216 @@ class StableBrowser {
|
|
|
2864
4060
|
console.log("#-#");
|
|
2865
4061
|
}
|
|
2866
4062
|
}
|
|
2867
|
-
|
|
2868
|
-
if (
|
|
2869
|
-
|
|
4063
|
+
async beforeScenario(world, scenario) {
|
|
4064
|
+
if (world && world.attach) {
|
|
4065
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4066
|
+
}
|
|
4067
|
+
this.context.loadedRoutes = null;
|
|
4068
|
+
this.beforeScenarioCalled = true;
|
|
4069
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4070
|
+
this.scenarioName = scenario.pickle.name;
|
|
4071
|
+
}
|
|
4072
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
4073
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
4074
|
+
}
|
|
4075
|
+
if (this.context) {
|
|
4076
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
4077
|
+
}
|
|
4078
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4079
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4080
|
+
// check if @global_test_data tag is present
|
|
4081
|
+
if (this.tags.includes("@global_test_data")) {
|
|
4082
|
+
this.saveTestDataAsGlobal({}, world);
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
// update test data based on feature/scenario
|
|
4086
|
+
let envName = null;
|
|
4087
|
+
if (this.context && this.context.environment) {
|
|
4088
|
+
envName = this.context.environment.name;
|
|
4089
|
+
}
|
|
4090
|
+
if (!process.env.TEMP_RUN) {
|
|
4091
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4092
|
+
}
|
|
4093
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4094
|
+
}
|
|
4095
|
+
async afterScenario(world, scenario) { }
|
|
4096
|
+
async beforeStep(world, step) {
|
|
4097
|
+
if (!this.beforeScenarioCalled) {
|
|
4098
|
+
this.beforeScenario(world, step);
|
|
4099
|
+
this.context.loadedRoutes = null;
|
|
4100
|
+
}
|
|
4101
|
+
if (this.stepIndex === undefined) {
|
|
4102
|
+
this.stepIndex = 0;
|
|
4103
|
+
}
|
|
4104
|
+
else {
|
|
4105
|
+
this.stepIndex++;
|
|
4106
|
+
}
|
|
4107
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4108
|
+
this.stepName = step.pickleStep.text;
|
|
4109
|
+
this.logger.info("step: " + this.stepName);
|
|
4110
|
+
}
|
|
4111
|
+
else if (step && step.text) {
|
|
4112
|
+
this.stepName = step.text;
|
|
4113
|
+
}
|
|
4114
|
+
else {
|
|
4115
|
+
this.stepName = "step " + this.stepIndex;
|
|
4116
|
+
}
|
|
4117
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4118
|
+
if (this.context.browserObject.context) {
|
|
4119
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
if (this.initSnapshotTaken === false) {
|
|
4123
|
+
this.initSnapshotTaken = true;
|
|
4124
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
4125
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4126
|
+
if (snapshot) {
|
|
4127
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
this.context.routeResults = null;
|
|
4132
|
+
await registerBeforeStepRoutes(this.context, this.stepName);
|
|
4133
|
+
networkBeforeStep(this.stepName);
|
|
4134
|
+
}
|
|
4135
|
+
async getAriaSnapshot() {
|
|
4136
|
+
try {
|
|
4137
|
+
// find the page url
|
|
4138
|
+
const url = await this.page.url();
|
|
4139
|
+
// extract the path from the url
|
|
4140
|
+
const path = new URL(url).pathname;
|
|
4141
|
+
// get the page title
|
|
4142
|
+
const title = await this.page.title();
|
|
4143
|
+
// go over other frams
|
|
4144
|
+
const frames = this.page.frames();
|
|
4145
|
+
const snapshots = [];
|
|
4146
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4147
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4148
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4149
|
+
const frame = frames[i];
|
|
4150
|
+
try {
|
|
4151
|
+
// Ensure frame is attached and has body
|
|
4152
|
+
const body = frame.locator("body");
|
|
4153
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4154
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4155
|
+
if (!snapshot) {
|
|
4156
|
+
continue;
|
|
4157
|
+
}
|
|
4158
|
+
content.push(`- frame: ${i}`);
|
|
4159
|
+
content.push(snapshot);
|
|
4160
|
+
}
|
|
4161
|
+
catch (innerErr) {
|
|
4162
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4163
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
return content.join("\n");
|
|
4167
|
+
}
|
|
4168
|
+
catch (e) {
|
|
4169
|
+
console.log("Error in getAriaSnapshot");
|
|
4170
|
+
//console.debug(e);
|
|
4171
|
+
}
|
|
4172
|
+
return null;
|
|
4173
|
+
}
|
|
4174
|
+
/**
|
|
4175
|
+
* Sends command with custom payload to report.
|
|
4176
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4177
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4178
|
+
* @param content - Content of the command to be shown in the report.
|
|
4179
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4180
|
+
* @param world - Optional world context.
|
|
4181
|
+
* @public
|
|
4182
|
+
*/
|
|
4183
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4184
|
+
const state = {
|
|
4185
|
+
options,
|
|
4186
|
+
world,
|
|
4187
|
+
locate: false,
|
|
4188
|
+
scroll: false,
|
|
4189
|
+
screenshot: options.screenshot ?? false,
|
|
4190
|
+
highlight: options.highlight ?? false,
|
|
4191
|
+
type: Types.REPORT_COMMAND,
|
|
4192
|
+
text: commandText,
|
|
4193
|
+
_text: commandText,
|
|
4194
|
+
operation: "report_command",
|
|
4195
|
+
log: "***** " + commandText + " *****\n",
|
|
4196
|
+
};
|
|
4197
|
+
try {
|
|
4198
|
+
await _preCommand(state, this);
|
|
4199
|
+
const payload = {
|
|
4200
|
+
type: options.type ?? "text",
|
|
4201
|
+
content: content,
|
|
4202
|
+
screenshotId: null,
|
|
4203
|
+
};
|
|
4204
|
+
state.payload = payload;
|
|
4205
|
+
if (commandStatus === "FAILED") {
|
|
4206
|
+
state.throwError = true;
|
|
4207
|
+
throw new Error("Command failed");
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
catch (e) {
|
|
4211
|
+
await _commandError(state, e, this);
|
|
4212
|
+
}
|
|
4213
|
+
finally {
|
|
4214
|
+
await _commandFinally(state, this);
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
async afterStep(world, step) {
|
|
4218
|
+
this.stepName = null;
|
|
4219
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4220
|
+
if (this.context.browserObject.context) {
|
|
4221
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
4222
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4223
|
+
});
|
|
4224
|
+
if (world && world.attach) {
|
|
4225
|
+
await world.attach(JSON.stringify({
|
|
4226
|
+
type: "trace",
|
|
4227
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4228
|
+
}), "application/json+trace");
|
|
4229
|
+
}
|
|
4230
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
if (this.context) {
|
|
4234
|
+
this.context.examplesRow = null;
|
|
4235
|
+
}
|
|
4236
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
4237
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4238
|
+
if (snapshot) {
|
|
4239
|
+
const obj = {};
|
|
4240
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4244
|
+
if (this.context.routeResults) {
|
|
4245
|
+
if (world && world.attach) {
|
|
4246
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4247
|
+
}
|
|
2870
4248
|
}
|
|
2871
|
-
|
|
4249
|
+
if (!process.env.TEMP_RUN) {
|
|
4250
|
+
const state = {
|
|
4251
|
+
world,
|
|
4252
|
+
locate: false,
|
|
4253
|
+
scroll: false,
|
|
4254
|
+
screenshot: true,
|
|
4255
|
+
highlight: true,
|
|
4256
|
+
type: Types.STEP_COMPLETE,
|
|
4257
|
+
text: "end of scenario",
|
|
4258
|
+
_text: "end of scenario",
|
|
4259
|
+
operation: "step_complete",
|
|
4260
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4261
|
+
};
|
|
4262
|
+
try {
|
|
4263
|
+
await _preCommand(state, this);
|
|
4264
|
+
}
|
|
4265
|
+
catch (e) {
|
|
4266
|
+
await _commandError(state, e, this);
|
|
4267
|
+
}
|
|
4268
|
+
finally {
|
|
4269
|
+
await _commandFinally(state, this);
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
networkAfterStep(this.stepName);
|
|
2872
4273
|
}
|
|
2873
4274
|
}
|
|
2874
4275
|
function createTimedPromise(promise, label) {
|
|
@@ -2876,156 +4277,5 @@ function createTimedPromise(promise, label) {
|
|
|
2876
4277
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2877
4278
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2878
4279
|
}
|
|
2879
|
-
const KEYBOARD_EVENTS = [
|
|
2880
|
-
"ALT",
|
|
2881
|
-
"AltGraph",
|
|
2882
|
-
"CapsLock",
|
|
2883
|
-
"Control",
|
|
2884
|
-
"Fn",
|
|
2885
|
-
"FnLock",
|
|
2886
|
-
"Hyper",
|
|
2887
|
-
"Meta",
|
|
2888
|
-
"NumLock",
|
|
2889
|
-
"ScrollLock",
|
|
2890
|
-
"Shift",
|
|
2891
|
-
"Super",
|
|
2892
|
-
"Symbol",
|
|
2893
|
-
"SymbolLock",
|
|
2894
|
-
"Enter",
|
|
2895
|
-
"Tab",
|
|
2896
|
-
"ArrowDown",
|
|
2897
|
-
"ArrowLeft",
|
|
2898
|
-
"ArrowRight",
|
|
2899
|
-
"ArrowUp",
|
|
2900
|
-
"End",
|
|
2901
|
-
"Home",
|
|
2902
|
-
"PageDown",
|
|
2903
|
-
"PageUp",
|
|
2904
|
-
"Backspace",
|
|
2905
|
-
"Clear",
|
|
2906
|
-
"Copy",
|
|
2907
|
-
"CrSel",
|
|
2908
|
-
"Cut",
|
|
2909
|
-
"Delete",
|
|
2910
|
-
"EraseEof",
|
|
2911
|
-
"ExSel",
|
|
2912
|
-
"Insert",
|
|
2913
|
-
"Paste",
|
|
2914
|
-
"Redo",
|
|
2915
|
-
"Undo",
|
|
2916
|
-
"Accept",
|
|
2917
|
-
"Again",
|
|
2918
|
-
"Attn",
|
|
2919
|
-
"Cancel",
|
|
2920
|
-
"ContextMenu",
|
|
2921
|
-
"Escape",
|
|
2922
|
-
"Execute",
|
|
2923
|
-
"Find",
|
|
2924
|
-
"Finish",
|
|
2925
|
-
"Help",
|
|
2926
|
-
"Pause",
|
|
2927
|
-
"Play",
|
|
2928
|
-
"Props",
|
|
2929
|
-
"Select",
|
|
2930
|
-
"ZoomIn",
|
|
2931
|
-
"ZoomOut",
|
|
2932
|
-
"BrightnessDown",
|
|
2933
|
-
"BrightnessUp",
|
|
2934
|
-
"Eject",
|
|
2935
|
-
"LogOff",
|
|
2936
|
-
"Power",
|
|
2937
|
-
"PowerOff",
|
|
2938
|
-
"PrintScreen",
|
|
2939
|
-
"Hibernate",
|
|
2940
|
-
"Standby",
|
|
2941
|
-
"WakeUp",
|
|
2942
|
-
"AllCandidates",
|
|
2943
|
-
"Alphanumeric",
|
|
2944
|
-
"CodeInput",
|
|
2945
|
-
"Compose",
|
|
2946
|
-
"Convert",
|
|
2947
|
-
"Dead",
|
|
2948
|
-
"FinalMode",
|
|
2949
|
-
"GroupFirst",
|
|
2950
|
-
"GroupLast",
|
|
2951
|
-
"GroupNext",
|
|
2952
|
-
"GroupPrevious",
|
|
2953
|
-
"ModeChange",
|
|
2954
|
-
"NextCandidate",
|
|
2955
|
-
"NonConvert",
|
|
2956
|
-
"PreviousCandidate",
|
|
2957
|
-
"Process",
|
|
2958
|
-
"SingleCandidate",
|
|
2959
|
-
"HangulMode",
|
|
2960
|
-
"HanjaMode",
|
|
2961
|
-
"JunjaMode",
|
|
2962
|
-
"Eisu",
|
|
2963
|
-
"Hankaku",
|
|
2964
|
-
"Hiragana",
|
|
2965
|
-
"HiraganaKatakana",
|
|
2966
|
-
"KanaMode",
|
|
2967
|
-
"KanjiMode",
|
|
2968
|
-
"Katakana",
|
|
2969
|
-
"Romaji",
|
|
2970
|
-
"Zenkaku",
|
|
2971
|
-
"ZenkakuHanaku",
|
|
2972
|
-
"F1",
|
|
2973
|
-
"F2",
|
|
2974
|
-
"F3",
|
|
2975
|
-
"F4",
|
|
2976
|
-
"F5",
|
|
2977
|
-
"F6",
|
|
2978
|
-
"F7",
|
|
2979
|
-
"F8",
|
|
2980
|
-
"F9",
|
|
2981
|
-
"F10",
|
|
2982
|
-
"F11",
|
|
2983
|
-
"F12",
|
|
2984
|
-
"Soft1",
|
|
2985
|
-
"Soft2",
|
|
2986
|
-
"Soft3",
|
|
2987
|
-
"Soft4",
|
|
2988
|
-
"ChannelDown",
|
|
2989
|
-
"ChannelUp",
|
|
2990
|
-
"Close",
|
|
2991
|
-
"MailForward",
|
|
2992
|
-
"MailReply",
|
|
2993
|
-
"MailSend",
|
|
2994
|
-
"MediaFastForward",
|
|
2995
|
-
"MediaPause",
|
|
2996
|
-
"MediaPlay",
|
|
2997
|
-
"MediaPlayPause",
|
|
2998
|
-
"MediaRecord",
|
|
2999
|
-
"MediaRewind",
|
|
3000
|
-
"MediaStop",
|
|
3001
|
-
"MediaTrackNext",
|
|
3002
|
-
"MediaTrackPrevious",
|
|
3003
|
-
"AudioBalanceLeft",
|
|
3004
|
-
"AudioBalanceRight",
|
|
3005
|
-
"AudioBassBoostDown",
|
|
3006
|
-
"AudioBassBoostToggle",
|
|
3007
|
-
"AudioBassBoostUp",
|
|
3008
|
-
"AudioFaderFront",
|
|
3009
|
-
"AudioFaderRear",
|
|
3010
|
-
"AudioSurroundModeNext",
|
|
3011
|
-
"AudioTrebleDown",
|
|
3012
|
-
"AudioTrebleUp",
|
|
3013
|
-
"AudioVolumeDown",
|
|
3014
|
-
"AudioVolumeMute",
|
|
3015
|
-
"AudioVolumeUp",
|
|
3016
|
-
"MicrophoneToggle",
|
|
3017
|
-
"MicrophoneVolumeDown",
|
|
3018
|
-
"MicrophoneVolumeMute",
|
|
3019
|
-
"MicrophoneVolumeUp",
|
|
3020
|
-
"TV",
|
|
3021
|
-
"TV3DMode",
|
|
3022
|
-
"TVAntennaCable",
|
|
3023
|
-
"TVAudioDescription",
|
|
3024
|
-
];
|
|
3025
|
-
function unEscapeString(str) {
|
|
3026
|
-
const placeholder = "__NEWLINE__";
|
|
3027
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
3028
|
-
return str;
|
|
3029
|
-
}
|
|
3030
4280
|
export { StableBrowser };
|
|
3031
4281
|
//# sourceMappingURL=stable_browser.js.map
|