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