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