automation_model 1.0.450-dev → 1.0.450
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 +270 -49
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +7 -3
- package/lib/browser_manager.js +176 -49
- 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 +117 -11
- 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 +331 -0
- package/lib/network.js.map +1 -0
- package/lib/route.d.ts +21 -0
- package/lib/route.js +390 -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 +2560 -1302
- 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 +7 -0
- package/lib/test_context.js +15 -10
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +35 -2
- package/lib/utils.js +683 -11
- package/lib/utils.js.map +1 -1
- package/package.json +21 -11
package/lib/stable_browser.js
CHANGED
|
@@ -2,34 +2,50 @@
|
|
|
2
2
|
import { expect } from "@playwright/test";
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
4
|
import fs from "fs";
|
|
5
|
+
import { Jimp } from "jimp";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import reg_parser from "regex-parser";
|
|
7
|
-
import sharp from "sharp";
|
|
8
8
|
import { findDateAlternatives, findNumberAlternatives } from "./analyze_helper.js";
|
|
9
9
|
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,16 +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);
|
|
148
|
+
if (!context.pageLoading) {
|
|
149
|
+
context.pageLoading = { status: false };
|
|
150
|
+
}
|
|
151
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
152
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
153
|
+
}
|
|
92
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
|
+
}
|
|
93
160
|
context.pageLoading.status = true;
|
|
94
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
|
+
}
|
|
95
170
|
context.page = page;
|
|
96
171
|
context.pages.push(page);
|
|
172
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
173
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
97
174
|
page.on("close", async () => {
|
|
98
175
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
99
176
|
this.context.pages.pop();
|
|
@@ -125,7 +202,7 @@ class StableBrowser {
|
|
|
125
202
|
}
|
|
126
203
|
let newContextCreated = false;
|
|
127
204
|
if (!apps[appName]) {
|
|
128
|
-
let newContext = await getContext(null, false, 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);
|
|
129
206
|
newContextCreated = true;
|
|
130
207
|
apps[appName] = {
|
|
131
208
|
context: newContext,
|
|
@@ -134,38 +211,47 @@ class StableBrowser {
|
|
|
134
211
|
};
|
|
135
212
|
}
|
|
136
213
|
const tempContext = {};
|
|
137
|
-
|
|
138
|
-
|
|
214
|
+
_copyContext(this, tempContext);
|
|
215
|
+
_copyContext(apps[appName], this);
|
|
139
216
|
apps[this.appName] = tempContext;
|
|
140
217
|
this.appName = appName;
|
|
141
218
|
if (newContextCreated) {
|
|
142
219
|
this.registerEventListeners(this.context);
|
|
143
220
|
await this.goto(this.context.environment.baseUrl);
|
|
144
|
-
|
|
221
|
+
if (!this.fastMode) {
|
|
222
|
+
await this.waitForPageLoad();
|
|
223
|
+
}
|
|
145
224
|
}
|
|
146
225
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
155
236
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
247
|
}
|
|
160
|
-
|
|
161
|
-
return path.join(logFolder, fileName);
|
|
248
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
162
249
|
}
|
|
163
250
|
registerConsoleLogListener(page, context) {
|
|
164
251
|
if (!this.context.webLogger) {
|
|
165
252
|
this.context.webLogger = [];
|
|
166
253
|
}
|
|
167
254
|
page.on("console", async (msg) => {
|
|
168
|
-
var _a;
|
|
169
255
|
const obj = {
|
|
170
256
|
type: msg.type(),
|
|
171
257
|
text: msg.text(),
|
|
@@ -173,7 +259,9 @@ class StableBrowser {
|
|
|
173
259
|
time: new Date().toISOString(),
|
|
174
260
|
};
|
|
175
261
|
this.context.webLogger.push(obj);
|
|
176
|
-
|
|
262
|
+
if (msg.type() === "error") {
|
|
263
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
264
|
+
}
|
|
177
265
|
});
|
|
178
266
|
}
|
|
179
267
|
registerRequestListener(page, context, logFile) {
|
|
@@ -181,7 +269,6 @@ class StableBrowser {
|
|
|
181
269
|
this.context.networkLogger = [];
|
|
182
270
|
}
|
|
183
271
|
page.on("request", async (data) => {
|
|
184
|
-
var _a;
|
|
185
272
|
const startTime = new Date().getTime();
|
|
186
273
|
try {
|
|
187
274
|
const pageUrl = new URL(page.url());
|
|
@@ -206,10 +293,10 @@ class StableBrowser {
|
|
|
206
293
|
startTime,
|
|
207
294
|
};
|
|
208
295
|
context.networkLogger.push(obj);
|
|
209
|
-
|
|
296
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
210
297
|
}
|
|
211
298
|
catch (error) {
|
|
212
|
-
console.error("Error in request listener", error);
|
|
299
|
+
// console.error("Error in request listener", error);
|
|
213
300
|
context.networkLogger.push({
|
|
214
301
|
error: "not able to listen",
|
|
215
302
|
message: error.message,
|
|
@@ -223,69 +310,110 @@ class StableBrowser {
|
|
|
223
310
|
// async closeUnexpectedPopups() {
|
|
224
311
|
// await closeUnexpectedPopups(this.page);
|
|
225
312
|
// }
|
|
226
|
-
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);
|
|
227
318
|
if (!url.startsWith("http")) {
|
|
228
319
|
url = "https://" + url;
|
|
229
320
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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);
|
|
240
340
|
}
|
|
241
|
-
|
|
242
|
-
|
|
341
|
+
catch (error) {
|
|
342
|
+
console.error("Error on goto", error);
|
|
343
|
+
_commandError(state, error, this);
|
|
243
344
|
}
|
|
244
|
-
|
|
245
|
-
|
|
345
|
+
finally {
|
|
346
|
+
await _commandFinally(state, this);
|
|
246
347
|
}
|
|
247
348
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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);
|
|
251
369
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// remove the _ prefix
|
|
256
|
-
regValue = key.substring(1);
|
|
257
|
-
}
|
|
258
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
370
|
+
catch (error) {
|
|
371
|
+
console.error("Error on goBack", error);
|
|
372
|
+
_commandError(state, error, this);
|
|
259
373
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
263
|
-
// check if not null
|
|
264
|
-
if (!locator) {
|
|
265
|
-
return locator;
|
|
374
|
+
finally {
|
|
375
|
+
await _commandFinally(state, this);
|
|
266
376
|
}
|
|
267
|
-
// clone the locator
|
|
268
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
269
|
-
this.scanAndManipulate(locator, _params);
|
|
270
|
-
return locator;
|
|
271
377
|
}
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
406
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
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);
|
|
284
415
|
}
|
|
285
416
|
}
|
|
286
|
-
}
|
|
287
|
-
_getLocator(locator, scope, _params) {
|
|
288
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
289
417
|
let locatorReturn;
|
|
290
418
|
if (locator.role) {
|
|
291
419
|
if (locator.role[1].nameReg) {
|
|
@@ -293,7 +421,7 @@ class StableBrowser {
|
|
|
293
421
|
delete locator.role[1].nameReg;
|
|
294
422
|
}
|
|
295
423
|
// if (locator.role[1].name) {
|
|
296
|
-
// locator.role[1].name =
|
|
424
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
297
425
|
// }
|
|
298
426
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
299
427
|
}
|
|
@@ -312,7 +440,7 @@ class StableBrowser {
|
|
|
312
440
|
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
313
441
|
}
|
|
314
442
|
}
|
|
315
|
-
if (locator
|
|
443
|
+
if (locator?.engine) {
|
|
316
444
|
if (locator.engine === "css") {
|
|
317
445
|
locatorReturn = scope.locator(locator.selector);
|
|
318
446
|
}
|
|
@@ -333,192 +461,180 @@ class StableBrowser {
|
|
|
333
461
|
return locatorReturn;
|
|
334
462
|
}
|
|
335
463
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
336
|
-
|
|
464
|
+
if (css && css.locator) {
|
|
465
|
+
css = css.locator;
|
|
466
|
+
}
|
|
467
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
337
468
|
if (result.elementCount === 0) {
|
|
338
469
|
return;
|
|
339
470
|
}
|
|
340
|
-
let textElementCss = "[data-blinq-id
|
|
471
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
341
472
|
// css climb to parent element
|
|
342
473
|
const climbArray = [];
|
|
343
474
|
for (let i = 0; i < climb; i++) {
|
|
344
475
|
climbArray.push("..");
|
|
345
476
|
}
|
|
346
477
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
// Iterate over child nodes
|
|
371
|
-
element.childNodes.forEach((child) => {
|
|
372
|
-
// Recursively call the function for each child node
|
|
373
|
-
document.collectAllShadowDomElements(child, result);
|
|
374
|
-
});
|
|
375
|
-
return result;
|
|
376
|
-
}
|
|
377
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
378
|
-
if (!tag) {
|
|
379
|
-
tag = "*";
|
|
380
|
-
}
|
|
381
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
382
|
-
let shadowHosts = [];
|
|
383
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
384
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
385
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
386
|
-
if (!shadowElement) {
|
|
387
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
391
|
-
elements = elements.concat(shadowElements);
|
|
392
|
-
}
|
|
393
|
-
let randomToken = null;
|
|
394
|
-
const foundElements = [];
|
|
395
|
-
if (regex) {
|
|
396
|
-
let regexpSearch = new RegExp(text, "im");
|
|
397
|
-
for (let i = 0; i < elements.length; i++) {
|
|
398
|
-
const element = elements[i];
|
|
399
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
400
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
401
|
-
foundElements.push(element);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
text = text.trim();
|
|
407
|
-
for (let i = 0; i < elements.length; i++) {
|
|
408
|
-
const element = elements[i];
|
|
409
|
-
if (partial) {
|
|
410
|
-
if ((element.innerText && element.innerText.trim().includes(text)) ||
|
|
411
|
-
(element.value && element.value.includes(text))) {
|
|
412
|
-
foundElements.push(element);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
417
|
-
(element.value && element.value === text)) {
|
|
418
|
-
foundElements.push(element);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
let noChildElements = [];
|
|
424
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
425
|
-
let element = foundElements[i];
|
|
426
|
-
let hasChild = false;
|
|
427
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
428
|
-
if (i === j) {
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
if (isParent(element, foundElements[j])) {
|
|
432
|
-
hasChild = true;
|
|
433
|
-
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;
|
|
434
500
|
}
|
|
435
501
|
}
|
|
436
|
-
if (!
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
let elementCount = 0;
|
|
441
|
-
if (noChildElements.length > 0) {
|
|
442
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
443
|
-
if (randomToken === null) {
|
|
444
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
445
|
-
}
|
|
446
|
-
let element = noChildElements[i];
|
|
447
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
448
|
-
elementCount++;
|
|
502
|
+
if (!el.setAttribute) {
|
|
503
|
+
el = el.parentElement;
|
|
449
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;
|
|
450
515
|
}
|
|
451
|
-
|
|
452
|
-
}
|
|
516
|
+
tagCount++;
|
|
517
|
+
}
|
|
518
|
+
return { elementCount: tagCount, randomToken };
|
|
453
519
|
}
|
|
454
|
-
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
|
+
}
|
|
455
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);
|
|
456
539
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
457
540
|
let locator = null;
|
|
458
541
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
459
|
-
|
|
542
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
543
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
460
544
|
if (!locatorString) {
|
|
545
|
+
info.failCause.textNotFound = true;
|
|
546
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
461
547
|
return;
|
|
462
548
|
}
|
|
463
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
549
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
464
550
|
}
|
|
465
551
|
else if (locatorSearch.text) {
|
|
466
|
-
let
|
|
552
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
553
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
467
554
|
if (result.elementCount === 0) {
|
|
555
|
+
info.failCause.textNotFound = true;
|
|
556
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
468
557
|
return;
|
|
469
558
|
}
|
|
470
|
-
locatorSearch.css = "[data-blinq-id
|
|
559
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
471
560
|
if (locatorSearch.childCss) {
|
|
472
561
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
473
562
|
}
|
|
474
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
563
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
475
564
|
}
|
|
476
565
|
else {
|
|
477
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
566
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
478
567
|
}
|
|
479
568
|
// let cssHref = false;
|
|
480
569
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
481
570
|
// cssHref = true;
|
|
482
571
|
// }
|
|
483
572
|
let count = await locator.count();
|
|
573
|
+
if (count > 0 && !info.failCause.count) {
|
|
574
|
+
info.failCause.count = count;
|
|
575
|
+
}
|
|
484
576
|
//info.log += "total elements found " + count + "\n";
|
|
485
577
|
//let visibleCount = 0;
|
|
486
578
|
let visibleLocator = null;
|
|
487
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
579
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
488
580
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
581
|
+
if (info.locatorLog) {
|
|
582
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
583
|
+
}
|
|
489
584
|
return;
|
|
490
585
|
}
|
|
586
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
587
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
588
|
+
}
|
|
491
589
|
for (let j = 0; j < count; j++) {
|
|
492
590
|
let visible = await locator.nth(j).isVisible();
|
|
493
591
|
const enabled = await locator.nth(j).isEnabled();
|
|
494
592
|
if (!visibleOnly) {
|
|
495
593
|
visible = true;
|
|
496
594
|
}
|
|
497
|
-
if (visible && enabled) {
|
|
595
|
+
if (visible && (allowDisabled || enabled)) {
|
|
498
596
|
foundLocators.push(locator.nth(j));
|
|
597
|
+
if (info.locatorLog) {
|
|
598
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
599
|
+
}
|
|
499
600
|
}
|
|
500
|
-
else {
|
|
601
|
+
else if (logErrors) {
|
|
602
|
+
info.failCause.visible = visible;
|
|
603
|
+
info.failCause.enabled = enabled;
|
|
501
604
|
if (!info.printMessages) {
|
|
502
605
|
info.printMessages = {};
|
|
503
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
|
+
}
|
|
504
615
|
if (!info.printMessages[j.toString()]) {
|
|
505
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
616
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
506
617
|
info.printMessages[j.toString()] = true;
|
|
507
618
|
}
|
|
508
619
|
}
|
|
509
620
|
}
|
|
510
621
|
}
|
|
511
622
|
async closeUnexpectedPopups(info, _params) {
|
|
623
|
+
if (!info) {
|
|
624
|
+
info = {};
|
|
625
|
+
info.failCause = {};
|
|
626
|
+
info.log = "";
|
|
627
|
+
}
|
|
512
628
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
513
629
|
if (!info) {
|
|
514
630
|
info = {};
|
|
515
631
|
}
|
|
516
|
-
info.log += "scan for popup handlers" + "\n";
|
|
632
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
517
633
|
const handlerGroup = [];
|
|
518
634
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
519
635
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
520
636
|
}
|
|
521
|
-
const scopes =
|
|
637
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
522
638
|
let result = null;
|
|
523
639
|
let scope = null;
|
|
524
640
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -540,55 +656,108 @@ class StableBrowser {
|
|
|
540
656
|
}
|
|
541
657
|
if (result.foundElements.length > 0) {
|
|
542
658
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
}
|
|
546
674
|
return { rerun: true };
|
|
547
675
|
}
|
|
548
676
|
}
|
|
549
677
|
}
|
|
550
678
|
return { rerun: false };
|
|
551
679
|
}
|
|
552
|
-
async _locate(selectors, info, _params, timeout =
|
|
680
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
681
|
+
if (!timeout) {
|
|
682
|
+
timeout = 30000;
|
|
683
|
+
}
|
|
553
684
|
for (let i = 0; i < 3; i++) {
|
|
554
685
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
555
686
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
556
687
|
let selector = selectors.locators[j];
|
|
557
688
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
558
689
|
}
|
|
559
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
690
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
560
691
|
if (!element.rerun) {
|
|
561
|
-
|
|
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);
|
|
562
714
|
}
|
|
563
715
|
}
|
|
564
716
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
565
717
|
}
|
|
566
|
-
async
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
718
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
719
|
+
if (!info) {
|
|
720
|
+
info = {};
|
|
721
|
+
info.failCause = {};
|
|
722
|
+
info.log = "";
|
|
723
|
+
}
|
|
724
|
+
let startTime = Date.now();
|
|
572
725
|
let scope = this.page;
|
|
726
|
+
if (selectors.frame) {
|
|
727
|
+
return selectors.frame;
|
|
728
|
+
}
|
|
573
729
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
574
|
-
const findFrame = (frame, framescope) => {
|
|
730
|
+
const findFrame = async (frame, framescope) => {
|
|
575
731
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
576
732
|
let frameLocator = frame.selectors[i];
|
|
577
733
|
if (frameLocator.css) {
|
|
578
|
-
|
|
579
|
-
|
|
734
|
+
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
735
|
+
if (frameLocator.index) {
|
|
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);
|
|
747
|
+
}
|
|
580
748
|
}
|
|
581
749
|
}
|
|
582
750
|
if (frame.children) {
|
|
583
|
-
return findFrame(frame.children, framescope);
|
|
751
|
+
return await findFrame(frame.children, framescope);
|
|
584
752
|
}
|
|
585
753
|
return framescope;
|
|
586
754
|
};
|
|
587
|
-
|
|
755
|
+
let fLocator = null;
|
|
588
756
|
while (true) {
|
|
589
757
|
let frameFound = false;
|
|
590
758
|
if (selectors.nestFrmLoc) {
|
|
591
|
-
|
|
759
|
+
fLocator = selectors.nestFrmLoc;
|
|
760
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
592
761
|
frameFound = true;
|
|
593
762
|
break;
|
|
594
763
|
}
|
|
@@ -596,6 +765,7 @@ class StableBrowser {
|
|
|
596
765
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
597
766
|
let frameLocator = selectors.frameLocators[i];
|
|
598
767
|
if (frameLocator.css) {
|
|
768
|
+
fLocator = frameLocator.css;
|
|
599
769
|
scope = scope.frameLocator(frameLocator.css);
|
|
600
770
|
frameFound = true;
|
|
601
771
|
break;
|
|
@@ -603,20 +773,55 @@ class StableBrowser {
|
|
|
603
773
|
}
|
|
604
774
|
}
|
|
605
775
|
if (!frameFound && selectors.iframe_src) {
|
|
776
|
+
fLocator = selectors.iframe_src;
|
|
606
777
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
607
778
|
}
|
|
608
779
|
if (!scope) {
|
|
609
|
-
info
|
|
610
|
-
|
|
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}"`;
|
|
611
787
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
612
788
|
}
|
|
613
789
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
614
790
|
}
|
|
615
791
|
else {
|
|
792
|
+
if (info && info.locatorLog) {
|
|
793
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
794
|
+
}
|
|
616
795
|
break;
|
|
617
796
|
}
|
|
618
797
|
}
|
|
619
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);
|
|
824
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
620
825
|
let selectorsLocators = null;
|
|
621
826
|
selectorsLocators = selectors.locators;
|
|
622
827
|
// group selectors by priority
|
|
@@ -652,18 +857,13 @@ class StableBrowser {
|
|
|
652
857
|
}
|
|
653
858
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
654
859
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
655
|
-
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);
|
|
656
861
|
if (result.foundElements.length === 0) {
|
|
657
862
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
658
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
659
|
-
}
|
|
660
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
661
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
863
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
662
864
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
666
|
-
}
|
|
865
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
866
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
667
867
|
}
|
|
668
868
|
let foundElements = result.foundElements;
|
|
669
869
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -703,24 +903,43 @@ class StableBrowser {
|
|
|
703
903
|
return maxCountElement.locator;
|
|
704
904
|
}
|
|
705
905
|
}
|
|
706
|
-
if (
|
|
906
|
+
if (Date.now() - startTime > timeout) {
|
|
707
907
|
break;
|
|
708
908
|
}
|
|
709
|
-
if (
|
|
710
|
-
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";
|
|
711
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
|
+
}
|
|
712
916
|
}
|
|
713
|
-
if (
|
|
714
|
-
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";
|
|
715
919
|
visibleOnly = false;
|
|
716
920
|
}
|
|
717
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
|
+
}
|
|
718
927
|
}
|
|
719
928
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
720
|
-
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
|
+
}
|
|
721
940
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
722
941
|
}
|
|
723
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
942
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
724
943
|
let foundElements = [];
|
|
725
944
|
const result = {
|
|
726
945
|
foundElements: foundElements,
|
|
@@ -728,17 +947,20 @@ class StableBrowser {
|
|
|
728
947
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
729
948
|
let foundLocators = [];
|
|
730
949
|
try {
|
|
731
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
950
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
732
951
|
}
|
|
733
952
|
catch (e) {
|
|
734
|
-
this
|
|
735
|
-
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);
|
|
736
956
|
foundLocators = [];
|
|
737
957
|
try {
|
|
738
|
-
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);
|
|
739
959
|
}
|
|
740
960
|
catch (e) {
|
|
741
|
-
|
|
961
|
+
if (logErrors) {
|
|
962
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
963
|
+
}
|
|
742
964
|
}
|
|
743
965
|
}
|
|
744
966
|
if (foundLocators.length === 1) {
|
|
@@ -749,270 +971,350 @@ class StableBrowser {
|
|
|
749
971
|
});
|
|
750
972
|
result.locatorIndex = i;
|
|
751
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
|
+
}
|
|
752
1011
|
}
|
|
753
1012
|
return result;
|
|
754
1013
|
}
|
|
755
|
-
async
|
|
756
|
-
|
|
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);
|
|
757
1028
|
const startTime = Date.now();
|
|
758
|
-
|
|
759
|
-
|
|
1029
|
+
let timeout = 30000;
|
|
1030
|
+
if (options && options.timeout) {
|
|
1031
|
+
timeout = options.timeout;
|
|
760
1032
|
}
|
|
761
|
-
|
|
762
|
-
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
763
|
-
info.operation = "click";
|
|
764
|
-
info.selectors = selectors;
|
|
765
|
-
let error = null;
|
|
766
|
-
let screenshotId = null;
|
|
767
|
-
let screenshotPath = null;
|
|
768
|
-
try {
|
|
769
|
-
let element = await this._locate(selectors, info, _params);
|
|
770
|
-
await this.scrollIfNeeded(element, info);
|
|
771
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1033
|
+
while (true) {
|
|
772
1034
|
try {
|
|
773
|
-
await this.
|
|
774
|
-
|
|
775
|
-
|
|
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
|
+
}
|
|
776
1048
|
}
|
|
777
1049
|
catch (e) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
+
}
|
|
783
1059
|
}
|
|
784
|
-
await
|
|
785
|
-
|
|
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;
|
|
786
1131
|
}
|
|
787
1132
|
catch (e) {
|
|
788
|
-
|
|
789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
790
|
-
info.screenshotPath = screenshotPath;
|
|
791
|
-
Object.assign(e, { info: info });
|
|
792
|
-
error = e;
|
|
793
|
-
throw e;
|
|
1133
|
+
await _commandError(state, e, this);
|
|
794
1134
|
}
|
|
795
1135
|
finally {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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));
|
|
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);
|
|
816
1168
|
}
|
|
1169
|
+
return found;
|
|
817
1170
|
}
|
|
818
1171
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
+
};
|
|
829
1183
|
try {
|
|
830
|
-
|
|
831
|
-
|
|
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));
|
|
832
1188
|
try {
|
|
833
|
-
|
|
834
|
-
|
|
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 });
|
|
835
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);
|
|
836
1198
|
}
|
|
837
1199
|
catch (e) {
|
|
838
1200
|
if (e.message && e.message.includes("did not change its state")) {
|
|
839
1201
|
this.logger.info("element did not change its state, ignoring...");
|
|
840
1202
|
}
|
|
841
1203
|
else {
|
|
842
|
-
//await this.closeUnexpectedPopups();
|
|
843
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
844
|
-
element = await this._locate(selectors, info, _params);
|
|
845
|
-
await element.setChecked(checked, { timeout: 5000, force: true });
|
|
846
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
|
+
}
|
|
847
1226
|
}
|
|
848
1227
|
}
|
|
849
1228
|
await this.waitForPageLoad();
|
|
850
|
-
return info;
|
|
1229
|
+
return state.info;
|
|
851
1230
|
}
|
|
852
1231
|
catch (e) {
|
|
853
|
-
|
|
854
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
855
|
-
info.screenshotPath = screenshotPath;
|
|
856
|
-
Object.assign(e, { info: info });
|
|
857
|
-
error = e;
|
|
858
|
-
throw e;
|
|
1232
|
+
await _commandError(state, e, this);
|
|
859
1233
|
}
|
|
860
1234
|
finally {
|
|
861
|
-
|
|
862
|
-
this._reportToWorld(world, {
|
|
863
|
-
element_name: selectors.element_name,
|
|
864
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
865
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
866
|
-
screenshotId,
|
|
867
|
-
result: error
|
|
868
|
-
? {
|
|
869
|
-
status: "FAILED",
|
|
870
|
-
startTime,
|
|
871
|
-
endTime,
|
|
872
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
873
|
-
}
|
|
874
|
-
: {
|
|
875
|
-
status: "PASSED",
|
|
876
|
-
startTime,
|
|
877
|
-
endTime,
|
|
878
|
-
},
|
|
879
|
-
info: info,
|
|
880
|
-
});
|
|
1235
|
+
await _commandFinally(state, this);
|
|
881
1236
|
}
|
|
882
1237
|
}
|
|
883
1238
|
async hover(selectors, _params, options = {}, world = null) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
+
};
|
|
893
1250
|
try {
|
|
894
|
-
|
|
895
|
-
(
|
|
896
|
-
|
|
897
|
-
await this._highlightElements(element);
|
|
898
|
-
await element.hover();
|
|
899
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
900
|
-
}
|
|
901
|
-
catch (e) {
|
|
902
|
-
//await this.closeUnexpectedPopups();
|
|
903
|
-
info.log += "hover failed, will try again" + "\n";
|
|
904
|
-
element = await this._locate(selectors, info, _params);
|
|
905
|
-
await element.hover({ timeout: 10000 });
|
|
906
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
907
|
-
}
|
|
1251
|
+
await _preCommand(state, this);
|
|
1252
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1253
|
+
await _screenshot(state, this);
|
|
908
1254
|
await this.waitForPageLoad();
|
|
909
|
-
return info;
|
|
1255
|
+
return state.info;
|
|
910
1256
|
}
|
|
911
1257
|
catch (e) {
|
|
912
|
-
|
|
913
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
914
|
-
info.screenshotPath = screenshotPath;
|
|
915
|
-
Object.assign(e, { info: info });
|
|
916
|
-
error = e;
|
|
917
|
-
throw e;
|
|
1258
|
+
await _commandError(state, e, this);
|
|
918
1259
|
}
|
|
919
1260
|
finally {
|
|
920
|
-
|
|
921
|
-
this._reportToWorld(world, {
|
|
922
|
-
element_name: selectors.element_name,
|
|
923
|
-
type: Types.HOVER,
|
|
924
|
-
text: `Hover element`,
|
|
925
|
-
screenshotId,
|
|
926
|
-
result: error
|
|
927
|
-
? {
|
|
928
|
-
status: "FAILED",
|
|
929
|
-
startTime,
|
|
930
|
-
endTime,
|
|
931
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
932
|
-
}
|
|
933
|
-
: {
|
|
934
|
-
status: "PASSED",
|
|
935
|
-
startTime,
|
|
936
|
-
endTime,
|
|
937
|
-
},
|
|
938
|
-
info: info,
|
|
939
|
-
});
|
|
1261
|
+
await _commandFinally(state, this);
|
|
940
1262
|
}
|
|
941
1263
|
}
|
|
942
1264
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
943
|
-
this._validateSelectors(selectors);
|
|
944
1265
|
if (!values) {
|
|
945
1266
|
throw new Error("values is null");
|
|
946
1267
|
}
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
+
};
|
|
955
1280
|
try {
|
|
956
|
-
|
|
957
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1281
|
+
await _preCommand(state, this);
|
|
958
1282
|
try {
|
|
959
|
-
await
|
|
960
|
-
await element.selectOption(values);
|
|
1283
|
+
await state.element.selectOption(values);
|
|
961
1284
|
}
|
|
962
1285
|
catch (e) {
|
|
963
1286
|
//await this.closeUnexpectedPopups();
|
|
964
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
965
|
-
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 });
|
|
966
1289
|
}
|
|
967
1290
|
await this.waitForPageLoad();
|
|
968
|
-
return info;
|
|
1291
|
+
return state.info;
|
|
969
1292
|
}
|
|
970
1293
|
catch (e) {
|
|
971
|
-
|
|
972
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
973
|
-
info.screenshotPath = screenshotPath;
|
|
974
|
-
Object.assign(e, { info: info });
|
|
975
|
-
this.logger.info("click failed, will try next selector");
|
|
976
|
-
error = e;
|
|
977
|
-
throw e;
|
|
1294
|
+
await _commandError(state, e, this);
|
|
978
1295
|
}
|
|
979
1296
|
finally {
|
|
980
|
-
|
|
981
|
-
this._reportToWorld(world, {
|
|
982
|
-
element_name: selectors.element_name,
|
|
983
|
-
type: Types.SELECT,
|
|
984
|
-
text: `Select option: ${values}`,
|
|
985
|
-
value: values.toString(),
|
|
986
|
-
screenshotId,
|
|
987
|
-
result: error
|
|
988
|
-
? {
|
|
989
|
-
status: "FAILED",
|
|
990
|
-
startTime,
|
|
991
|
-
endTime,
|
|
992
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
993
|
-
}
|
|
994
|
-
: {
|
|
995
|
-
status: "PASSED",
|
|
996
|
-
startTime,
|
|
997
|
-
endTime,
|
|
998
|
-
},
|
|
999
|
-
info: info,
|
|
1000
|
-
});
|
|
1297
|
+
await _commandFinally(state, this);
|
|
1001
1298
|
}
|
|
1002
1299
|
}
|
|
1003
1300
|
async type(_value, _params = null, options = {}, world = null) {
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
+
};
|
|
1013
1315
|
try {
|
|
1014
|
-
|
|
1015
|
-
const valueSegment =
|
|
1316
|
+
await _preCommand(state, this);
|
|
1317
|
+
const valueSegment = state.value.split("&&");
|
|
1016
1318
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1017
1319
|
if (i > 0) {
|
|
1018
1320
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1032,134 +1334,77 @@ class StableBrowser {
|
|
|
1032
1334
|
await this.page.keyboard.type(value);
|
|
1033
1335
|
}
|
|
1034
1336
|
}
|
|
1035
|
-
return info;
|
|
1337
|
+
return state.info;
|
|
1036
1338
|
}
|
|
1037
1339
|
catch (e) {
|
|
1038
|
-
|
|
1039
|
-
this.logger.error("type failed " + info.log);
|
|
1040
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1041
|
-
info.screenshotPath = screenshotPath;
|
|
1042
|
-
Object.assign(e, { info: info });
|
|
1043
|
-
error = e;
|
|
1044
|
-
throw e;
|
|
1340
|
+
await _commandError(state, e, this);
|
|
1045
1341
|
}
|
|
1046
1342
|
finally {
|
|
1047
|
-
|
|
1048
|
-
this._reportToWorld(world, {
|
|
1049
|
-
type: Types.TYPE_PRESS,
|
|
1050
|
-
screenshotId,
|
|
1051
|
-
value: _value,
|
|
1052
|
-
text: `type value: ${_value}`,
|
|
1053
|
-
result: error
|
|
1054
|
-
? {
|
|
1055
|
-
status: "FAILED",
|
|
1056
|
-
startTime,
|
|
1057
|
-
endTime,
|
|
1058
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1059
|
-
}
|
|
1060
|
-
: {
|
|
1061
|
-
status: "PASSED",
|
|
1062
|
-
startTime,
|
|
1063
|
-
endTime,
|
|
1064
|
-
},
|
|
1065
|
-
info: info,
|
|
1066
|
-
});
|
|
1343
|
+
await _commandFinally(state, this);
|
|
1067
1344
|
}
|
|
1068
1345
|
}
|
|
1069
1346
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
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
|
+
};
|
|
1082
1358
|
try {
|
|
1083
|
-
|
|
1084
|
-
let
|
|
1085
|
-
await this.scrollIfNeeded(element, info);
|
|
1086
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1087
|
-
await this._highlightElements(element);
|
|
1359
|
+
await _preCommand(state, this);
|
|
1360
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1088
1361
|
try {
|
|
1089
|
-
await element.evaluateHandle((el, value) => {
|
|
1362
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1090
1363
|
el.value = value;
|
|
1091
1364
|
}, value);
|
|
1092
1365
|
}
|
|
1093
1366
|
catch (error) {
|
|
1094
1367
|
this.logger.error("setInputValue failed, will try again");
|
|
1095
|
-
|
|
1096
|
-
info.
|
|
1097
|
-
|
|
1098
|
-
await element.evaluateHandle((el, value) => {
|
|
1368
|
+
await _screenshot(state, this);
|
|
1369
|
+
Object.assign(error, { info: state.info });
|
|
1370
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1099
1371
|
el.value = value;
|
|
1100
1372
|
});
|
|
1101
1373
|
}
|
|
1102
1374
|
}
|
|
1103
1375
|
catch (e) {
|
|
1104
|
-
|
|
1105
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1106
|
-
info.screenshotPath = screenshotPath;
|
|
1107
|
-
Object.assign(e, { info: info });
|
|
1108
|
-
error = e;
|
|
1109
|
-
throw e;
|
|
1376
|
+
await _commandError(state, e, this);
|
|
1110
1377
|
}
|
|
1111
1378
|
finally {
|
|
1112
|
-
|
|
1113
|
-
this._reportToWorld(world, {
|
|
1114
|
-
element_name: selectors.element_name,
|
|
1115
|
-
type: Types.SET_INPUT,
|
|
1116
|
-
text: `Set input value`,
|
|
1117
|
-
value: value,
|
|
1118
|
-
screenshotId,
|
|
1119
|
-
result: error
|
|
1120
|
-
? {
|
|
1121
|
-
status: "FAILED",
|
|
1122
|
-
startTime,
|
|
1123
|
-
endTime,
|
|
1124
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1125
|
-
}
|
|
1126
|
-
: {
|
|
1127
|
-
status: "PASSED",
|
|
1128
|
-
startTime,
|
|
1129
|
-
endTime,
|
|
1130
|
-
},
|
|
1131
|
-
info: info,
|
|
1132
|
-
});
|
|
1379
|
+
await _commandFinally(state, this);
|
|
1133
1380
|
}
|
|
1134
1381
|
}
|
|
1135
1382
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
+
};
|
|
1146
1396
|
try {
|
|
1147
|
-
|
|
1148
|
-
let element = await this._locate(selectors, info, _params);
|
|
1149
|
-
//insert red border around the element
|
|
1150
|
-
await this.scrollIfNeeded(element, info);
|
|
1151
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1152
|
-
await this._highlightElements(element);
|
|
1397
|
+
await _preCommand(state, this);
|
|
1153
1398
|
try {
|
|
1154
|
-
await element
|
|
1399
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1155
1400
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1156
1401
|
if (format) {
|
|
1157
|
-
value = dayjs(value).format(format);
|
|
1158
|
-
await element.fill(value);
|
|
1402
|
+
state.value = dayjs(state.value).format(format);
|
|
1403
|
+
await state.element.fill(state.value);
|
|
1159
1404
|
}
|
|
1160
1405
|
else {
|
|
1161
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1162
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1406
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1407
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1163
1408
|
el.value = ""; // clear input
|
|
1164
1409
|
el.value = dateTimeValue;
|
|
1165
1410
|
}, dateTimeValue);
|
|
@@ -1172,20 +1417,19 @@ class StableBrowser {
|
|
|
1172
1417
|
}
|
|
1173
1418
|
catch (err) {
|
|
1174
1419
|
//await this.closeUnexpectedPopups();
|
|
1175
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1420
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1176
1421
|
this.logger.info("Trying again");
|
|
1177
|
-
|
|
1178
|
-
info.
|
|
1179
|
-
Object.assign(err, { info: info });
|
|
1422
|
+
await _screenshot(state, this);
|
|
1423
|
+
Object.assign(err, { info: state.info });
|
|
1180
1424
|
await element.click();
|
|
1181
1425
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1182
1426
|
if (format) {
|
|
1183
|
-
value = dayjs(value).format(format);
|
|
1184
|
-
await element.fill(value);
|
|
1427
|
+
state.value = dayjs(state.value).format(format);
|
|
1428
|
+
await state.element.fill(state.value);
|
|
1185
1429
|
}
|
|
1186
1430
|
else {
|
|
1187
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1188
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1431
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1432
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1189
1433
|
el.value = ""; // clear input
|
|
1190
1434
|
el.value = dateTimeValue;
|
|
1191
1435
|
}, dateTimeValue);
|
|
@@ -1198,85 +1442,63 @@ class StableBrowser {
|
|
|
1198
1442
|
}
|
|
1199
1443
|
}
|
|
1200
1444
|
catch (e) {
|
|
1201
|
-
|
|
1202
|
-
throw e;
|
|
1445
|
+
await _commandError(state, e, this);
|
|
1203
1446
|
}
|
|
1204
1447
|
finally {
|
|
1205
|
-
|
|
1206
|
-
this._reportToWorld(world, {
|
|
1207
|
-
element_name: selectors.element_name,
|
|
1208
|
-
type: Types.SET_DATE_TIME,
|
|
1209
|
-
screenshotId,
|
|
1210
|
-
value: value,
|
|
1211
|
-
text: `setDateTime input with value: ${value}`,
|
|
1212
|
-
result: error
|
|
1213
|
-
? {
|
|
1214
|
-
status: "FAILED",
|
|
1215
|
-
startTime,
|
|
1216
|
-
endTime,
|
|
1217
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1218
|
-
}
|
|
1219
|
-
: {
|
|
1220
|
-
status: "PASSED",
|
|
1221
|
-
startTime,
|
|
1222
|
-
endTime,
|
|
1223
|
-
},
|
|
1224
|
-
info: info,
|
|
1225
|
-
});
|
|
1448
|
+
await _commandFinally(state, this);
|
|
1226
1449
|
}
|
|
1227
1450
|
}
|
|
1228
1451
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1229
1452
|
_value = unEscapeString(_value);
|
|
1230
|
-
this._validateSelectors(selectors);
|
|
1231
|
-
const startTime = Date.now();
|
|
1232
|
-
let error = null;
|
|
1233
|
-
let screenshotId = null;
|
|
1234
|
-
let screenshotPath = null;
|
|
1235
|
-
const info = {};
|
|
1236
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1237
|
-
info.operation = "clickType";
|
|
1238
|
-
info.selectors = selectors;
|
|
1239
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
|
+
}
|
|
1240
1470
|
if (newValue !== _value) {
|
|
1241
1471
|
//this.logger.info(_value + "=" + newValue);
|
|
1242
1472
|
_value = newValue;
|
|
1243
1473
|
}
|
|
1244
|
-
info.value = _value;
|
|
1245
1474
|
try {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1250
|
-
await this._highlightElements(element);
|
|
1251
|
-
if (options === null || options === undefined || !options.press) {
|
|
1475
|
+
await _preCommand(state, this);
|
|
1476
|
+
state.info.value = _value;
|
|
1477
|
+
if (!options.press) {
|
|
1252
1478
|
try {
|
|
1253
|
-
let currentValue = await element.inputValue();
|
|
1479
|
+
let currentValue = await state.element.inputValue();
|
|
1254
1480
|
if (currentValue) {
|
|
1255
|
-
await element.fill("");
|
|
1481
|
+
await state.element.fill("");
|
|
1256
1482
|
}
|
|
1257
1483
|
}
|
|
1258
1484
|
catch (e) {
|
|
1259
1485
|
this.logger.info("unable to clear input value");
|
|
1260
1486
|
}
|
|
1261
1487
|
}
|
|
1262
|
-
if (options
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
catch (e) {
|
|
1267
|
-
await element.dispatchEvent("click");
|
|
1268
|
-
}
|
|
1488
|
+
if (options.press) {
|
|
1489
|
+
options.timeout = 5000;
|
|
1490
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1269
1491
|
}
|
|
1270
1492
|
else {
|
|
1271
1493
|
try {
|
|
1272
|
-
await element.focus();
|
|
1494
|
+
await state.element.focus();
|
|
1273
1495
|
}
|
|
1274
1496
|
catch (e) {
|
|
1275
|
-
await element.dispatchEvent("focus");
|
|
1497
|
+
await state.element.dispatchEvent("focus");
|
|
1276
1498
|
}
|
|
1277
1499
|
}
|
|
1278
1500
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1279
|
-
const valueSegment =
|
|
1501
|
+
const valueSegment = state.value.split("&&");
|
|
1280
1502
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1281
1503
|
if (i > 0) {
|
|
1282
1504
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1296,13 +1518,21 @@ class StableBrowser {
|
|
|
1296
1518
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1297
1519
|
}
|
|
1298
1520
|
}
|
|
1521
|
+
//if (!this.fastMode) {
|
|
1522
|
+
await _screenshot(state, this);
|
|
1523
|
+
//}
|
|
1299
1524
|
if (enter === true) {
|
|
1300
1525
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1301
1526
|
await this.page.keyboard.press("Enter");
|
|
1302
1527
|
await this.waitForPageLoad();
|
|
1303
1528
|
}
|
|
1304
1529
|
else if (enter === false) {
|
|
1305
|
-
|
|
1530
|
+
try {
|
|
1531
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1532
|
+
}
|
|
1533
|
+
catch (e) {
|
|
1534
|
+
// ignore
|
|
1535
|
+
}
|
|
1306
1536
|
//await this.page.keyboard.press("Tab");
|
|
1307
1537
|
}
|
|
1308
1538
|
else {
|
|
@@ -1311,112 +1541,95 @@ class StableBrowser {
|
|
|
1311
1541
|
await this.waitForPageLoad();
|
|
1312
1542
|
}
|
|
1313
1543
|
}
|
|
1314
|
-
return info;
|
|
1544
|
+
return state.info;
|
|
1315
1545
|
}
|
|
1316
1546
|
catch (e) {
|
|
1317
|
-
|
|
1318
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1319
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1320
|
-
info.screenshotPath = screenshotPath;
|
|
1321
|
-
Object.assign(e, { info: info });
|
|
1322
|
-
error = e;
|
|
1323
|
-
throw e;
|
|
1547
|
+
await _commandError(state, e, this);
|
|
1324
1548
|
}
|
|
1325
1549
|
finally {
|
|
1326
|
-
|
|
1327
|
-
this._reportToWorld(world, {
|
|
1328
|
-
element_name: selectors.element_name,
|
|
1329
|
-
type: Types.FILL,
|
|
1330
|
-
screenshotId,
|
|
1331
|
-
value: _value,
|
|
1332
|
-
text: `clickType input with value: ${_value}`,
|
|
1333
|
-
result: error
|
|
1334
|
-
? {
|
|
1335
|
-
status: "FAILED",
|
|
1336
|
-
startTime,
|
|
1337
|
-
endTime,
|
|
1338
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1339
|
-
}
|
|
1340
|
-
: {
|
|
1341
|
-
status: "PASSED",
|
|
1342
|
-
startTime,
|
|
1343
|
-
endTime,
|
|
1344
|
-
},
|
|
1345
|
-
info: info,
|
|
1346
|
-
});
|
|
1550
|
+
await _commandFinally(state, this);
|
|
1347
1551
|
}
|
|
1348
1552
|
}
|
|
1349
1553
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
+
};
|
|
1361
1565
|
try {
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
await
|
|
1365
|
-
await element.fill(value);
|
|
1366
|
-
await element.dispatchEvent("change");
|
|
1566
|
+
await _preCommand(state, this);
|
|
1567
|
+
await state.element.fill(value);
|
|
1568
|
+
await state.element.dispatchEvent("change");
|
|
1367
1569
|
if (enter) {
|
|
1368
1570
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1369
1571
|
await this.page.keyboard.press("Enter");
|
|
1370
1572
|
}
|
|
1371
1573
|
await this.waitForPageLoad();
|
|
1372
|
-
return info;
|
|
1574
|
+
return state.info;
|
|
1373
1575
|
}
|
|
1374
1576
|
catch (e) {
|
|
1375
|
-
|
|
1376
|
-
this.logger.error("fill failed " + info.log);
|
|
1377
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1378
|
-
info.screenshotPath = screenshotPath;
|
|
1379
|
-
Object.assign(e, { info: info });
|
|
1380
|
-
error = e;
|
|
1381
|
-
throw e;
|
|
1577
|
+
await _commandError(state, e, this);
|
|
1382
1578
|
}
|
|
1383
1579
|
finally {
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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);
|
|
1405
1616
|
}
|
|
1406
1617
|
}
|
|
1407
1618
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1408
1619
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1409
1620
|
}
|
|
1410
1621
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1411
|
-
this.
|
|
1622
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1623
|
+
_validateSelectors(selectors);
|
|
1412
1624
|
let screenshotId = null;
|
|
1413
1625
|
let screenshotPath = null;
|
|
1414
1626
|
if (!info.log) {
|
|
1415
1627
|
info.log = "";
|
|
1628
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1416
1629
|
}
|
|
1417
1630
|
info.operation = "getText";
|
|
1418
1631
|
info.selectors = selectors;
|
|
1419
|
-
let element = await this._locate(selectors, info, _params);
|
|
1632
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1420
1633
|
if (climb > 0) {
|
|
1421
1634
|
const climbArray = [];
|
|
1422
1635
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1435,6 +1648,18 @@ class StableBrowser {
|
|
|
1435
1648
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1436
1649
|
try {
|
|
1437
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
|
+
// }
|
|
1438
1663
|
const elementText = await element.innerText();
|
|
1439
1664
|
return {
|
|
1440
1665
|
text: elementText,
|
|
@@ -1446,189 +1671,219 @@ class StableBrowser {
|
|
|
1446
1671
|
}
|
|
1447
1672
|
catch (e) {
|
|
1448
1673
|
//await this.closeUnexpectedPopups();
|
|
1449
|
-
this.logger.info("no innerText will use textContent");
|
|
1674
|
+
this.logger.info("no innerText, will use textContent");
|
|
1450
1675
|
const elementText = await element.textContent();
|
|
1451
1676
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1452
1677
|
}
|
|
1453
1678
|
}
|
|
1454
1679
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1455
|
-
var _a;
|
|
1456
|
-
this._validateSelectors(selectors);
|
|
1457
1680
|
if (!pattern) {
|
|
1458
1681
|
throw new Error("pattern is null");
|
|
1459
1682
|
}
|
|
1460
1683
|
if (!text) {
|
|
1461
1684
|
throw new Error("text is null");
|
|
1462
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
|
+
};
|
|
1463
1703
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1464
1704
|
if (newValue !== text) {
|
|
1465
1705
|
this.logger.info(text + "=" + newValue);
|
|
1466
1706
|
text = newValue;
|
|
1467
1707
|
}
|
|
1468
|
-
const startTime = Date.now();
|
|
1469
|
-
let error = null;
|
|
1470
|
-
let screenshotId = null;
|
|
1471
|
-
let screenshotPath = null;
|
|
1472
|
-
const info = {};
|
|
1473
|
-
info.log =
|
|
1474
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1475
|
-
info.operation = "containsPattern";
|
|
1476
|
-
info.selectors = selectors;
|
|
1477
|
-
info.value = text;
|
|
1478
|
-
info.pattern = pattern;
|
|
1479
1708
|
let foundObj = null;
|
|
1480
1709
|
try {
|
|
1481
|
-
|
|
1710
|
+
await _preCommand(state, this);
|
|
1711
|
+
state.info.pattern = pattern;
|
|
1712
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1482
1713
|
if (foundObj && foundObj.element) {
|
|
1483
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1714
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1484
1715
|
}
|
|
1485
|
-
|
|
1716
|
+
await _screenshot(state, this);
|
|
1486
1717
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1487
1718
|
pattern = pattern.replace("{text}", escapedText);
|
|
1488
1719
|
let regex = new RegExp(pattern, "im");
|
|
1489
|
-
if (!regex.test(foundObj
|
|
1490
|
-
info.foundText = foundObj
|
|
1720
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1721
|
+
state.info.foundText = foundObj?.text;
|
|
1491
1722
|
throw new Error("element doesn't contain text " + text);
|
|
1492
1723
|
}
|
|
1493
|
-
return info;
|
|
1724
|
+
return state.info;
|
|
1494
1725
|
}
|
|
1495
1726
|
catch (e) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1499
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1500
|
-
info.screenshotPath = screenshotPath;
|
|
1501
|
-
Object.assign(e, { info: info });
|
|
1502
|
-
error = e;
|
|
1503
|
-
throw e;
|
|
1727
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1728
|
+
await _commandError(state, e, this);
|
|
1504
1729
|
}
|
|
1505
1730
|
finally {
|
|
1506
|
-
|
|
1507
|
-
this._reportToWorld(world, {
|
|
1508
|
-
element_name: selectors.element_name,
|
|
1509
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1510
|
-
value: pattern,
|
|
1511
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1512
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1513
|
-
result: error
|
|
1514
|
-
? {
|
|
1515
|
-
status: "FAILED",
|
|
1516
|
-
startTime,
|
|
1517
|
-
endTime,
|
|
1518
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1519
|
-
}
|
|
1520
|
-
: {
|
|
1521
|
-
status: "PASSED",
|
|
1522
|
-
startTime,
|
|
1523
|
-
endTime,
|
|
1524
|
-
},
|
|
1525
|
-
info: info,
|
|
1526
|
-
});
|
|
1731
|
+
await _commandFinally(state, this);
|
|
1527
1732
|
}
|
|
1528
1733
|
}
|
|
1529
1734
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
+
};
|
|
1533
1752
|
if (!text) {
|
|
1534
1753
|
throw new Error("text is null");
|
|
1535
1754
|
}
|
|
1536
|
-
|
|
1537
|
-
let error = null;
|
|
1538
|
-
let screenshotId = null;
|
|
1539
|
-
let screenshotPath = null;
|
|
1540
|
-
const info = {};
|
|
1541
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1542
|
-
info.operation = "containsText";
|
|
1543
|
-
info.selectors = selectors;
|
|
1755
|
+
text = unEscapeString(text);
|
|
1544
1756
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1545
1757
|
if (newValue !== text) {
|
|
1546
1758
|
this.logger.info(text + "=" + newValue);
|
|
1547
1759
|
text = newValue;
|
|
1548
1760
|
}
|
|
1549
|
-
info.value = text;
|
|
1550
1761
|
let foundObj = null;
|
|
1551
1762
|
try {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1559
|
-
if (dateAlternatives.date) {
|
|
1560
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1561
|
-
if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
|
|
1562
|
-
((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
|
|
1563
|
-
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);
|
|
1564
1769
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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;
|
|
1573
1791
|
}
|
|
1574
1792
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
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
|
|
1581
1798
|
}
|
|
1582
|
-
|
|
1799
|
+
state.info.foundText = foundObj?.text;
|
|
1800
|
+
state.info.value = foundObj?.value;
|
|
1801
|
+
throw new Error("element doesn't contain text " + text);
|
|
1583
1802
|
}
|
|
1584
1803
|
catch (e) {
|
|
1585
|
-
|
|
1586
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1587
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1588
|
-
info.screenshotPath = screenshotPath;
|
|
1589
|
-
Object.assign(e, { info: info });
|
|
1590
|
-
error = e;
|
|
1804
|
+
await _commandError(state, e, this);
|
|
1591
1805
|
throw e;
|
|
1592
1806
|
}
|
|
1593
1807
|
finally {
|
|
1594
|
-
|
|
1595
|
-
this._reportToWorld(world, {
|
|
1596
|
-
element_name: selectors.element_name,
|
|
1597
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1598
|
-
text: `Verify element contains text: ${text}`,
|
|
1599
|
-
value: text,
|
|
1600
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1601
|
-
result: error
|
|
1602
|
-
? {
|
|
1603
|
-
status: "FAILED",
|
|
1604
|
-
startTime,
|
|
1605
|
-
endTime,
|
|
1606
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1607
|
-
}
|
|
1608
|
-
: {
|
|
1609
|
-
status: "PASSED",
|
|
1610
|
-
startTime,
|
|
1611
|
-
endTime,
|
|
1612
|
-
},
|
|
1613
|
-
info: info,
|
|
1614
|
-
});
|
|
1808
|
+
await _commandFinally(state, this);
|
|
1615
1809
|
}
|
|
1616
1810
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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");
|
|
1621
1834
|
}
|
|
1622
|
-
else if (this.
|
|
1623
|
-
|
|
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");
|
|
1624
1837
|
}
|
|
1625
|
-
else if (
|
|
1626
|
-
|
|
1838
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1839
|
+
text = referanceSnapshot.substring(5);
|
|
1627
1840
|
}
|
|
1628
1841
|
else {
|
|
1629
|
-
|
|
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);
|
|
1630
1886
|
}
|
|
1631
|
-
return dataFile;
|
|
1632
1887
|
}
|
|
1633
1888
|
async waitForUserInput(message, world = null) {
|
|
1634
1889
|
if (!message) {
|
|
@@ -1658,13 +1913,22 @@ class StableBrowser {
|
|
|
1658
1913
|
return;
|
|
1659
1914
|
}
|
|
1660
1915
|
// if data file exists, load it
|
|
1661
|
-
const dataFile =
|
|
1916
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1662
1917
|
let data = this.getTestData(world);
|
|
1663
1918
|
// merge the testData with the existing data
|
|
1664
1919
|
Object.assign(data, testData);
|
|
1665
1920
|
// save the data to the file
|
|
1666
1921
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1667
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
|
+
}
|
|
1668
1932
|
_getDataFilePath(fileName) {
|
|
1669
1933
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1670
1934
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1761,12 +2025,7 @@ class StableBrowser {
|
|
|
1761
2025
|
}
|
|
1762
2026
|
}
|
|
1763
2027
|
getTestData(world = null) {
|
|
1764
|
-
|
|
1765
|
-
let data = {};
|
|
1766
|
-
if (fs.existsSync(dataFile)) {
|
|
1767
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1768
|
-
}
|
|
1769
|
-
return data;
|
|
2028
|
+
return _getTestData(world, this.context, this);
|
|
1770
2029
|
}
|
|
1771
2030
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1772
2031
|
// collect url/path/title
|
|
@@ -1793,11 +2052,9 @@ class StableBrowser {
|
|
|
1793
2052
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1794
2053
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1795
2054
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
}
|
|
1800
|
-
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");
|
|
1801
2058
|
try {
|
|
1802
2059
|
await this.takeScreenshot(screenshotPath);
|
|
1803
2060
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1807,15 +2064,15 @@ class StableBrowser {
|
|
|
1807
2064
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1808
2065
|
// }
|
|
1809
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
|
+
}
|
|
1810
2072
|
}
|
|
1811
2073
|
catch (e) {
|
|
1812
2074
|
this.logger.info("unable to take screenshot, ignored");
|
|
1813
2075
|
}
|
|
1814
|
-
result.screenshotId = nextIndex;
|
|
1815
|
-
result.screenshotPath = screenshotPath;
|
|
1816
|
-
if (info && info.box) {
|
|
1817
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1818
|
-
}
|
|
1819
2076
|
}
|
|
1820
2077
|
else if (options && options.screenshot) {
|
|
1821
2078
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1840,7 +2097,6 @@ class StableBrowser {
|
|
|
1840
2097
|
}
|
|
1841
2098
|
async takeScreenshot(screenshotPath) {
|
|
1842
2099
|
const playContext = this.context.playContext;
|
|
1843
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1844
2100
|
// Using CDP to capture the screenshot
|
|
1845
2101
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1846
2102
|
document.body.scrollWidth,
|
|
@@ -1850,165 +2106,560 @@ class StableBrowser {
|
|
|
1850
2106
|
document.body.clientWidth,
|
|
1851
2107
|
document.documentElement.clientWidth,
|
|
1852
2108
|
])));
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
screenshotBuffer =
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
|
|
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();
|
|
2139
|
+
}
|
|
2140
|
+
// if (focusedElement) {
|
|
2141
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2142
|
+
// await this._unhighlightElements(focusedElement);
|
|
2143
|
+
// }
|
|
2144
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
2145
|
+
// Get the image dimensions
|
|
2146
|
+
const { width, height } = image.bitmap;
|
|
2147
|
+
const resizeRatio = viewportWidth / width;
|
|
2148
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
2149
|
+
if (width > viewportWidth) {
|
|
2150
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
2151
|
+
await image.write(screenshotPath);
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
2155
|
+
}
|
|
2156
|
+
return screenshotBuffer;
|
|
1888
2157
|
}
|
|
1889
2158
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
+
};
|
|
1895
2169
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1896
|
-
const info = {};
|
|
1897
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1898
|
-
info.operation = "verify";
|
|
1899
|
-
info.selectors = selectors;
|
|
1900
2170
|
try {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
}
|
|
1905
|
-
await this._highlightElements(element);
|
|
1906
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1907
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1908
|
-
return info;
|
|
2171
|
+
await _preCommand(state, this);
|
|
2172
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
2173
|
+
return state.info;
|
|
1909
2174
|
}
|
|
1910
2175
|
catch (e) {
|
|
1911
|
-
|
|
1912
|
-
this.logger.error("verify failed " + info.log);
|
|
1913
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1914
|
-
info.screenshotPath = screenshotPath;
|
|
1915
|
-
Object.assign(e, { info: info });
|
|
1916
|
-
error = e;
|
|
1917
|
-
throw e;
|
|
2176
|
+
await _commandError(state, e, this);
|
|
1918
2177
|
}
|
|
1919
2178
|
finally {
|
|
1920
|
-
|
|
1921
|
-
this._reportToWorld(world, {
|
|
1922
|
-
element_name: selectors.element_name,
|
|
1923
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1924
|
-
text: "Verify element exists in page",
|
|
1925
|
-
screenshotId,
|
|
1926
|
-
result: error
|
|
1927
|
-
? {
|
|
1928
|
-
status: "FAILED",
|
|
1929
|
-
startTime,
|
|
1930
|
-
endTime,
|
|
1931
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1932
|
-
}
|
|
1933
|
-
: {
|
|
1934
|
-
status: "PASSED",
|
|
1935
|
-
startTime,
|
|
1936
|
-
endTime,
|
|
1937
|
-
},
|
|
1938
|
-
info: info,
|
|
1939
|
-
});
|
|
2179
|
+
await _commandFinally(state, this);
|
|
1940
2180
|
}
|
|
1941
2181
|
}
|
|
1942
2182
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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
|
+
};
|
|
1948
2197
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1949
|
-
const info = {};
|
|
1950
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1951
|
-
info.operation = "extract";
|
|
1952
|
-
info.selectors = selectors;
|
|
1953
2198
|
try {
|
|
1954
|
-
|
|
1955
|
-
await this._highlightElements(element);
|
|
1956
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2199
|
+
await _preCommand(state, this);
|
|
1957
2200
|
switch (attribute) {
|
|
1958
2201
|
case "inner_text":
|
|
1959
|
-
|
|
2202
|
+
state.value = await state.element.innerText();
|
|
1960
2203
|
break;
|
|
1961
2204
|
case "href":
|
|
1962
|
-
|
|
2205
|
+
state.value = await state.element.getAttribute("href");
|
|
1963
2206
|
break;
|
|
1964
2207
|
case "value":
|
|
1965
|
-
|
|
2208
|
+
state.value = await state.element.inputValue();
|
|
2209
|
+
break;
|
|
2210
|
+
case "text":
|
|
2211
|
+
state.value = await state.element.textContent();
|
|
1966
2212
|
break;
|
|
1967
2213
|
default:
|
|
1968
|
-
|
|
2214
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1969
2215
|
break;
|
|
1970
2216
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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
|
+
}
|
|
1974
2234
|
}
|
|
1975
|
-
|
|
1976
|
-
this.
|
|
1977
|
-
|
|
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;
|
|
1978
2240
|
}
|
|
1979
2241
|
catch (e) {
|
|
1980
|
-
|
|
1981
|
-
this.logger.error("extract failed " + info.log);
|
|
1982
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1983
|
-
info.screenshotPath = screenshotPath;
|
|
1984
|
-
Object.assign(e, { info: info });
|
|
1985
|
-
error = e;
|
|
1986
|
-
throw e;
|
|
2242
|
+
await _commandError(state, e, this);
|
|
1987
2243
|
}
|
|
1988
2244
|
finally {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
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}`)) || "";
|
|
2003
2283
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
+
}
|
|
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);
|
|
2011
2658
|
}
|
|
2659
|
+
catch (finallyError) {
|
|
2660
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2661
|
+
}
|
|
2662
|
+
return state.info;
|
|
2012
2663
|
}
|
|
2013
2664
|
async extractEmailData(emailAddress, options, world) {
|
|
2014
2665
|
if (!emailAddress) {
|
|
@@ -2028,7 +2679,7 @@ class StableBrowser {
|
|
|
2028
2679
|
if (options && options.timeout) {
|
|
2029
2680
|
timeout = options.timeout;
|
|
2030
2681
|
}
|
|
2031
|
-
const serviceUrl =
|
|
2682
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
2032
2683
|
const request = {
|
|
2033
2684
|
method: "POST",
|
|
2034
2685
|
url: serviceUrl,
|
|
@@ -2084,7 +2735,8 @@ class StableBrowser {
|
|
|
2084
2735
|
catch (e) {
|
|
2085
2736
|
errorCount++;
|
|
2086
2737
|
if (errorCount > 3) {
|
|
2087
|
-
throw e;
|
|
2738
|
+
// throw e;
|
|
2739
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2088
2740
|
}
|
|
2089
2741
|
// ignore
|
|
2090
2742
|
}
|
|
@@ -2098,27 +2750,32 @@ class StableBrowser {
|
|
|
2098
2750
|
async _highlightElements(scope, css) {
|
|
2099
2751
|
try {
|
|
2100
2752
|
if (!scope) {
|
|
2753
|
+
// console.log(`Scope is not defined`);
|
|
2101
2754
|
return;
|
|
2102
2755
|
}
|
|
2103
2756
|
if (!css) {
|
|
2104
2757
|
scope
|
|
2105
2758
|
.evaluate((node) => {
|
|
2106
2759
|
if (node && node.style) {
|
|
2107
|
-
let
|
|
2108
|
-
|
|
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}`);
|
|
2109
2765
|
if (window) {
|
|
2110
2766
|
window.addEventListener("beforeunload", function (e) {
|
|
2111
|
-
node.style.
|
|
2767
|
+
node.style.outline = originalOutline;
|
|
2112
2768
|
});
|
|
2113
2769
|
}
|
|
2114
2770
|
setTimeout(function () {
|
|
2115
|
-
node.style.
|
|
2771
|
+
node.style.outline = originalOutline;
|
|
2116
2772
|
}, 2000);
|
|
2117
2773
|
}
|
|
2118
2774
|
})
|
|
2119
2775
|
.then(() => { })
|
|
2120
2776
|
.catch((e) => {
|
|
2121
2777
|
// ignore
|
|
2778
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2122
2779
|
});
|
|
2123
2780
|
}
|
|
2124
2781
|
else {
|
|
@@ -2134,17 +2791,18 @@ class StableBrowser {
|
|
|
2134
2791
|
if (!element.style) {
|
|
2135
2792
|
return;
|
|
2136
2793
|
}
|
|
2137
|
-
|
|
2794
|
+
let originalOutline = element.style.outline;
|
|
2795
|
+
element.__previousOutline = originalOutline;
|
|
2138
2796
|
// Set the new border to be red and 2px solid
|
|
2139
|
-
element.style.
|
|
2797
|
+
element.style.outline = "2px solid red";
|
|
2140
2798
|
if (window) {
|
|
2141
2799
|
window.addEventListener("beforeunload", function (e) {
|
|
2142
|
-
element.style.
|
|
2800
|
+
element.style.outline = originalOutline;
|
|
2143
2801
|
});
|
|
2144
2802
|
}
|
|
2145
2803
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2146
2804
|
setTimeout(function () {
|
|
2147
|
-
element.style.
|
|
2805
|
+
element.style.outline = originalOutline;
|
|
2148
2806
|
}, 2000);
|
|
2149
2807
|
}
|
|
2150
2808
|
return;
|
|
@@ -2152,6 +2810,7 @@ class StableBrowser {
|
|
|
2152
2810
|
.then(() => { })
|
|
2153
2811
|
.catch((e) => {
|
|
2154
2812
|
// ignore
|
|
2813
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2155
2814
|
});
|
|
2156
2815
|
}
|
|
2157
2816
|
}
|
|
@@ -2159,8 +2818,49 @@ class StableBrowser {
|
|
|
2159
2818
|
console.debug(error);
|
|
2160
2819
|
}
|
|
2161
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
|
+
*/
|
|
2162
2863
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2163
|
-
const startTime = Date.now();
|
|
2164
2864
|
let error = null;
|
|
2165
2865
|
let screenshotId = null;
|
|
2166
2866
|
let screenshotPath = null;
|
|
@@ -2174,159 +2874,520 @@ class StableBrowser {
|
|
|
2174
2874
|
pathPart = newValue;
|
|
2175
2875
|
}
|
|
2176
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
|
+
};
|
|
2177
2891
|
try {
|
|
2892
|
+
await _preCommand(state, this);
|
|
2893
|
+
state.info.text = queryText;
|
|
2178
2894
|
for (let i = 0; i < 30; i++) {
|
|
2179
2895
|
const url = await this.page.url();
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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`);
|
|
2183
3157
|
}
|
|
2184
3158
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2185
3159
|
continue;
|
|
2186
3160
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
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
|
+
}
|
|
2189
3178
|
}
|
|
2190
3179
|
}
|
|
2191
3180
|
catch (e) {
|
|
2192
|
-
|
|
2193
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2194
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2195
|
-
info.screenshotPath = screenshotPath;
|
|
2196
|
-
Object.assign(e, { info: info });
|
|
2197
|
-
error = e;
|
|
2198
|
-
throw e;
|
|
3181
|
+
await _commandError(state, e, this);
|
|
2199
3182
|
}
|
|
2200
3183
|
finally {
|
|
2201
|
-
|
|
2202
|
-
this._reportToWorld(world, {
|
|
2203
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2204
|
-
text: "Verify page path",
|
|
2205
|
-
screenshotId,
|
|
2206
|
-
result: error
|
|
2207
|
-
? {
|
|
2208
|
-
status: "FAILED",
|
|
2209
|
-
startTime,
|
|
2210
|
-
endTime,
|
|
2211
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2212
|
-
}
|
|
2213
|
-
: {
|
|
2214
|
-
status: "PASSED",
|
|
2215
|
-
startTime,
|
|
2216
|
-
endTime,
|
|
2217
|
-
},
|
|
2218
|
-
info: info,
|
|
2219
|
-
});
|
|
3184
|
+
await _commandFinally(state, this);
|
|
2220
3185
|
}
|
|
2221
3186
|
}
|
|
2222
|
-
async
|
|
3187
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2223
3188
|
text = unEscapeString(text);
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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);
|
|
2229
3206
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2230
|
-
const info = {};
|
|
2231
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2232
|
-
info.operation = "verifyTextExistInPage";
|
|
2233
3207
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2234
3208
|
if (newValue !== text) {
|
|
2235
3209
|
this.logger.info(text + "=" + newValue);
|
|
2236
3210
|
text = newValue;
|
|
2237
3211
|
}
|
|
2238
|
-
info.text = text;
|
|
2239
3212
|
let dateAlternatives = findDateAlternatives(text);
|
|
2240
3213
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2241
3214
|
try {
|
|
3215
|
+
await _preCommand(state, this);
|
|
3216
|
+
state.info.text = text;
|
|
3217
|
+
let resultWithElementsFound = {
|
|
3218
|
+
length: null, // initial cannot be 0
|
|
3219
|
+
};
|
|
2242
3220
|
while (true) {
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
|
|
2249
|
-
result.frame = frames[i];
|
|
2250
|
-
results.push(result);
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
else if (numberAlternatives.number) {
|
|
2254
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2255
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
|
|
2256
|
-
result.frame = frames[i];
|
|
2257
|
-
results.push(result);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
else {
|
|
2261
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, {});
|
|
2262
|
-
result.frame = frames[i];
|
|
2263
|
-
results.push(result);
|
|
2264
|
-
}
|
|
3221
|
+
try {
|
|
3222
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3223
|
+
}
|
|
3224
|
+
catch (error) {
|
|
3225
|
+
// ignore
|
|
2265
3226
|
}
|
|
2266
|
-
info.results = results;
|
|
2267
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2268
3227
|
if (resultWithElementsFound.length === 0) {
|
|
2269
|
-
|
|
2270
|
-
|
|
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`);
|
|
2271
3293
|
}
|
|
2272
3294
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2273
3295
|
continue;
|
|
2274
3296
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
+
}
|
|
2283
3353
|
}
|
|
2284
3354
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
3355
|
+
catch (error) {
|
|
3356
|
+
console.error(error);
|
|
3357
|
+
}
|
|
2287
3358
|
}
|
|
2288
3359
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2289
3360
|
}
|
|
2290
3361
|
catch (e) {
|
|
2291
|
-
|
|
2292
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2293
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2294
|
-
info.screenshotPath = screenshotPath;
|
|
2295
|
-
Object.assign(e, { info: info });
|
|
2296
|
-
error = e;
|
|
2297
|
-
throw e;
|
|
3362
|
+
await _commandError(state, e, this);
|
|
2298
3363
|
}
|
|
2299
3364
|
finally {
|
|
2300
|
-
|
|
2301
|
-
this._reportToWorld(world, {
|
|
2302
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2303
|
-
text: "Verify text exists in page",
|
|
2304
|
-
screenshotId,
|
|
2305
|
-
result: error
|
|
2306
|
-
? {
|
|
2307
|
-
status: "FAILED",
|
|
2308
|
-
startTime,
|
|
2309
|
-
endTime,
|
|
2310
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2311
|
-
}
|
|
2312
|
-
: {
|
|
2313
|
-
status: "PASSED",
|
|
2314
|
-
startTime,
|
|
2315
|
-
endTime,
|
|
2316
|
-
},
|
|
2317
|
-
info: info,
|
|
2318
|
-
});
|
|
3365
|
+
await _commandFinally(state, this);
|
|
2319
3366
|
}
|
|
2320
3367
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
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
|
+
}
|
|
2328
3387
|
}
|
|
2329
|
-
|
|
3388
|
+
// state.info.results = results;
|
|
3389
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3390
|
+
return resultWithElementsFound;
|
|
2330
3391
|
}
|
|
2331
3392
|
async visualVerification(text, options = {}, world = null) {
|
|
2332
3393
|
const startTime = Date.now();
|
|
@@ -2342,14 +3403,17 @@ class StableBrowser {
|
|
|
2342
3403
|
throw new Error("TOKEN is not set");
|
|
2343
3404
|
}
|
|
2344
3405
|
try {
|
|
2345
|
-
let serviceUrl =
|
|
3406
|
+
let serviceUrl = _getServerUrl();
|
|
2346
3407
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2347
3408
|
info.screenshotPath = screenshotPath;
|
|
2348
3409
|
const screenshot = await this.takeScreenshot();
|
|
2349
|
-
|
|
2350
|
-
method: "
|
|
3410
|
+
let request = {
|
|
3411
|
+
method: "post",
|
|
3412
|
+
maxBodyLength: Infinity,
|
|
2351
3413
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2352
3414
|
headers: {
|
|
3415
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3416
|
+
"x-source": "aaa",
|
|
2353
3417
|
"Content-Type": "application/json",
|
|
2354
3418
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2355
3419
|
},
|
|
@@ -2358,7 +3422,7 @@ class StableBrowser {
|
|
|
2358
3422
|
screenshot: screenshot,
|
|
2359
3423
|
}),
|
|
2360
3424
|
};
|
|
2361
|
-
|
|
3425
|
+
const result = await axios.request(request);
|
|
2362
3426
|
if (result.data.status !== true) {
|
|
2363
3427
|
throw new Error("Visual validation failed");
|
|
2364
3428
|
}
|
|
@@ -2378,20 +3442,22 @@ class StableBrowser {
|
|
|
2378
3442
|
info.screenshotPath = screenshotPath;
|
|
2379
3443
|
Object.assign(e, { info: info });
|
|
2380
3444
|
error = e;
|
|
2381
|
-
throw e;
|
|
3445
|
+
// throw e;
|
|
3446
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2382
3447
|
}
|
|
2383
3448
|
finally {
|
|
2384
3449
|
const endTime = Date.now();
|
|
2385
|
-
|
|
3450
|
+
_reportToWorld(world, {
|
|
2386
3451
|
type: Types.VERIFY_VISUAL,
|
|
2387
3452
|
text: "Visual verification",
|
|
3453
|
+
_text: "Visual verification of " + text,
|
|
2388
3454
|
screenshotId,
|
|
2389
3455
|
result: error
|
|
2390
3456
|
? {
|
|
2391
3457
|
status: "FAILED",
|
|
2392
3458
|
startTime,
|
|
2393
3459
|
endTime,
|
|
2394
|
-
message: error
|
|
3460
|
+
message: error?.message,
|
|
2395
3461
|
}
|
|
2396
3462
|
: {
|
|
2397
3463
|
status: "PASSED",
|
|
@@ -2423,13 +3489,14 @@ class StableBrowser {
|
|
|
2423
3489
|
this.logger.info("Table data verified");
|
|
2424
3490
|
}
|
|
2425
3491
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2426
|
-
|
|
3492
|
+
_validateSelectors(selectors);
|
|
2427
3493
|
const startTime = Date.now();
|
|
2428
3494
|
let error = null;
|
|
2429
3495
|
let screenshotId = null;
|
|
2430
3496
|
let screenshotPath = null;
|
|
2431
3497
|
const info = {};
|
|
2432
3498
|
info.log = "";
|
|
3499
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2433
3500
|
info.operation = "getTableData";
|
|
2434
3501
|
info.selectors = selectors;
|
|
2435
3502
|
try {
|
|
@@ -2445,11 +3512,12 @@ class StableBrowser {
|
|
|
2445
3512
|
info.screenshotPath = screenshotPath;
|
|
2446
3513
|
Object.assign(e, { info: info });
|
|
2447
3514
|
error = e;
|
|
2448
|
-
throw e;
|
|
3515
|
+
// throw e;
|
|
3516
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2449
3517
|
}
|
|
2450
3518
|
finally {
|
|
2451
3519
|
const endTime = Date.now();
|
|
2452
|
-
|
|
3520
|
+
_reportToWorld(world, {
|
|
2453
3521
|
element_name: selectors.element_name,
|
|
2454
3522
|
type: Types.GET_TABLE_DATA,
|
|
2455
3523
|
text: "Get table data",
|
|
@@ -2459,7 +3527,7 @@ class StableBrowser {
|
|
|
2459
3527
|
status: "FAILED",
|
|
2460
3528
|
startTime,
|
|
2461
3529
|
endTime,
|
|
2462
|
-
message: error
|
|
3530
|
+
message: error?.message,
|
|
2463
3531
|
}
|
|
2464
3532
|
: {
|
|
2465
3533
|
status: "PASSED",
|
|
@@ -2471,7 +3539,7 @@ class StableBrowser {
|
|
|
2471
3539
|
}
|
|
2472
3540
|
}
|
|
2473
3541
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2474
|
-
|
|
3542
|
+
_validateSelectors(selectors);
|
|
2475
3543
|
if (!query) {
|
|
2476
3544
|
throw new Error("query is null");
|
|
2477
3545
|
}
|
|
@@ -2504,7 +3572,7 @@ class StableBrowser {
|
|
|
2504
3572
|
info.operation = "analyzeTable";
|
|
2505
3573
|
info.selectors = selectors;
|
|
2506
3574
|
info.query = query;
|
|
2507
|
-
query =
|
|
3575
|
+
query = _fixUsingParams(query, _params);
|
|
2508
3576
|
info.query_fixed = query;
|
|
2509
3577
|
info.operator = operator;
|
|
2510
3578
|
info.value = value;
|
|
@@ -2610,11 +3678,12 @@ class StableBrowser {
|
|
|
2610
3678
|
info.screenshotPath = screenshotPath;
|
|
2611
3679
|
Object.assign(e, { info: info });
|
|
2612
3680
|
error = e;
|
|
2613
|
-
throw e;
|
|
3681
|
+
// throw e;
|
|
3682
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2614
3683
|
}
|
|
2615
3684
|
finally {
|
|
2616
3685
|
const endTime = Date.now();
|
|
2617
|
-
|
|
3686
|
+
_reportToWorld(world, {
|
|
2618
3687
|
element_name: selectors.element_name,
|
|
2619
3688
|
type: Types.ANALYZE_TABLE,
|
|
2620
3689
|
text: "Analyze table",
|
|
@@ -2624,7 +3693,7 @@ class StableBrowser {
|
|
|
2624
3693
|
status: "FAILED",
|
|
2625
3694
|
startTime,
|
|
2626
3695
|
endTime,
|
|
2627
|
-
message: error
|
|
3696
|
+
message: error?.message,
|
|
2628
3697
|
}
|
|
2629
3698
|
: {
|
|
2630
3699
|
status: "PASSED",
|
|
@@ -2635,28 +3704,51 @@ class StableBrowser {
|
|
|
2635
3704
|
});
|
|
2636
3705
|
}
|
|
2637
3706
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
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");
|
|
2654
3733
|
}
|
|
3734
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3735
|
+
return state.info;
|
|
3736
|
+
}
|
|
3737
|
+
catch (e) {
|
|
3738
|
+
await _commandError(state, e, this);
|
|
2655
3739
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
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;
|
|
2658
3751
|
}
|
|
2659
|
-
return value;
|
|
2660
3752
|
}
|
|
2661
3753
|
_getLoadTimeout(options) {
|
|
2662
3754
|
let timeout = 15000;
|
|
@@ -2668,6 +3760,37 @@ class StableBrowser {
|
|
|
2668
3760
|
}
|
|
2669
3761
|
return timeout;
|
|
2670
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
|
+
}
|
|
2671
3794
|
async waitForPageLoad(options = {}, world = null) {
|
|
2672
3795
|
let timeout = this._getLoadTimeout(options);
|
|
2673
3796
|
const promiseArray = [];
|
|
@@ -2701,13 +3824,12 @@ class StableBrowser {
|
|
|
2701
3824
|
else if (e.label === "domcontentloaded") {
|
|
2702
3825
|
console.log("waited for the domcontent loaded timeout");
|
|
2703
3826
|
}
|
|
2704
|
-
console.log(".");
|
|
2705
3827
|
}
|
|
2706
3828
|
finally {
|
|
2707
3829
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2708
3830
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2709
3831
|
const endTime = Date.now();
|
|
2710
|
-
|
|
3832
|
+
_reportToWorld(world, {
|
|
2711
3833
|
type: Types.GET_PAGE_STATUS,
|
|
2712
3834
|
text: "Wait for page load",
|
|
2713
3835
|
screenshotId,
|
|
@@ -2716,7 +3838,7 @@ class StableBrowser {
|
|
|
2716
3838
|
status: "FAILED",
|
|
2717
3839
|
startTime,
|
|
2718
3840
|
endTime,
|
|
2719
|
-
message: error
|
|
3841
|
+
message: error?.message,
|
|
2720
3842
|
}
|
|
2721
3843
|
: {
|
|
2722
3844
|
status: "PASSED",
|
|
@@ -2727,41 +3849,122 @@ class StableBrowser {
|
|
|
2727
3849
|
}
|
|
2728
3850
|
}
|
|
2729
3851
|
async closePage(options = {}, world = null) {
|
|
2730
|
-
const
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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
|
+
};
|
|
2735
3865
|
try {
|
|
3866
|
+
await _preCommand(state, this);
|
|
2736
3867
|
await this.page.close();
|
|
2737
3868
|
}
|
|
2738
3869
|
catch (e) {
|
|
2739
|
-
|
|
3870
|
+
await _commandError(state, e, this);
|
|
2740
3871
|
}
|
|
2741
3872
|
finally {
|
|
2742
|
-
await
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
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;
|
|
2755
3908
|
}
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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);
|
|
2763
3961
|
}
|
|
2764
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
|
+
}
|
|
2765
3968
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2766
3969
|
const startTime = Date.now();
|
|
2767
3970
|
let error = null;
|
|
@@ -2778,22 +3981,23 @@ class StableBrowser {
|
|
|
2778
3981
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2779
3982
|
}
|
|
2780
3983
|
catch (e) {
|
|
2781
|
-
|
|
3984
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2782
3985
|
}
|
|
2783
3986
|
finally {
|
|
2784
3987
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2785
3988
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2786
3989
|
const endTime = Date.now();
|
|
2787
|
-
|
|
3990
|
+
_reportToWorld(world, {
|
|
2788
3991
|
type: Types.SET_VIEWPORT,
|
|
2789
3992
|
text: "set viewport size to " + width + "x" + hight,
|
|
3993
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2790
3994
|
screenshotId,
|
|
2791
3995
|
result: error
|
|
2792
3996
|
? {
|
|
2793
3997
|
status: "FAILED",
|
|
2794
3998
|
startTime,
|
|
2795
3999
|
endTime,
|
|
2796
|
-
message: error
|
|
4000
|
+
message: error?.message,
|
|
2797
4001
|
}
|
|
2798
4002
|
: {
|
|
2799
4003
|
status: "PASSED",
|
|
@@ -2814,13 +4018,13 @@ class StableBrowser {
|
|
|
2814
4018
|
await this.page.reload();
|
|
2815
4019
|
}
|
|
2816
4020
|
catch (e) {
|
|
2817
|
-
|
|
4021
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2818
4022
|
}
|
|
2819
4023
|
finally {
|
|
2820
4024
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2821
4025
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2822
4026
|
const endTime = Date.now();
|
|
2823
|
-
|
|
4027
|
+
_reportToWorld(world, {
|
|
2824
4028
|
type: Types.GET_PAGE_STATUS,
|
|
2825
4029
|
text: "page relaod",
|
|
2826
4030
|
screenshotId,
|
|
@@ -2829,7 +4033,7 @@ class StableBrowser {
|
|
|
2829
4033
|
status: "FAILED",
|
|
2830
4034
|
startTime,
|
|
2831
4035
|
endTime,
|
|
2832
|
-
message: error
|
|
4036
|
+
message: error?.message,
|
|
2833
4037
|
}
|
|
2834
4038
|
: {
|
|
2835
4039
|
status: "PASSED",
|
|
@@ -2856,11 +4060,216 @@ class StableBrowser {
|
|
|
2856
4060
|
console.log("#-#");
|
|
2857
4061
|
}
|
|
2858
4062
|
}
|
|
2859
|
-
|
|
2860
|
-
if (
|
|
2861
|
-
|
|
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) {
|
|
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
|
+
}
|
|
2862
4248
|
}
|
|
2863
|
-
|
|
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);
|
|
2864
4273
|
}
|
|
2865
4274
|
}
|
|
2866
4275
|
function createTimedPromise(promise, label) {
|
|
@@ -2868,156 +4277,5 @@ function createTimedPromise(promise, label) {
|
|
|
2868
4277
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2869
4278
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2870
4279
|
}
|
|
2871
|
-
const KEYBOARD_EVENTS = [
|
|
2872
|
-
"ALT",
|
|
2873
|
-
"AltGraph",
|
|
2874
|
-
"CapsLock",
|
|
2875
|
-
"Control",
|
|
2876
|
-
"Fn",
|
|
2877
|
-
"FnLock",
|
|
2878
|
-
"Hyper",
|
|
2879
|
-
"Meta",
|
|
2880
|
-
"NumLock",
|
|
2881
|
-
"ScrollLock",
|
|
2882
|
-
"Shift",
|
|
2883
|
-
"Super",
|
|
2884
|
-
"Symbol",
|
|
2885
|
-
"SymbolLock",
|
|
2886
|
-
"Enter",
|
|
2887
|
-
"Tab",
|
|
2888
|
-
"ArrowDown",
|
|
2889
|
-
"ArrowLeft",
|
|
2890
|
-
"ArrowRight",
|
|
2891
|
-
"ArrowUp",
|
|
2892
|
-
"End",
|
|
2893
|
-
"Home",
|
|
2894
|
-
"PageDown",
|
|
2895
|
-
"PageUp",
|
|
2896
|
-
"Backspace",
|
|
2897
|
-
"Clear",
|
|
2898
|
-
"Copy",
|
|
2899
|
-
"CrSel",
|
|
2900
|
-
"Cut",
|
|
2901
|
-
"Delete",
|
|
2902
|
-
"EraseEof",
|
|
2903
|
-
"ExSel",
|
|
2904
|
-
"Insert",
|
|
2905
|
-
"Paste",
|
|
2906
|
-
"Redo",
|
|
2907
|
-
"Undo",
|
|
2908
|
-
"Accept",
|
|
2909
|
-
"Again",
|
|
2910
|
-
"Attn",
|
|
2911
|
-
"Cancel",
|
|
2912
|
-
"ContextMenu",
|
|
2913
|
-
"Escape",
|
|
2914
|
-
"Execute",
|
|
2915
|
-
"Find",
|
|
2916
|
-
"Finish",
|
|
2917
|
-
"Help",
|
|
2918
|
-
"Pause",
|
|
2919
|
-
"Play",
|
|
2920
|
-
"Props",
|
|
2921
|
-
"Select",
|
|
2922
|
-
"ZoomIn",
|
|
2923
|
-
"ZoomOut",
|
|
2924
|
-
"BrightnessDown",
|
|
2925
|
-
"BrightnessUp",
|
|
2926
|
-
"Eject",
|
|
2927
|
-
"LogOff",
|
|
2928
|
-
"Power",
|
|
2929
|
-
"PowerOff",
|
|
2930
|
-
"PrintScreen",
|
|
2931
|
-
"Hibernate",
|
|
2932
|
-
"Standby",
|
|
2933
|
-
"WakeUp",
|
|
2934
|
-
"AllCandidates",
|
|
2935
|
-
"Alphanumeric",
|
|
2936
|
-
"CodeInput",
|
|
2937
|
-
"Compose",
|
|
2938
|
-
"Convert",
|
|
2939
|
-
"Dead",
|
|
2940
|
-
"FinalMode",
|
|
2941
|
-
"GroupFirst",
|
|
2942
|
-
"GroupLast",
|
|
2943
|
-
"GroupNext",
|
|
2944
|
-
"GroupPrevious",
|
|
2945
|
-
"ModeChange",
|
|
2946
|
-
"NextCandidate",
|
|
2947
|
-
"NonConvert",
|
|
2948
|
-
"PreviousCandidate",
|
|
2949
|
-
"Process",
|
|
2950
|
-
"SingleCandidate",
|
|
2951
|
-
"HangulMode",
|
|
2952
|
-
"HanjaMode",
|
|
2953
|
-
"JunjaMode",
|
|
2954
|
-
"Eisu",
|
|
2955
|
-
"Hankaku",
|
|
2956
|
-
"Hiragana",
|
|
2957
|
-
"HiraganaKatakana",
|
|
2958
|
-
"KanaMode",
|
|
2959
|
-
"KanjiMode",
|
|
2960
|
-
"Katakana",
|
|
2961
|
-
"Romaji",
|
|
2962
|
-
"Zenkaku",
|
|
2963
|
-
"ZenkakuHanaku",
|
|
2964
|
-
"F1",
|
|
2965
|
-
"F2",
|
|
2966
|
-
"F3",
|
|
2967
|
-
"F4",
|
|
2968
|
-
"F5",
|
|
2969
|
-
"F6",
|
|
2970
|
-
"F7",
|
|
2971
|
-
"F8",
|
|
2972
|
-
"F9",
|
|
2973
|
-
"F10",
|
|
2974
|
-
"F11",
|
|
2975
|
-
"F12",
|
|
2976
|
-
"Soft1",
|
|
2977
|
-
"Soft2",
|
|
2978
|
-
"Soft3",
|
|
2979
|
-
"Soft4",
|
|
2980
|
-
"ChannelDown",
|
|
2981
|
-
"ChannelUp",
|
|
2982
|
-
"Close",
|
|
2983
|
-
"MailForward",
|
|
2984
|
-
"MailReply",
|
|
2985
|
-
"MailSend",
|
|
2986
|
-
"MediaFastForward",
|
|
2987
|
-
"MediaPause",
|
|
2988
|
-
"MediaPlay",
|
|
2989
|
-
"MediaPlayPause",
|
|
2990
|
-
"MediaRecord",
|
|
2991
|
-
"MediaRewind",
|
|
2992
|
-
"MediaStop",
|
|
2993
|
-
"MediaTrackNext",
|
|
2994
|
-
"MediaTrackPrevious",
|
|
2995
|
-
"AudioBalanceLeft",
|
|
2996
|
-
"AudioBalanceRight",
|
|
2997
|
-
"AudioBassBoostDown",
|
|
2998
|
-
"AudioBassBoostToggle",
|
|
2999
|
-
"AudioBassBoostUp",
|
|
3000
|
-
"AudioFaderFront",
|
|
3001
|
-
"AudioFaderRear",
|
|
3002
|
-
"AudioSurroundModeNext",
|
|
3003
|
-
"AudioTrebleDown",
|
|
3004
|
-
"AudioTrebleUp",
|
|
3005
|
-
"AudioVolumeDown",
|
|
3006
|
-
"AudioVolumeMute",
|
|
3007
|
-
"AudioVolumeUp",
|
|
3008
|
-
"MicrophoneToggle",
|
|
3009
|
-
"MicrophoneVolumeDown",
|
|
3010
|
-
"MicrophoneVolumeMute",
|
|
3011
|
-
"MicrophoneVolumeUp",
|
|
3012
|
-
"TV",
|
|
3013
|
-
"TV3DMode",
|
|
3014
|
-
"TVAntennaCable",
|
|
3015
|
-
"TVAudioDescription",
|
|
3016
|
-
];
|
|
3017
|
-
function unEscapeString(str) {
|
|
3018
|
-
const placeholder = "__NEWLINE__";
|
|
3019
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
3020
|
-
return str;
|
|
3021
|
-
}
|
|
3022
4280
|
export { StableBrowser };
|
|
3023
4281
|
//# sourceMappingURL=stable_browser.js.map
|