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