automation_model 1.0.442-dev → 1.0.442-stage
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/lib/api.d.ts +42 -1
- package/lib/api.js +192 -8
- package/lib/api.js.map +1 -1
- package/lib/auto_page.js +21 -39
- package/lib/auto_page.js.map +1 -1
- package/lib/axe/axe.mini.js +12 -0
- package/lib/browser_manager.js +19 -9
- package/lib/browser_manager.js.map +1 -1
- package/lib/command_common.d.ts +5 -0
- package/lib/command_common.js +123 -0
- package/lib/command_common.js.map +1 -0
- package/lib/environment.d.ts +3 -0
- package/lib/environment.js +5 -2
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.d.ts +6 -0
- package/lib/error-messages.js +142 -0
- package/lib/error-messages.js.map +1 -0
- package/lib/init_browser.d.ts +2 -1
- package/lib/init_browser.js +51 -3
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.d.ts +7 -0
- package/lib/locate_element.js +213 -0
- package/lib/locate_element.js.map +1 -0
- package/lib/network.d.ts +3 -0
- package/lib/network.js +144 -0
- package/lib/network.js.map +1 -0
- package/lib/stable_browser.d.ts +30 -20
- package/lib/stable_browser.js +583 -642
- package/lib/stable_browser.js.map +1 -1
- package/lib/test_context.d.ts +2 -0
- package/lib/test_context.js +11 -10
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +3 -1
- package/lib/utils.js +68 -1
- package/lib/utils.js.map +1 -1
- package/package.json +7 -9
package/lib/stable_browser.js
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
import { expect } from "@playwright/test";
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
4
|
import fs from "fs";
|
|
5
|
+
import { Jimp } from "jimp";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import reg_parser from "regex-parser";
|
|
7
|
-
import sharp from "sharp";
|
|
8
8
|
import { findDateAlternatives, findNumberAlternatives } from "./analyze_helper.js";
|
|
9
9
|
import { getDateTimeValue } from "./date_time.js";
|
|
10
10
|
import drawRectangle from "./drawRect.js";
|
|
11
11
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
-
import { decrypt } from "./utils.js";
|
|
13
|
+
import { maskValue, replaceWithLocalTestData } from "./utils.js";
|
|
15
14
|
import csv from "csv-parser";
|
|
16
15
|
import { Readable } from "node:stream";
|
|
17
16
|
import readline from "readline";
|
|
17
|
+
import { getContext } from "./init_browser.js";
|
|
18
|
+
import { locate_element } from "./locate_element.js";
|
|
19
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot } from "./command_common.js";
|
|
20
|
+
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
18
21
|
const Types = {
|
|
19
22
|
CLICK: "click_element",
|
|
20
23
|
NAVIGATE: "navigate",
|
|
@@ -42,15 +45,24 @@ const Types = {
|
|
|
42
45
|
LOAD_DATA: "load_data",
|
|
43
46
|
SET_INPUT: "set_input",
|
|
44
47
|
};
|
|
48
|
+
export const apps = {};
|
|
45
49
|
class StableBrowser {
|
|
46
|
-
|
|
50
|
+
browser;
|
|
51
|
+
page;
|
|
52
|
+
logger;
|
|
53
|
+
context;
|
|
54
|
+
world;
|
|
55
|
+
project_path = null;
|
|
56
|
+
webLogFile = null;
|
|
57
|
+
networkLogger = null;
|
|
58
|
+
configuration = null;
|
|
59
|
+
appName = "main";
|
|
60
|
+
constructor(browser, page, logger = null, context = null, world = null) {
|
|
47
61
|
this.browser = browser;
|
|
48
62
|
this.page = page;
|
|
49
63
|
this.logger = logger;
|
|
50
64
|
this.context = context;
|
|
51
|
-
this.
|
|
52
|
-
this.webLogFile = null;
|
|
53
|
-
this.configuration = null;
|
|
65
|
+
this.world = world;
|
|
54
66
|
if (!this.logger) {
|
|
55
67
|
this.logger = console;
|
|
56
68
|
}
|
|
@@ -76,16 +88,31 @@ class StableBrowser {
|
|
|
76
88
|
this.logger.error("unable to read ai_config.json");
|
|
77
89
|
}
|
|
78
90
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
79
|
-
this.
|
|
80
|
-
this.registerConsoleLogListener(page, context, this.webLogFile);
|
|
81
|
-
this.registerRequestListener();
|
|
91
|
+
this.world = world;
|
|
82
92
|
context.pages = [this.page];
|
|
83
93
|
context.pageLoading = { status: false };
|
|
94
|
+
this.registerEventListeners(this.context);
|
|
95
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
96
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
97
|
+
}
|
|
98
|
+
registerEventListeners(context) {
|
|
99
|
+
this.registerConsoleLogListener(this.page, context);
|
|
100
|
+
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
101
|
+
if (!context.pageLoading) {
|
|
102
|
+
context.pageLoading = { status: false };
|
|
103
|
+
}
|
|
84
104
|
context.playContext.on("page", async function (page) {
|
|
105
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
106
|
+
console.log("close unexpected popups");
|
|
107
|
+
await page.close();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
85
110
|
context.pageLoading.status = true;
|
|
86
111
|
this.page = page;
|
|
87
112
|
context.page = page;
|
|
88
113
|
context.pages.push(page);
|
|
114
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
115
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
89
116
|
page.on("close", async () => {
|
|
90
117
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
91
118
|
this.context.pages.pop();
|
|
@@ -110,6 +137,36 @@ class StableBrowser {
|
|
|
110
137
|
context.pageLoading.status = false;
|
|
111
138
|
}.bind(this));
|
|
112
139
|
}
|
|
140
|
+
async switchApp(appName) {
|
|
141
|
+
// check if the current app (this.appName) is the same as the new app
|
|
142
|
+
if (this.appName === appName) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let navigate = false;
|
|
146
|
+
if (!apps[appName]) {
|
|
147
|
+
let newContext = await getContext(null, false, this.logger, appName, false, this);
|
|
148
|
+
navigate = true;
|
|
149
|
+
apps[appName] = {
|
|
150
|
+
context: newContext,
|
|
151
|
+
browser: newContext.browser,
|
|
152
|
+
page: newContext.page,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const tempContext = {};
|
|
156
|
+
this._copyContext(this, tempContext);
|
|
157
|
+
this._copyContext(apps[appName], this);
|
|
158
|
+
apps[this.appName] = tempContext;
|
|
159
|
+
this.appName = appName;
|
|
160
|
+
if (navigate) {
|
|
161
|
+
await this.goto(this.context.environment.baseUrl);
|
|
162
|
+
await this.waitForPageLoad();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
_copyContext(from, to) {
|
|
166
|
+
to.browser = from.browser;
|
|
167
|
+
to.page = from.page;
|
|
168
|
+
to.context = from.context;
|
|
169
|
+
}
|
|
113
170
|
getWebLogFile(logFolder) {
|
|
114
171
|
if (!fs.existsSync(logFolder)) {
|
|
115
172
|
fs.mkdirSync(logFolder, { recursive: true });
|
|
@@ -121,37 +178,63 @@ class StableBrowser {
|
|
|
121
178
|
const fileName = nextIndex + ".json";
|
|
122
179
|
return path.join(logFolder, fileName);
|
|
123
180
|
}
|
|
124
|
-
registerConsoleLogListener(page, context
|
|
181
|
+
registerConsoleLogListener(page, context) {
|
|
125
182
|
if (!this.context.webLogger) {
|
|
126
183
|
this.context.webLogger = [];
|
|
127
184
|
}
|
|
128
185
|
page.on("console", async (msg) => {
|
|
129
|
-
|
|
186
|
+
const obj = {
|
|
130
187
|
type: msg.type(),
|
|
131
188
|
text: msg.text(),
|
|
132
189
|
location: msg.location(),
|
|
133
190
|
time: new Date().toISOString(),
|
|
134
|
-
}
|
|
135
|
-
|
|
191
|
+
};
|
|
192
|
+
this.context.webLogger.push(obj);
|
|
193
|
+
if (msg.type() === "error") {
|
|
194
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
195
|
+
}
|
|
136
196
|
});
|
|
137
197
|
}
|
|
138
|
-
registerRequestListener() {
|
|
139
|
-
this.
|
|
198
|
+
registerRequestListener(page, context, logFile) {
|
|
199
|
+
if (!this.context.networkLogger) {
|
|
200
|
+
this.context.networkLogger = [];
|
|
201
|
+
}
|
|
202
|
+
page.on("request", async (data) => {
|
|
203
|
+
const startTime = new Date().getTime();
|
|
140
204
|
try {
|
|
141
|
-
const pageUrl = new URL(
|
|
205
|
+
const pageUrl = new URL(page.url());
|
|
142
206
|
const requestUrl = new URL(data.url());
|
|
143
207
|
if (pageUrl.hostname === requestUrl.hostname) {
|
|
144
208
|
const method = data.method();
|
|
145
|
-
if (
|
|
209
|
+
if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
146
210
|
const token = await data.headerValue("Authorization");
|
|
147
211
|
if (token) {
|
|
148
|
-
|
|
212
|
+
context.authtoken = token;
|
|
149
213
|
}
|
|
150
214
|
}
|
|
151
215
|
}
|
|
216
|
+
const response = await data.response();
|
|
217
|
+
const endTime = new Date().getTime();
|
|
218
|
+
const obj = {
|
|
219
|
+
url: data.url(),
|
|
220
|
+
method: data.method(),
|
|
221
|
+
postData: data.postData(),
|
|
222
|
+
error: data.failure() ? data.failure().errorText : null,
|
|
223
|
+
duration: endTime - startTime,
|
|
224
|
+
startTime,
|
|
225
|
+
};
|
|
226
|
+
context.networkLogger.push(obj);
|
|
227
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
152
228
|
}
|
|
153
229
|
catch (error) {
|
|
154
230
|
console.error("Error in request listener", error);
|
|
231
|
+
context.networkLogger.push({
|
|
232
|
+
error: "not able to listen",
|
|
233
|
+
message: error.message,
|
|
234
|
+
stack: error.stack,
|
|
235
|
+
time: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
// await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
|
|
155
238
|
}
|
|
156
239
|
});
|
|
157
240
|
}
|
|
@@ -166,20 +249,6 @@ class StableBrowser {
|
|
|
166
249
|
timeout: 60000,
|
|
167
250
|
});
|
|
168
251
|
}
|
|
169
|
-
_validateSelectors(selectors) {
|
|
170
|
-
if (!selectors) {
|
|
171
|
-
throw new Error("selectors is null");
|
|
172
|
-
}
|
|
173
|
-
if (!selectors.locators) {
|
|
174
|
-
throw new Error("selectors.locators is null");
|
|
175
|
-
}
|
|
176
|
-
if (!Array.isArray(selectors.locators)) {
|
|
177
|
-
throw new Error("selectors.locators expected to be array");
|
|
178
|
-
}
|
|
179
|
-
if (selectors.locators.length === 0) {
|
|
180
|
-
throw new Error("selectors.locators expected to be non empty array");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
252
|
_fixUsingParams(text, _params) {
|
|
184
253
|
if (!_params || typeof text !== "string") {
|
|
185
254
|
return text;
|
|
@@ -247,7 +316,7 @@ class StableBrowser {
|
|
|
247
316
|
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
248
317
|
}
|
|
249
318
|
}
|
|
250
|
-
if (locator
|
|
319
|
+
if (locator?.engine) {
|
|
251
320
|
if (locator.engine === "css") {
|
|
252
321
|
locatorReturn = scope.locator(locator.selector);
|
|
253
322
|
}
|
|
@@ -268,7 +337,10 @@ class StableBrowser {
|
|
|
268
337
|
return locatorReturn;
|
|
269
338
|
}
|
|
270
339
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
271
|
-
|
|
340
|
+
if (css && css.locator) {
|
|
341
|
+
css = css.locator;
|
|
342
|
+
}
|
|
343
|
+
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*:not(script, style, head)", false, false, _params);
|
|
272
344
|
if (result.elementCount === 0) {
|
|
273
345
|
return;
|
|
274
346
|
}
|
|
@@ -283,7 +355,7 @@ class StableBrowser {
|
|
|
283
355
|
}
|
|
284
356
|
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
|
|
285
357
|
//const stringifyText = JSON.stringify(text);
|
|
286
|
-
return await scope.evaluate(([text, tag, regex, partial]) => {
|
|
358
|
+
return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
|
|
287
359
|
function isParent(parent, child) {
|
|
288
360
|
let currentNode = child.parentNode;
|
|
289
361
|
while (currentNode !== null) {
|
|
@@ -295,6 +367,15 @@ class StableBrowser {
|
|
|
295
367
|
return false;
|
|
296
368
|
}
|
|
297
369
|
document.isParent = isParent;
|
|
370
|
+
function getRegex(str) {
|
|
371
|
+
const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
|
|
372
|
+
if (!match) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
let [_, pattern, flags] = match;
|
|
376
|
+
return new RegExp(pattern, flags);
|
|
377
|
+
}
|
|
378
|
+
document.getRegex = getRegex;
|
|
298
379
|
function collectAllShadowDomElements(element, result = []) {
|
|
299
380
|
// Check and add the element if it has a shadow root
|
|
300
381
|
if (element.shadowRoot) {
|
|
@@ -311,7 +392,11 @@ class StableBrowser {
|
|
|
311
392
|
}
|
|
312
393
|
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
313
394
|
if (!tag) {
|
|
314
|
-
tag = "
|
|
395
|
+
tag = "*:not(script, style, head)";
|
|
396
|
+
}
|
|
397
|
+
let regexpSearch = document.getRegex(text);
|
|
398
|
+
if (regexpSearch) {
|
|
399
|
+
regex = true;
|
|
315
400
|
}
|
|
316
401
|
let elements = Array.from(document.querySelectorAll(tag));
|
|
317
402
|
let shadowHosts = [];
|
|
@@ -328,7 +413,9 @@ class StableBrowser {
|
|
|
328
413
|
let randomToken = null;
|
|
329
414
|
const foundElements = [];
|
|
330
415
|
if (regex) {
|
|
331
|
-
|
|
416
|
+
if (!regexpSearch) {
|
|
417
|
+
regexpSearch = new RegExp(text, "im");
|
|
418
|
+
}
|
|
332
419
|
for (let i = 0; i < elements.length; i++) {
|
|
333
420
|
const element = elements[i];
|
|
334
421
|
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
@@ -342,8 +429,8 @@ class StableBrowser {
|
|
|
342
429
|
for (let i = 0; i < elements.length; i++) {
|
|
343
430
|
const element = elements[i];
|
|
344
431
|
if (partial) {
|
|
345
|
-
if ((element.innerText && element.innerText.trim().includes(text)) ||
|
|
346
|
-
(element.value && element.value.includes(text))) {
|
|
432
|
+
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
433
|
+
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
347
434
|
foundElements.push(element);
|
|
348
435
|
}
|
|
349
436
|
}
|
|
@@ -388,18 +475,29 @@ class StableBrowser {
|
|
|
388
475
|
}
|
|
389
476
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
390
477
|
let locatorSearch = selectorHierarchy[index];
|
|
478
|
+
try {
|
|
479
|
+
locatorSearch = JSON.parse(this._fixUsingParams(JSON.stringify(locatorSearch), _params));
|
|
480
|
+
}
|
|
481
|
+
catch (e) {
|
|
482
|
+
console.error(e);
|
|
483
|
+
}
|
|
391
484
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
392
485
|
let locator = null;
|
|
393
486
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
394
487
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
395
488
|
if (!locatorString) {
|
|
489
|
+
info.failCause.textNotFound = true;
|
|
490
|
+
info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
|
|
396
491
|
return;
|
|
397
492
|
}
|
|
398
493
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
399
494
|
}
|
|
400
495
|
else if (locatorSearch.text) {
|
|
401
|
-
let
|
|
496
|
+
let text = this._fixUsingParams(locatorSearch.text, _params);
|
|
497
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, _params);
|
|
402
498
|
if (result.elementCount === 0) {
|
|
499
|
+
info.failCause.textNotFound = true;
|
|
500
|
+
info.failCause.lastError = "failed to locate element by text: " + text;
|
|
403
501
|
return;
|
|
404
502
|
}
|
|
405
503
|
locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
|
|
@@ -416,6 +514,9 @@ class StableBrowser {
|
|
|
416
514
|
// cssHref = true;
|
|
417
515
|
// }
|
|
418
516
|
let count = await locator.count();
|
|
517
|
+
if (count > 0 && !info.failCause.count) {
|
|
518
|
+
info.failCause.count = count;
|
|
519
|
+
}
|
|
419
520
|
//info.log += "total elements found " + count + "\n";
|
|
420
521
|
//let visibleCount = 0;
|
|
421
522
|
let visibleLocator = null;
|
|
@@ -433,6 +534,8 @@ class StableBrowser {
|
|
|
433
534
|
foundLocators.push(locator.nth(j));
|
|
434
535
|
}
|
|
435
536
|
else {
|
|
537
|
+
info.failCause.visible = visible;
|
|
538
|
+
info.failCause.enabled = enabled;
|
|
436
539
|
if (!info.printMessages) {
|
|
437
540
|
info.printMessages = {};
|
|
438
541
|
}
|
|
@@ -444,6 +547,11 @@ class StableBrowser {
|
|
|
444
547
|
}
|
|
445
548
|
}
|
|
446
549
|
async closeUnexpectedPopups(info, _params) {
|
|
550
|
+
if (!info) {
|
|
551
|
+
info = {};
|
|
552
|
+
info.failCause = {};
|
|
553
|
+
info.log = "";
|
|
554
|
+
}
|
|
447
555
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
448
556
|
if (!info) {
|
|
449
557
|
info = {};
|
|
@@ -476,13 +584,18 @@ class StableBrowser {
|
|
|
476
584
|
if (result.foundElements.length > 0) {
|
|
477
585
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
478
586
|
await dialogCloseLocator.click();
|
|
587
|
+
// wait for the dialog to close
|
|
588
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
479
589
|
return { rerun: true };
|
|
480
590
|
}
|
|
481
591
|
}
|
|
482
592
|
}
|
|
483
593
|
return { rerun: false };
|
|
484
594
|
}
|
|
485
|
-
async _locate(selectors, info, _params, timeout
|
|
595
|
+
async _locate(selectors, info, _params, timeout) {
|
|
596
|
+
if (!timeout) {
|
|
597
|
+
timeout = 30000;
|
|
598
|
+
}
|
|
486
599
|
for (let i = 0; i < 3; i++) {
|
|
487
600
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
488
601
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
@@ -496,32 +609,46 @@ class StableBrowser {
|
|
|
496
609
|
}
|
|
497
610
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
498
611
|
}
|
|
499
|
-
async
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
612
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
613
|
+
if (!info) {
|
|
614
|
+
info = {};
|
|
615
|
+
info.failCause = {};
|
|
616
|
+
info.log = "";
|
|
617
|
+
}
|
|
505
618
|
let scope = this.page;
|
|
619
|
+
if (selectors.frame) {
|
|
620
|
+
return selectors.frame;
|
|
621
|
+
}
|
|
506
622
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
507
|
-
const findFrame = (frame, framescope) => {
|
|
623
|
+
const findFrame = async (frame, framescope) => {
|
|
508
624
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
509
625
|
let frameLocator = frame.selectors[i];
|
|
510
626
|
if (frameLocator.css) {
|
|
511
|
-
|
|
512
|
-
|
|
627
|
+
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
628
|
+
if (frameLocator.index) {
|
|
629
|
+
testframescope = framescope.nth(frameLocator.index);
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
await testframescope.owner().evaluateHandle(() => true, null, {
|
|
633
|
+
timeout: 5000,
|
|
634
|
+
});
|
|
635
|
+
framescope = testframescope;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
console.error("frame not found " + frameLocator.css);
|
|
640
|
+
}
|
|
513
641
|
}
|
|
514
642
|
}
|
|
515
643
|
if (frame.children) {
|
|
516
|
-
return findFrame(frame.children, framescope);
|
|
644
|
+
return await findFrame(frame.children, framescope);
|
|
517
645
|
}
|
|
518
646
|
return framescope;
|
|
519
647
|
};
|
|
520
|
-
info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
|
|
521
648
|
while (true) {
|
|
522
649
|
let frameFound = false;
|
|
523
650
|
if (selectors.nestFrmLoc) {
|
|
524
|
-
scope = findFrame(selectors.nestFrmLoc, scope);
|
|
651
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
525
652
|
frameFound = true;
|
|
526
653
|
break;
|
|
527
654
|
}
|
|
@@ -541,6 +668,8 @@ class StableBrowser {
|
|
|
541
668
|
if (!scope) {
|
|
542
669
|
info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
543
670
|
if (performance.now() - startTime > timeout) {
|
|
671
|
+
info.failCause.iframeNotFound = true;
|
|
672
|
+
info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
|
|
544
673
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
545
674
|
}
|
|
546
675
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -550,6 +679,30 @@ class StableBrowser {
|
|
|
550
679
|
}
|
|
551
680
|
}
|
|
552
681
|
}
|
|
682
|
+
if (!scope) {
|
|
683
|
+
scope = this.page;
|
|
684
|
+
}
|
|
685
|
+
return scope;
|
|
686
|
+
}
|
|
687
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
688
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
689
|
+
return scope.evaluate(() => {
|
|
690
|
+
var bodyContent = document.body.innerHTML;
|
|
691
|
+
return bodyContent;
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async _locate_internal(selectors, info, _params, timeout = 30000) {
|
|
695
|
+
if (!info) {
|
|
696
|
+
info = {};
|
|
697
|
+
info.failCause = {};
|
|
698
|
+
info.log = "";
|
|
699
|
+
}
|
|
700
|
+
let highPriorityTimeout = 5000;
|
|
701
|
+
let visibleOnlyTimeout = 6000;
|
|
702
|
+
let startTime = performance.now();
|
|
703
|
+
let locatorsCount = 0;
|
|
704
|
+
//let arrayMode = Array.isArray(selectors);
|
|
705
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
553
706
|
let selectorsLocators = null;
|
|
554
707
|
selectorsLocators = selectors.locators;
|
|
555
708
|
// group selectors by priority
|
|
@@ -651,6 +804,8 @@ class StableBrowser {
|
|
|
651
804
|
}
|
|
652
805
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
653
806
|
info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
807
|
+
info.failCause.locatorNotFound = true;
|
|
808
|
+
info.failCause.lastError = "failed to locate unique element";
|
|
654
809
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
655
810
|
}
|
|
656
811
|
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
@@ -682,89 +837,129 @@ class StableBrowser {
|
|
|
682
837
|
});
|
|
683
838
|
result.locatorIndex = i;
|
|
684
839
|
}
|
|
840
|
+
if (foundLocators.length > 1) {
|
|
841
|
+
info.failCause.foundMultiple = true;
|
|
842
|
+
}
|
|
685
843
|
}
|
|
686
844
|
return result;
|
|
687
845
|
}
|
|
688
|
-
async
|
|
689
|
-
this._validateSelectors(selectors);
|
|
846
|
+
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
690
847
|
const startTime = Date.now();
|
|
691
|
-
|
|
692
|
-
|
|
848
|
+
let timeout = 30000;
|
|
849
|
+
if (options && options.timeout) {
|
|
850
|
+
timeout = options.timeout;
|
|
693
851
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
852
|
+
while (true) {
|
|
853
|
+
try {
|
|
854
|
+
const result = await locate_element(this.context, elementDescription, "click");
|
|
855
|
+
if (result?.elementNumber >= 0) {
|
|
856
|
+
const selectors = {
|
|
857
|
+
frame: result?.frame,
|
|
858
|
+
locators: [
|
|
859
|
+
{
|
|
860
|
+
css: result?.css,
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
};
|
|
864
|
+
await this.click(selectors, _params, options, world);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch (e) {
|
|
869
|
+
if (performance.now() - startTime > timeout) {
|
|
870
|
+
// throw e;
|
|
871
|
+
await _commandError({ text: "simpleClick", operation: "simpleClick", elementDescription, info: {} }, e, this);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
878
|
+
const startTime = Date.now();
|
|
879
|
+
let timeout = 30000;
|
|
880
|
+
if (options && options.timeout) {
|
|
881
|
+
timeout = options.timeout;
|
|
882
|
+
}
|
|
883
|
+
while (true) {
|
|
884
|
+
try {
|
|
885
|
+
const result = await locate_element(this.context, elementDescription, "fill", value);
|
|
886
|
+
if (result?.elementNumber >= 0) {
|
|
887
|
+
const selectors = {
|
|
888
|
+
frame: result?.frame,
|
|
889
|
+
locators: [
|
|
890
|
+
{
|
|
891
|
+
css: result?.css,
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
await this.clickType(selectors, value, false, _params, options, world);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch (e) {
|
|
900
|
+
if (performance.now() - startTime > timeout) {
|
|
901
|
+
// throw e;
|
|
902
|
+
await _commandError({ text: "simpleClickType", operation: "simpleClickType", value, elementDescription, info: {} }, e, this);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
async click(selectors, _params, options = {}, world = null) {
|
|
909
|
+
const state = {
|
|
910
|
+
selectors,
|
|
911
|
+
_params,
|
|
912
|
+
options,
|
|
913
|
+
world,
|
|
914
|
+
text: "Click element",
|
|
915
|
+
type: Types.CLICK,
|
|
916
|
+
operation: "click",
|
|
917
|
+
log: "***** click on " + selectors.element_name + " *****\n",
|
|
918
|
+
};
|
|
701
919
|
try {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
920
|
+
await _preCommand(state, this);
|
|
921
|
+
if (state.options && state.options.context) {
|
|
922
|
+
state.selectors.locators[0].text = state.options.context;
|
|
923
|
+
}
|
|
705
924
|
try {
|
|
706
|
-
await
|
|
707
|
-
await element.click();
|
|
925
|
+
await state.element.click();
|
|
708
926
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
709
927
|
}
|
|
710
928
|
catch (e) {
|
|
711
929
|
// await this.closeUnexpectedPopups();
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
await element.dispatchEvent("click");
|
|
930
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
931
|
+
await state.element.dispatchEvent("click");
|
|
715
932
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
716
933
|
}
|
|
717
934
|
await this.waitForPageLoad();
|
|
718
|
-
return info;
|
|
935
|
+
return state.info;
|
|
719
936
|
}
|
|
720
937
|
catch (e) {
|
|
721
|
-
|
|
722
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
723
|
-
info.screenshotPath = screenshotPath;
|
|
724
|
-
Object.assign(e, { info: info });
|
|
725
|
-
error = e;
|
|
726
|
-
throw e;
|
|
938
|
+
await _commandError(state, e, this);
|
|
727
939
|
}
|
|
728
940
|
finally {
|
|
729
|
-
|
|
730
|
-
this._reportToWorld(world, {
|
|
731
|
-
element_name: selectors.element_name,
|
|
732
|
-
type: Types.CLICK,
|
|
733
|
-
text: `Click element`,
|
|
734
|
-
screenshotId,
|
|
735
|
-
result: error
|
|
736
|
-
? {
|
|
737
|
-
status: "FAILED",
|
|
738
|
-
startTime,
|
|
739
|
-
endTime,
|
|
740
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
741
|
-
}
|
|
742
|
-
: {
|
|
743
|
-
status: "PASSED",
|
|
744
|
-
startTime,
|
|
745
|
-
endTime,
|
|
746
|
-
},
|
|
747
|
-
info: info,
|
|
748
|
-
});
|
|
941
|
+
_commandFinally(state, this);
|
|
749
942
|
}
|
|
750
943
|
}
|
|
751
944
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
945
|
+
const state = {
|
|
946
|
+
selectors,
|
|
947
|
+
_params,
|
|
948
|
+
options,
|
|
949
|
+
world,
|
|
950
|
+
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
951
|
+
text: checked ? `Check element` : `Uncheck element`,
|
|
952
|
+
operation: "setCheck",
|
|
953
|
+
log: "***** check " + selectors.element_name + " *****\n",
|
|
954
|
+
};
|
|
762
955
|
try {
|
|
763
|
-
|
|
764
|
-
|
|
956
|
+
await _preCommand(state, this);
|
|
957
|
+
state.info.checked = checked;
|
|
958
|
+
// let element = await this._locate(selectors, info, _params);
|
|
959
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
765
960
|
try {
|
|
766
|
-
await this._highlightElements(element);
|
|
767
|
-
await element.setChecked(checked);
|
|
961
|
+
// await this._highlightElements(element);
|
|
962
|
+
await state.element.setChecked(checked);
|
|
768
963
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
769
964
|
}
|
|
770
965
|
catch (e) {
|
|
@@ -773,179 +968,108 @@ class StableBrowser {
|
|
|
773
968
|
}
|
|
774
969
|
else {
|
|
775
970
|
//await this.closeUnexpectedPopups();
|
|
776
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
777
|
-
element = await this._locate(selectors, info, _params);
|
|
778
|
-
await element.setChecked(checked, { timeout: 5000, force: true });
|
|
971
|
+
state.info.log += "setCheck failed, will try again" + "\n";
|
|
972
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
973
|
+
await state.element.setChecked(checked, { timeout: 5000, force: true });
|
|
779
974
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
780
975
|
}
|
|
781
976
|
}
|
|
782
977
|
await this.waitForPageLoad();
|
|
783
|
-
return info;
|
|
978
|
+
return state.info;
|
|
784
979
|
}
|
|
785
980
|
catch (e) {
|
|
786
|
-
|
|
787
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
788
|
-
info.screenshotPath = screenshotPath;
|
|
789
|
-
Object.assign(e, { info: info });
|
|
790
|
-
error = e;
|
|
791
|
-
throw e;
|
|
981
|
+
await _commandError(state, e, this);
|
|
792
982
|
}
|
|
793
983
|
finally {
|
|
794
|
-
|
|
795
|
-
this._reportToWorld(world, {
|
|
796
|
-
element_name: selectors.element_name,
|
|
797
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
798
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
799
|
-
screenshotId,
|
|
800
|
-
result: error
|
|
801
|
-
? {
|
|
802
|
-
status: "FAILED",
|
|
803
|
-
startTime,
|
|
804
|
-
endTime,
|
|
805
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
806
|
-
}
|
|
807
|
-
: {
|
|
808
|
-
status: "PASSED",
|
|
809
|
-
startTime,
|
|
810
|
-
endTime,
|
|
811
|
-
},
|
|
812
|
-
info: info,
|
|
813
|
-
});
|
|
984
|
+
_commandFinally(state, this);
|
|
814
985
|
}
|
|
815
986
|
}
|
|
816
987
|
async hover(selectors, _params, options = {}, world = null) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
988
|
+
const state = {
|
|
989
|
+
selectors,
|
|
990
|
+
_params,
|
|
991
|
+
options,
|
|
992
|
+
world,
|
|
993
|
+
type: Types.HOVER,
|
|
994
|
+
text: `Hover element`,
|
|
995
|
+
operation: "hover",
|
|
996
|
+
log: "***** hover " + selectors.element_name + " *****\n",
|
|
997
|
+
};
|
|
826
998
|
try {
|
|
827
|
-
|
|
828
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
999
|
+
await _preCommand(state, this);
|
|
829
1000
|
try {
|
|
830
|
-
await
|
|
831
|
-
await element.hover();
|
|
1001
|
+
await state.element.hover();
|
|
832
1002
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
833
1003
|
}
|
|
834
1004
|
catch (e) {
|
|
835
1005
|
//await this.closeUnexpectedPopups();
|
|
836
|
-
info.log += "hover failed, will try again" + "\n";
|
|
837
|
-
element = await this._locate(selectors, info, _params);
|
|
838
|
-
await element.hover({ timeout: 10000 });
|
|
1006
|
+
state.info.log += "hover failed, will try again" + "\n";
|
|
1007
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
1008
|
+
await state.element.hover({ timeout: 10000 });
|
|
839
1009
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
840
1010
|
}
|
|
841
1011
|
await this.waitForPageLoad();
|
|
842
|
-
return info;
|
|
1012
|
+
return state.info;
|
|
843
1013
|
}
|
|
844
1014
|
catch (e) {
|
|
845
|
-
|
|
846
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
847
|
-
info.screenshotPath = screenshotPath;
|
|
848
|
-
Object.assign(e, { info: info });
|
|
849
|
-
error = e;
|
|
850
|
-
throw e;
|
|
1015
|
+
await _commandError(state, e, this);
|
|
851
1016
|
}
|
|
852
1017
|
finally {
|
|
853
|
-
|
|
854
|
-
this._reportToWorld(world, {
|
|
855
|
-
element_name: selectors.element_name,
|
|
856
|
-
type: Types.HOVER,
|
|
857
|
-
text: `Hover element`,
|
|
858
|
-
screenshotId,
|
|
859
|
-
result: error
|
|
860
|
-
? {
|
|
861
|
-
status: "FAILED",
|
|
862
|
-
startTime,
|
|
863
|
-
endTime,
|
|
864
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
865
|
-
}
|
|
866
|
-
: {
|
|
867
|
-
status: "PASSED",
|
|
868
|
-
startTime,
|
|
869
|
-
endTime,
|
|
870
|
-
},
|
|
871
|
-
info: info,
|
|
872
|
-
});
|
|
1018
|
+
_commandFinally(state, this);
|
|
873
1019
|
}
|
|
874
1020
|
}
|
|
875
1021
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
876
|
-
this._validateSelectors(selectors);
|
|
877
1022
|
if (!values) {
|
|
878
1023
|
throw new Error("values is null");
|
|
879
1024
|
}
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1025
|
+
const state = {
|
|
1026
|
+
selectors,
|
|
1027
|
+
_params,
|
|
1028
|
+
options,
|
|
1029
|
+
world,
|
|
1030
|
+
value: values.toString(),
|
|
1031
|
+
type: Types.SELECT,
|
|
1032
|
+
text: `Select option: ${values}`,
|
|
1033
|
+
operation: "selectOption",
|
|
1034
|
+
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1035
|
+
};
|
|
888
1036
|
try {
|
|
889
|
-
|
|
890
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1037
|
+
await _preCommand(state, this);
|
|
891
1038
|
try {
|
|
892
|
-
await
|
|
893
|
-
await element.selectOption(values);
|
|
1039
|
+
await state.element.selectOption(values);
|
|
894
1040
|
}
|
|
895
1041
|
catch (e) {
|
|
896
1042
|
//await this.closeUnexpectedPopups();
|
|
897
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
898
|
-
await element.selectOption(values, { timeout: 10000, force: true });
|
|
1043
|
+
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1044
|
+
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
899
1045
|
}
|
|
900
1046
|
await this.waitForPageLoad();
|
|
901
|
-
return info;
|
|
1047
|
+
return state.info;
|
|
902
1048
|
}
|
|
903
1049
|
catch (e) {
|
|
904
|
-
|
|
905
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
906
|
-
info.screenshotPath = screenshotPath;
|
|
907
|
-
Object.assign(e, { info: info });
|
|
908
|
-
this.logger.info("click failed, will try next selector");
|
|
909
|
-
error = e;
|
|
910
|
-
throw e;
|
|
1050
|
+
await _commandError(state, e, this);
|
|
911
1051
|
}
|
|
912
1052
|
finally {
|
|
913
|
-
|
|
914
|
-
this._reportToWorld(world, {
|
|
915
|
-
element_name: selectors.element_name,
|
|
916
|
-
type: Types.SELECT,
|
|
917
|
-
text: `Select option: ${values}`,
|
|
918
|
-
value: values.toString(),
|
|
919
|
-
screenshotId,
|
|
920
|
-
result: error
|
|
921
|
-
? {
|
|
922
|
-
status: "FAILED",
|
|
923
|
-
startTime,
|
|
924
|
-
endTime,
|
|
925
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
926
|
-
}
|
|
927
|
-
: {
|
|
928
|
-
status: "PASSED",
|
|
929
|
-
startTime,
|
|
930
|
-
endTime,
|
|
931
|
-
},
|
|
932
|
-
info: info,
|
|
933
|
-
});
|
|
1053
|
+
_commandFinally(state, this);
|
|
934
1054
|
}
|
|
935
1055
|
}
|
|
936
1056
|
async type(_value, _params = null, options = {}, world = null) {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1057
|
+
const state = {
|
|
1058
|
+
value: _value,
|
|
1059
|
+
_params,
|
|
1060
|
+
options,
|
|
1061
|
+
world,
|
|
1062
|
+
locate: false,
|
|
1063
|
+
scroll: false,
|
|
1064
|
+
highlight: false,
|
|
1065
|
+
type: Types.TYPE_PRESS,
|
|
1066
|
+
text: `Type value: ${_value}`,
|
|
1067
|
+
operation: "type",
|
|
1068
|
+
log: "",
|
|
1069
|
+
};
|
|
946
1070
|
try {
|
|
947
|
-
|
|
948
|
-
const valueSegment =
|
|
1071
|
+
await _preCommand(state, this);
|
|
1072
|
+
const valueSegment = state.value.split("&&");
|
|
949
1073
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
950
1074
|
if (i > 0) {
|
|
951
1075
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -965,108 +1089,53 @@ class StableBrowser {
|
|
|
965
1089
|
await this.page.keyboard.type(value);
|
|
966
1090
|
}
|
|
967
1091
|
}
|
|
968
|
-
return info;
|
|
1092
|
+
return state.info;
|
|
969
1093
|
}
|
|
970
1094
|
catch (e) {
|
|
971
|
-
|
|
972
|
-
this.logger.error("type failed " + info.log);
|
|
973
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
974
|
-
info.screenshotPath = screenshotPath;
|
|
975
|
-
Object.assign(e, { info: info });
|
|
976
|
-
error = e;
|
|
977
|
-
throw e;
|
|
1095
|
+
await _commandError(state, e, this);
|
|
978
1096
|
}
|
|
979
1097
|
finally {
|
|
980
|
-
|
|
981
|
-
this._reportToWorld(world, {
|
|
982
|
-
type: Types.TYPE_PRESS,
|
|
983
|
-
screenshotId,
|
|
984
|
-
value: _value,
|
|
985
|
-
text: `type value: ${_value}`,
|
|
986
|
-
result: error
|
|
987
|
-
? {
|
|
988
|
-
status: "FAILED",
|
|
989
|
-
startTime,
|
|
990
|
-
endTime,
|
|
991
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
992
|
-
}
|
|
993
|
-
: {
|
|
994
|
-
status: "PASSED",
|
|
995
|
-
startTime,
|
|
996
|
-
endTime,
|
|
997
|
-
},
|
|
998
|
-
info: info,
|
|
999
|
-
});
|
|
1098
|
+
_commandFinally(state, this);
|
|
1000
1099
|
}
|
|
1001
1100
|
}
|
|
1002
1101
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
let screenshotPath = null;
|
|
1102
|
+
const state = {
|
|
1103
|
+
selectors,
|
|
1104
|
+
_params,
|
|
1105
|
+
value,
|
|
1106
|
+
options,
|
|
1107
|
+
world,
|
|
1108
|
+
type: Types.SET_INPUT,
|
|
1109
|
+
text: `Set input value`,
|
|
1110
|
+
operation: "setInputValue",
|
|
1111
|
+
log: "***** set input value " + selectors.element_name + " *****\n",
|
|
1112
|
+
};
|
|
1015
1113
|
try {
|
|
1016
|
-
|
|
1017
|
-
let
|
|
1018
|
-
await this.scrollIfNeeded(element, info);
|
|
1019
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1020
|
-
await this._highlightElements(element);
|
|
1114
|
+
await _preCommand(state, this);
|
|
1115
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1021
1116
|
try {
|
|
1022
|
-
await element.evaluateHandle((el, value) => {
|
|
1117
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1023
1118
|
el.value = value;
|
|
1024
1119
|
}, value);
|
|
1025
1120
|
}
|
|
1026
1121
|
catch (error) {
|
|
1027
1122
|
this.logger.error("setInputValue failed, will try again");
|
|
1028
|
-
|
|
1029
|
-
info.
|
|
1030
|
-
|
|
1031
|
-
await element.evaluateHandle((el, value) => {
|
|
1123
|
+
await _screenshot(state, this);
|
|
1124
|
+
Object.assign(error, { info: state.info });
|
|
1125
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1032
1126
|
el.value = value;
|
|
1033
1127
|
});
|
|
1034
1128
|
}
|
|
1035
1129
|
}
|
|
1036
1130
|
catch (e) {
|
|
1037
|
-
|
|
1038
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1039
|
-
info.screenshotPath = screenshotPath;
|
|
1040
|
-
Object.assign(e, { info: info });
|
|
1041
|
-
error = e;
|
|
1042
|
-
throw e;
|
|
1131
|
+
await _commandError(state, e, this);
|
|
1043
1132
|
}
|
|
1044
1133
|
finally {
|
|
1045
|
-
|
|
1046
|
-
this._reportToWorld(world, {
|
|
1047
|
-
element_name: selectors.element_name,
|
|
1048
|
-
type: Types.SET_INPUT,
|
|
1049
|
-
text: `Set input value`,
|
|
1050
|
-
value: value,
|
|
1051
|
-
screenshotId,
|
|
1052
|
-
result: error
|
|
1053
|
-
? {
|
|
1054
|
-
status: "FAILED",
|
|
1055
|
-
startTime,
|
|
1056
|
-
endTime,
|
|
1057
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1058
|
-
}
|
|
1059
|
-
: {
|
|
1060
|
-
status: "PASSED",
|
|
1061
|
-
startTime,
|
|
1062
|
-
endTime,
|
|
1063
|
-
},
|
|
1064
|
-
info: info,
|
|
1065
|
-
});
|
|
1134
|
+
_commandFinally(state, this);
|
|
1066
1135
|
}
|
|
1067
1136
|
}
|
|
1068
1137
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1069
|
-
|
|
1138
|
+
_validateSelectors(selectors);
|
|
1070
1139
|
const startTime = Date.now();
|
|
1071
1140
|
let error = null;
|
|
1072
1141
|
let screenshotId = null;
|
|
@@ -1132,7 +1201,8 @@ class StableBrowser {
|
|
|
1132
1201
|
}
|
|
1133
1202
|
catch (e) {
|
|
1134
1203
|
error = e;
|
|
1135
|
-
throw e;
|
|
1204
|
+
// throw e;
|
|
1205
|
+
await _commandError({ text: "setDateTime", operation: "setDateTime", selectors, value, info }, e, this);
|
|
1136
1206
|
}
|
|
1137
1207
|
finally {
|
|
1138
1208
|
const endTime = Date.now();
|
|
@@ -1159,32 +1229,32 @@ class StableBrowser {
|
|
|
1159
1229
|
}
|
|
1160
1230
|
}
|
|
1161
1231
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1162
|
-
|
|
1163
|
-
const startTime = Date.now();
|
|
1164
|
-
let error = null;
|
|
1165
|
-
let screenshotId = null;
|
|
1166
|
-
let screenshotPath = null;
|
|
1167
|
-
const info = {};
|
|
1168
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1169
|
-
info.operation = "clickType";
|
|
1170
|
-
info.selectors = selectors;
|
|
1232
|
+
_value = unEscapeString(_value);
|
|
1171
1233
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1234
|
+
const state = {
|
|
1235
|
+
selectors,
|
|
1236
|
+
_params,
|
|
1237
|
+
value: newValue,
|
|
1238
|
+
originalValue: _value,
|
|
1239
|
+
options,
|
|
1240
|
+
world,
|
|
1241
|
+
type: Types.FILL,
|
|
1242
|
+
text: `Click type input with value: ${_value}`,
|
|
1243
|
+
operation: "clickType",
|
|
1244
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1245
|
+
};
|
|
1172
1246
|
if (newValue !== _value) {
|
|
1173
1247
|
//this.logger.info(_value + "=" + newValue);
|
|
1174
1248
|
_value = newValue;
|
|
1175
1249
|
}
|
|
1176
|
-
info.value = _value;
|
|
1177
1250
|
try {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
await this.scrollIfNeeded(element, info);
|
|
1181
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1182
|
-
await this._highlightElements(element);
|
|
1251
|
+
await _preCommand(state, this);
|
|
1252
|
+
state.info.value = _value;
|
|
1183
1253
|
if (options === null || options === undefined || !options.press) {
|
|
1184
1254
|
try {
|
|
1185
|
-
let currentValue = await element.inputValue();
|
|
1255
|
+
let currentValue = await state.element.inputValue();
|
|
1186
1256
|
if (currentValue) {
|
|
1187
|
-
await element.fill("");
|
|
1257
|
+
await state.element.fill("");
|
|
1188
1258
|
}
|
|
1189
1259
|
}
|
|
1190
1260
|
catch (e) {
|
|
@@ -1193,22 +1263,22 @@ class StableBrowser {
|
|
|
1193
1263
|
}
|
|
1194
1264
|
if (options === null || options === undefined || options.press) {
|
|
1195
1265
|
try {
|
|
1196
|
-
await element.click({ timeout: 5000 });
|
|
1266
|
+
await state.element.click({ timeout: 5000 });
|
|
1197
1267
|
}
|
|
1198
1268
|
catch (e) {
|
|
1199
|
-
await element.dispatchEvent("click");
|
|
1269
|
+
await state.element.dispatchEvent("click");
|
|
1200
1270
|
}
|
|
1201
1271
|
}
|
|
1202
1272
|
else {
|
|
1203
1273
|
try {
|
|
1204
|
-
await element.focus();
|
|
1274
|
+
await state.element.focus();
|
|
1205
1275
|
}
|
|
1206
1276
|
catch (e) {
|
|
1207
|
-
await element.dispatchEvent("focus");
|
|
1277
|
+
await state.element.dispatchEvent("focus");
|
|
1208
1278
|
}
|
|
1209
1279
|
}
|
|
1210
1280
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1211
|
-
const valueSegment =
|
|
1281
|
+
const valueSegment = state.value.split("&&");
|
|
1212
1282
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1213
1283
|
if (i > 0) {
|
|
1214
1284
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1228,13 +1298,14 @@ class StableBrowser {
|
|
|
1228
1298
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1229
1299
|
}
|
|
1230
1300
|
}
|
|
1301
|
+
await _screenshot(state, this);
|
|
1231
1302
|
if (enter === true) {
|
|
1232
1303
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1233
1304
|
await this.page.keyboard.press("Enter");
|
|
1234
1305
|
await this.waitForPageLoad();
|
|
1235
1306
|
}
|
|
1236
1307
|
else if (enter === false) {
|
|
1237
|
-
await element.dispatchEvent("change");
|
|
1308
|
+
await state.element.dispatchEvent("change");
|
|
1238
1309
|
//await this.page.keyboard.press("Tab");
|
|
1239
1310
|
}
|
|
1240
1311
|
else {
|
|
@@ -1243,103 +1314,50 @@ class StableBrowser {
|
|
|
1243
1314
|
await this.waitForPageLoad();
|
|
1244
1315
|
}
|
|
1245
1316
|
}
|
|
1246
|
-
return info;
|
|
1317
|
+
return state.info;
|
|
1247
1318
|
}
|
|
1248
1319
|
catch (e) {
|
|
1249
|
-
|
|
1250
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1251
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1252
|
-
info.screenshotPath = screenshotPath;
|
|
1253
|
-
Object.assign(e, { info: info });
|
|
1254
|
-
error = e;
|
|
1255
|
-
throw e;
|
|
1320
|
+
await _commandError(state, e, this);
|
|
1256
1321
|
}
|
|
1257
1322
|
finally {
|
|
1258
|
-
|
|
1259
|
-
this._reportToWorld(world, {
|
|
1260
|
-
element_name: selectors.element_name,
|
|
1261
|
-
type: Types.FILL,
|
|
1262
|
-
screenshotId,
|
|
1263
|
-
value: _value,
|
|
1264
|
-
text: `clickType input with value: ${_value}`,
|
|
1265
|
-
result: error
|
|
1266
|
-
? {
|
|
1267
|
-
status: "FAILED",
|
|
1268
|
-
startTime,
|
|
1269
|
-
endTime,
|
|
1270
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1271
|
-
}
|
|
1272
|
-
: {
|
|
1273
|
-
status: "PASSED",
|
|
1274
|
-
startTime,
|
|
1275
|
-
endTime,
|
|
1276
|
-
},
|
|
1277
|
-
info: info,
|
|
1278
|
-
});
|
|
1323
|
+
_commandFinally(state, this);
|
|
1279
1324
|
}
|
|
1280
1325
|
}
|
|
1281
1326
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1327
|
+
const state = {
|
|
1328
|
+
selectors,
|
|
1329
|
+
_params,
|
|
1330
|
+
value: unEscapeString(value),
|
|
1331
|
+
options,
|
|
1332
|
+
world,
|
|
1333
|
+
type: Types.FILL,
|
|
1334
|
+
text: `Fill input with value: ${value}`,
|
|
1335
|
+
operation: "fill",
|
|
1336
|
+
log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
|
|
1337
|
+
};
|
|
1292
1338
|
try {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
await
|
|
1296
|
-
await element.fill(value);
|
|
1297
|
-
await element.dispatchEvent("change");
|
|
1339
|
+
await _preCommand(state, this);
|
|
1340
|
+
await state.element.fill(value);
|
|
1341
|
+
await state.element.dispatchEvent("change");
|
|
1298
1342
|
if (enter) {
|
|
1299
1343
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1300
1344
|
await this.page.keyboard.press("Enter");
|
|
1301
1345
|
}
|
|
1302
1346
|
await this.waitForPageLoad();
|
|
1303
|
-
return info;
|
|
1347
|
+
return state.info;
|
|
1304
1348
|
}
|
|
1305
1349
|
catch (e) {
|
|
1306
|
-
|
|
1307
|
-
this.logger.error("fill failed " + info.log);
|
|
1308
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1309
|
-
info.screenshotPath = screenshotPath;
|
|
1310
|
-
Object.assign(e, { info: info });
|
|
1311
|
-
error = e;
|
|
1312
|
-
throw e;
|
|
1350
|
+
await _commandError(state, e, this);
|
|
1313
1351
|
}
|
|
1314
1352
|
finally {
|
|
1315
|
-
|
|
1316
|
-
this._reportToWorld(world, {
|
|
1317
|
-
element_name: selectors.element_name,
|
|
1318
|
-
type: Types.FILL,
|
|
1319
|
-
screenshotId,
|
|
1320
|
-
value,
|
|
1321
|
-
text: `Fill input with value: ${value}`,
|
|
1322
|
-
result: error
|
|
1323
|
-
? {
|
|
1324
|
-
status: "FAILED",
|
|
1325
|
-
startTime,
|
|
1326
|
-
endTime,
|
|
1327
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1328
|
-
}
|
|
1329
|
-
: {
|
|
1330
|
-
status: "PASSED",
|
|
1331
|
-
startTime,
|
|
1332
|
-
endTime,
|
|
1333
|
-
},
|
|
1334
|
-
info: info,
|
|
1335
|
-
});
|
|
1353
|
+
_commandFinally(state, this);
|
|
1336
1354
|
}
|
|
1337
1355
|
}
|
|
1338
1356
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1339
1357
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1340
1358
|
}
|
|
1341
1359
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1342
|
-
|
|
1360
|
+
_validateSelectors(selectors);
|
|
1343
1361
|
let screenshotId = null;
|
|
1344
1362
|
let screenshotPath = null;
|
|
1345
1363
|
if (!info.log) {
|
|
@@ -1383,165 +1401,124 @@ class StableBrowser {
|
|
|
1383
1401
|
}
|
|
1384
1402
|
}
|
|
1385
1403
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1386
|
-
var _a;
|
|
1387
|
-
this._validateSelectors(selectors);
|
|
1388
1404
|
if (!pattern) {
|
|
1389
1405
|
throw new Error("pattern is null");
|
|
1390
1406
|
}
|
|
1391
1407
|
if (!text) {
|
|
1392
1408
|
throw new Error("text is null");
|
|
1393
1409
|
}
|
|
1410
|
+
const state = {
|
|
1411
|
+
selectors,
|
|
1412
|
+
_params,
|
|
1413
|
+
pattern,
|
|
1414
|
+
value: pattern,
|
|
1415
|
+
options,
|
|
1416
|
+
world,
|
|
1417
|
+
locate: false,
|
|
1418
|
+
scroll: false,
|
|
1419
|
+
screenshot: false,
|
|
1420
|
+
highlight: false,
|
|
1421
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1422
|
+
text: `Verify element contains pattern: ${pattern}`,
|
|
1423
|
+
operation: "containsPattern",
|
|
1424
|
+
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1425
|
+
};
|
|
1394
1426
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1395
1427
|
if (newValue !== text) {
|
|
1396
1428
|
this.logger.info(text + "=" + newValue);
|
|
1397
1429
|
text = newValue;
|
|
1398
1430
|
}
|
|
1399
|
-
const startTime = Date.now();
|
|
1400
|
-
let error = null;
|
|
1401
|
-
let screenshotId = null;
|
|
1402
|
-
let screenshotPath = null;
|
|
1403
|
-
const info = {};
|
|
1404
|
-
info.log =
|
|
1405
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1406
|
-
info.operation = "containsPattern";
|
|
1407
|
-
info.selectors = selectors;
|
|
1408
|
-
info.value = text;
|
|
1409
|
-
info.pattern = pattern;
|
|
1410
1431
|
let foundObj = null;
|
|
1411
1432
|
try {
|
|
1412
|
-
|
|
1433
|
+
await _preCommand(state, this);
|
|
1434
|
+
state.info.pattern = pattern;
|
|
1435
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1413
1436
|
if (foundObj && foundObj.element) {
|
|
1414
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1437
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1415
1438
|
}
|
|
1416
|
-
|
|
1439
|
+
await _screenshot(state, this);
|
|
1417
1440
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1418
1441
|
pattern = pattern.replace("{text}", escapedText);
|
|
1419
1442
|
let regex = new RegExp(pattern, "im");
|
|
1420
|
-
if (!regex.test(foundObj
|
|
1421
|
-
info.foundText = foundObj
|
|
1443
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1444
|
+
state.info.foundText = foundObj?.text;
|
|
1422
1445
|
throw new Error("element doesn't contain text " + text);
|
|
1423
1446
|
}
|
|
1424
|
-
return info;
|
|
1447
|
+
return state.info;
|
|
1425
1448
|
}
|
|
1426
1449
|
catch (e) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1430
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1431
|
-
info.screenshotPath = screenshotPath;
|
|
1432
|
-
Object.assign(e, { info: info });
|
|
1433
|
-
error = e;
|
|
1434
|
-
throw e;
|
|
1450
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1451
|
+
await _commandError(state, e, this);
|
|
1435
1452
|
}
|
|
1436
1453
|
finally {
|
|
1437
|
-
|
|
1438
|
-
this._reportToWorld(world, {
|
|
1439
|
-
element_name: selectors.element_name,
|
|
1440
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1441
|
-
value: pattern,
|
|
1442
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1443
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1444
|
-
result: error
|
|
1445
|
-
? {
|
|
1446
|
-
status: "FAILED",
|
|
1447
|
-
startTime,
|
|
1448
|
-
endTime,
|
|
1449
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1450
|
-
}
|
|
1451
|
-
: {
|
|
1452
|
-
status: "PASSED",
|
|
1453
|
-
startTime,
|
|
1454
|
-
endTime,
|
|
1455
|
-
},
|
|
1456
|
-
info: info,
|
|
1457
|
-
});
|
|
1454
|
+
_commandFinally(state, this);
|
|
1458
1455
|
}
|
|
1459
1456
|
}
|
|
1460
1457
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1458
|
+
const state = {
|
|
1459
|
+
selectors,
|
|
1460
|
+
_params,
|
|
1461
|
+
value: text,
|
|
1462
|
+
options,
|
|
1463
|
+
world,
|
|
1464
|
+
locate: false,
|
|
1465
|
+
scroll: false,
|
|
1466
|
+
screenshot: false,
|
|
1467
|
+
highlight: false,
|
|
1468
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1469
|
+
text: `Verify element contains text: ${text}`,
|
|
1470
|
+
operation: "containsText",
|
|
1471
|
+
log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
|
|
1472
|
+
};
|
|
1463
1473
|
if (!text) {
|
|
1464
1474
|
throw new Error("text is null");
|
|
1465
1475
|
}
|
|
1466
|
-
|
|
1467
|
-
let error = null;
|
|
1468
|
-
let screenshotId = null;
|
|
1469
|
-
let screenshotPath = null;
|
|
1470
|
-
const info = {};
|
|
1471
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1472
|
-
info.operation = "containsText";
|
|
1473
|
-
info.selectors = selectors;
|
|
1476
|
+
text = unEscapeString(text);
|
|
1474
1477
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1475
1478
|
if (newValue !== text) {
|
|
1476
1479
|
this.logger.info(text + "=" + newValue);
|
|
1477
1480
|
text = newValue;
|
|
1478
1481
|
}
|
|
1479
|
-
info.value = text;
|
|
1480
1482
|
let foundObj = null;
|
|
1481
1483
|
try {
|
|
1482
|
-
|
|
1484
|
+
await _preCommand(state, this);
|
|
1485
|
+
foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
|
|
1483
1486
|
if (foundObj && foundObj.element) {
|
|
1484
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1487
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1485
1488
|
}
|
|
1486
|
-
|
|
1489
|
+
await _screenshot(state, this);
|
|
1487
1490
|
const dateAlternatives = findDateAlternatives(text);
|
|
1488
1491
|
const numberAlternatives = findNumberAlternatives(text);
|
|
1489
1492
|
if (dateAlternatives.date) {
|
|
1490
1493
|
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1491
|
-
if (
|
|
1492
|
-
|
|
1493
|
-
return info;
|
|
1494
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1495
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1496
|
+
return state.info;
|
|
1494
1497
|
}
|
|
1495
1498
|
}
|
|
1496
1499
|
throw new Error("element doesn't contain text " + text);
|
|
1497
1500
|
}
|
|
1498
1501
|
else if (numberAlternatives.number) {
|
|
1499
1502
|
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1500
|
-
if (
|
|
1501
|
-
|
|
1502
|
-
return info;
|
|
1503
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1504
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1505
|
+
return state.info;
|
|
1503
1506
|
}
|
|
1504
1507
|
}
|
|
1505
1508
|
throw new Error("element doesn't contain text " + text);
|
|
1506
1509
|
}
|
|
1507
|
-
else if (!
|
|
1508
|
-
info.foundText = foundObj
|
|
1509
|
-
info.value = foundObj
|
|
1510
|
+
else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
|
|
1511
|
+
state.info.foundText = foundObj?.text;
|
|
1512
|
+
state.info.value = foundObj?.value;
|
|
1510
1513
|
throw new Error("element doesn't contain text " + text);
|
|
1511
1514
|
}
|
|
1512
|
-
return info;
|
|
1515
|
+
return state.info;
|
|
1513
1516
|
}
|
|
1514
1517
|
catch (e) {
|
|
1515
|
-
|
|
1516
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1517
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1518
|
-
info.screenshotPath = screenshotPath;
|
|
1519
|
-
Object.assign(e, { info: info });
|
|
1520
|
-
error = e;
|
|
1521
|
-
throw e;
|
|
1518
|
+
await _commandError(state, e, this);
|
|
1522
1519
|
}
|
|
1523
1520
|
finally {
|
|
1524
|
-
|
|
1525
|
-
this._reportToWorld(world, {
|
|
1526
|
-
element_name: selectors.element_name,
|
|
1527
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1528
|
-
text: `Verify element contains text: ${text}`,
|
|
1529
|
-
value: text,
|
|
1530
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1531
|
-
result: error
|
|
1532
|
-
? {
|
|
1533
|
-
status: "FAILED",
|
|
1534
|
-
startTime,
|
|
1535
|
-
endTime,
|
|
1536
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1537
|
-
}
|
|
1538
|
-
: {
|
|
1539
|
-
status: "PASSED",
|
|
1540
|
-
startTime,
|
|
1541
|
-
endTime,
|
|
1542
|
-
},
|
|
1543
|
-
info: info,
|
|
1544
|
-
});
|
|
1521
|
+
_commandFinally(state, this);
|
|
1545
1522
|
}
|
|
1546
1523
|
}
|
|
1547
1524
|
_getDataFile(world = null) {
|
|
@@ -1770,7 +1747,6 @@ class StableBrowser {
|
|
|
1770
1747
|
}
|
|
1771
1748
|
async takeScreenshot(screenshotPath) {
|
|
1772
1749
|
const playContext = this.context.playContext;
|
|
1773
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1774
1750
|
// Using CDP to capture the screenshot
|
|
1775
1751
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1776
1752
|
document.body.scrollWidth,
|
|
@@ -1780,97 +1756,67 @@ class StableBrowser {
|
|
|
1780
1756
|
document.body.clientWidth,
|
|
1781
1757
|
document.documentElement.clientWidth,
|
|
1782
1758
|
])));
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
await client.detach();
|
|
1759
|
+
let screenshotBuffer = null;
|
|
1760
|
+
if (this.context.browserName === "chromium") {
|
|
1761
|
+
const client = await playContext.newCDPSession(this.page);
|
|
1762
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
1763
|
+
format: "png",
|
|
1764
|
+
// clip: {
|
|
1765
|
+
// x: 0,
|
|
1766
|
+
// y: 0,
|
|
1767
|
+
// width: viewportWidth,
|
|
1768
|
+
// height: viewportHeight,
|
|
1769
|
+
// scale: 1,
|
|
1770
|
+
// },
|
|
1771
|
+
});
|
|
1772
|
+
await client.detach();
|
|
1773
|
+
if (!screenshotPath) {
|
|
1774
|
+
return data;
|
|
1775
|
+
}
|
|
1776
|
+
screenshotBuffer = Buffer.from(data, "base64");
|
|
1777
|
+
}
|
|
1778
|
+
else {
|
|
1779
|
+
screenshotBuffer = await this.page.screenshot();
|
|
1780
|
+
}
|
|
1781
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
1782
|
+
// Get the image dimensions
|
|
1783
|
+
const { width, height } = image.bitmap;
|
|
1784
|
+
const resizeRatio = viewportWidth / width;
|
|
1785
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
1786
|
+
if (width > viewportWidth) {
|
|
1787
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
1788
|
+
await image.write(screenshotPath);
|
|
1789
|
+
}
|
|
1790
|
+
else {
|
|
1791
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1792
|
+
}
|
|
1818
1793
|
}
|
|
1819
1794
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1795
|
+
const state = {
|
|
1796
|
+
selectors,
|
|
1797
|
+
_params,
|
|
1798
|
+
options,
|
|
1799
|
+
world,
|
|
1800
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1801
|
+
text: `Verify element exists in page`,
|
|
1802
|
+
operation: "verifyElementExistInPage",
|
|
1803
|
+
log: "***** verify element " + selectors.element_name + " exists in page *****\n",
|
|
1804
|
+
};
|
|
1825
1805
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1826
|
-
const info = {};
|
|
1827
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1828
|
-
info.operation = "verify";
|
|
1829
|
-
info.selectors = selectors;
|
|
1830
1806
|
try {
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
await this._highlightElements(element);
|
|
1836
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1837
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1838
|
-
return info;
|
|
1807
|
+
await _preCommand(state, this);
|
|
1808
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
1809
|
+
return state.info;
|
|
1839
1810
|
}
|
|
1840
1811
|
catch (e) {
|
|
1841
|
-
|
|
1842
|
-
this.logger.error("verify failed " + info.log);
|
|
1843
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1844
|
-
info.screenshotPath = screenshotPath;
|
|
1845
|
-
Object.assign(e, { info: info });
|
|
1846
|
-
error = e;
|
|
1847
|
-
throw e;
|
|
1812
|
+
await _commandError(state, e, this);
|
|
1848
1813
|
}
|
|
1849
1814
|
finally {
|
|
1850
|
-
|
|
1851
|
-
this._reportToWorld(world, {
|
|
1852
|
-
element_name: selectors.element_name,
|
|
1853
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1854
|
-
text: "Verify element exists in page",
|
|
1855
|
-
screenshotId,
|
|
1856
|
-
result: error
|
|
1857
|
-
? {
|
|
1858
|
-
status: "FAILED",
|
|
1859
|
-
startTime,
|
|
1860
|
-
endTime,
|
|
1861
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1862
|
-
}
|
|
1863
|
-
: {
|
|
1864
|
-
status: "PASSED",
|
|
1865
|
-
startTime,
|
|
1866
|
-
endTime,
|
|
1867
|
-
},
|
|
1868
|
-
info: info,
|
|
1869
|
-
});
|
|
1815
|
+
_commandFinally(state, this);
|
|
1870
1816
|
}
|
|
1871
1817
|
}
|
|
1872
1818
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1873
|
-
|
|
1819
|
+
_validateSelectors(selectors);
|
|
1874
1820
|
const startTime = Date.now();
|
|
1875
1821
|
let error = null;
|
|
1876
1822
|
let screenshotId = null;
|
|
@@ -1913,7 +1859,8 @@ class StableBrowser {
|
|
|
1913
1859
|
info.screenshotPath = screenshotPath;
|
|
1914
1860
|
Object.assign(e, { info: info });
|
|
1915
1861
|
error = e;
|
|
1916
|
-
throw e;
|
|
1862
|
+
// throw e;
|
|
1863
|
+
await _commandError({ text: "extractAttribute", operation: "extractAttribute", selectors, attribute, variable }, e, this);
|
|
1917
1864
|
}
|
|
1918
1865
|
finally {
|
|
1919
1866
|
const endTime = Date.now();
|
|
@@ -1929,7 +1876,7 @@ class StableBrowser {
|
|
|
1929
1876
|
status: "FAILED",
|
|
1930
1877
|
startTime,
|
|
1931
1878
|
endTime,
|
|
1932
|
-
message: error
|
|
1879
|
+
message: error?.message,
|
|
1933
1880
|
}
|
|
1934
1881
|
: {
|
|
1935
1882
|
status: "PASSED",
|
|
@@ -2014,7 +1961,8 @@ class StableBrowser {
|
|
|
2014
1961
|
catch (e) {
|
|
2015
1962
|
errorCount++;
|
|
2016
1963
|
if (errorCount > 3) {
|
|
2017
|
-
throw e;
|
|
1964
|
+
// throw e;
|
|
1965
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2018
1966
|
}
|
|
2019
1967
|
// ignore
|
|
2020
1968
|
}
|
|
@@ -2125,7 +2073,8 @@ class StableBrowser {
|
|
|
2125
2073
|
info.screenshotPath = screenshotPath;
|
|
2126
2074
|
Object.assign(e, { info: info });
|
|
2127
2075
|
error = e;
|
|
2128
|
-
throw e;
|
|
2076
|
+
// throw e;
|
|
2077
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2129
2078
|
}
|
|
2130
2079
|
finally {
|
|
2131
2080
|
const endTime = Date.now();
|
|
@@ -2138,7 +2087,7 @@ class StableBrowser {
|
|
|
2138
2087
|
status: "FAILED",
|
|
2139
2088
|
startTime,
|
|
2140
2089
|
endTime,
|
|
2141
|
-
message: error
|
|
2090
|
+
message: error?.message,
|
|
2142
2091
|
}
|
|
2143
2092
|
: {
|
|
2144
2093
|
status: "PASSED",
|
|
@@ -2174,20 +2123,20 @@ class StableBrowser {
|
|
|
2174
2123
|
for (let i = 0; i < frames.length; i++) {
|
|
2175
2124
|
if (dateAlternatives.date) {
|
|
2176
2125
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2177
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "
|
|
2126
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
|
|
2178
2127
|
result.frame = frames[i];
|
|
2179
2128
|
results.push(result);
|
|
2180
2129
|
}
|
|
2181
2130
|
}
|
|
2182
2131
|
else if (numberAlternatives.number) {
|
|
2183
2132
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2184
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "
|
|
2133
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
|
|
2185
2134
|
result.frame = frames[i];
|
|
2186
2135
|
results.push(result);
|
|
2187
2136
|
}
|
|
2188
2137
|
}
|
|
2189
2138
|
else {
|
|
2190
|
-
const result = await this._locateElementByText(frames[i], text, "
|
|
2139
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
|
|
2191
2140
|
result.frame = frames[i];
|
|
2192
2141
|
results.push(result);
|
|
2193
2142
|
}
|
|
@@ -2223,7 +2172,8 @@ class StableBrowser {
|
|
|
2223
2172
|
info.screenshotPath = screenshotPath;
|
|
2224
2173
|
Object.assign(e, { info: info });
|
|
2225
2174
|
error = e;
|
|
2226
|
-
throw e;
|
|
2175
|
+
// throw e;
|
|
2176
|
+
await _commandError({ text: "verifyTextExistInPage", operation: "verifyTextExistInPage", text, info }, e, this);
|
|
2227
2177
|
}
|
|
2228
2178
|
finally {
|
|
2229
2179
|
const endTime = Date.now();
|
|
@@ -2236,7 +2186,7 @@ class StableBrowser {
|
|
|
2236
2186
|
status: "FAILED",
|
|
2237
2187
|
startTime,
|
|
2238
2188
|
endTime,
|
|
2239
|
-
message: error
|
|
2189
|
+
message: error?.message,
|
|
2240
2190
|
}
|
|
2241
2191
|
: {
|
|
2242
2192
|
status: "PASSED",
|
|
@@ -2307,7 +2257,8 @@ class StableBrowser {
|
|
|
2307
2257
|
info.screenshotPath = screenshotPath;
|
|
2308
2258
|
Object.assign(e, { info: info });
|
|
2309
2259
|
error = e;
|
|
2310
|
-
throw e;
|
|
2260
|
+
// throw e;
|
|
2261
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2311
2262
|
}
|
|
2312
2263
|
finally {
|
|
2313
2264
|
const endTime = Date.now();
|
|
@@ -2320,7 +2271,7 @@ class StableBrowser {
|
|
|
2320
2271
|
status: "FAILED",
|
|
2321
2272
|
startTime,
|
|
2322
2273
|
endTime,
|
|
2323
|
-
message: error
|
|
2274
|
+
message: error?.message,
|
|
2324
2275
|
}
|
|
2325
2276
|
: {
|
|
2326
2277
|
status: "PASSED",
|
|
@@ -2352,7 +2303,7 @@ class StableBrowser {
|
|
|
2352
2303
|
this.logger.info("Table data verified");
|
|
2353
2304
|
}
|
|
2354
2305
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2355
|
-
|
|
2306
|
+
_validateSelectors(selectors);
|
|
2356
2307
|
const startTime = Date.now();
|
|
2357
2308
|
let error = null;
|
|
2358
2309
|
let screenshotId = null;
|
|
@@ -2374,7 +2325,8 @@ class StableBrowser {
|
|
|
2374
2325
|
info.screenshotPath = screenshotPath;
|
|
2375
2326
|
Object.assign(e, { info: info });
|
|
2376
2327
|
error = e;
|
|
2377
|
-
throw e;
|
|
2328
|
+
// throw e;
|
|
2329
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2378
2330
|
}
|
|
2379
2331
|
finally {
|
|
2380
2332
|
const endTime = Date.now();
|
|
@@ -2388,7 +2340,7 @@ class StableBrowser {
|
|
|
2388
2340
|
status: "FAILED",
|
|
2389
2341
|
startTime,
|
|
2390
2342
|
endTime,
|
|
2391
|
-
message: error
|
|
2343
|
+
message: error?.message,
|
|
2392
2344
|
}
|
|
2393
2345
|
: {
|
|
2394
2346
|
status: "PASSED",
|
|
@@ -2400,7 +2352,7 @@ class StableBrowser {
|
|
|
2400
2352
|
}
|
|
2401
2353
|
}
|
|
2402
2354
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2403
|
-
|
|
2355
|
+
_validateSelectors(selectors);
|
|
2404
2356
|
if (!query) {
|
|
2405
2357
|
throw new Error("query is null");
|
|
2406
2358
|
}
|
|
@@ -2539,7 +2491,8 @@ class StableBrowser {
|
|
|
2539
2491
|
info.screenshotPath = screenshotPath;
|
|
2540
2492
|
Object.assign(e, { info: info });
|
|
2541
2493
|
error = e;
|
|
2542
|
-
throw e;
|
|
2494
|
+
// throw e;
|
|
2495
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2543
2496
|
}
|
|
2544
2497
|
finally {
|
|
2545
2498
|
const endTime = Date.now();
|
|
@@ -2553,7 +2506,7 @@ class StableBrowser {
|
|
|
2553
2506
|
status: "FAILED",
|
|
2554
2507
|
startTime,
|
|
2555
2508
|
endTime,
|
|
2556
|
-
message: error
|
|
2509
|
+
message: error?.message,
|
|
2557
2510
|
}
|
|
2558
2511
|
: {
|
|
2559
2512
|
status: "PASSED",
|
|
@@ -2565,27 +2518,7 @@ class StableBrowser {
|
|
|
2565
2518
|
}
|
|
2566
2519
|
}
|
|
2567
2520
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2568
|
-
|
|
2569
|
-
return value;
|
|
2570
|
-
}
|
|
2571
|
-
// find all the accurance of {{(.*?)}} and replace with the value
|
|
2572
|
-
let regex = /{{(.*?)}}/g;
|
|
2573
|
-
let matches = value.match(regex);
|
|
2574
|
-
if (matches) {
|
|
2575
|
-
const testData = this.getTestData(world);
|
|
2576
|
-
for (let i = 0; i < matches.length; i++) {
|
|
2577
|
-
let match = matches[i];
|
|
2578
|
-
let key = match.substring(2, match.length - 2);
|
|
2579
|
-
let newValue = objectPath.get(testData, key, null);
|
|
2580
|
-
if (newValue !== null) {
|
|
2581
|
-
value = value.replace(match, newValue);
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
|
|
2586
|
-
return await decrypt(value, null, totpWait);
|
|
2587
|
-
}
|
|
2588
|
-
return value;
|
|
2521
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2589
2522
|
}
|
|
2590
2523
|
_getLoadTimeout(options) {
|
|
2591
2524
|
let timeout = 15000;
|
|
@@ -2645,7 +2578,7 @@ class StableBrowser {
|
|
|
2645
2578
|
status: "FAILED",
|
|
2646
2579
|
startTime,
|
|
2647
2580
|
endTime,
|
|
2648
|
-
message: error
|
|
2581
|
+
message: error?.message,
|
|
2649
2582
|
}
|
|
2650
2583
|
: {
|
|
2651
2584
|
status: "PASSED",
|
|
@@ -2666,6 +2599,7 @@ class StableBrowser {
|
|
|
2666
2599
|
}
|
|
2667
2600
|
catch (e) {
|
|
2668
2601
|
console.log(".");
|
|
2602
|
+
await _commandError({ text: "closePage", operation: "closePage", info }, e, this);
|
|
2669
2603
|
}
|
|
2670
2604
|
finally {
|
|
2671
2605
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -2680,7 +2614,7 @@ class StableBrowser {
|
|
|
2680
2614
|
status: "FAILED",
|
|
2681
2615
|
startTime,
|
|
2682
2616
|
endTime,
|
|
2683
|
-
message: error
|
|
2617
|
+
message: error?.message,
|
|
2684
2618
|
}
|
|
2685
2619
|
: {
|
|
2686
2620
|
status: "PASSED",
|
|
@@ -2708,6 +2642,7 @@ class StableBrowser {
|
|
|
2708
2642
|
}
|
|
2709
2643
|
catch (e) {
|
|
2710
2644
|
console.log(".");
|
|
2645
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2711
2646
|
}
|
|
2712
2647
|
finally {
|
|
2713
2648
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -2722,7 +2657,7 @@ class StableBrowser {
|
|
|
2722
2657
|
status: "FAILED",
|
|
2723
2658
|
startTime,
|
|
2724
2659
|
endTime,
|
|
2725
|
-
message: error
|
|
2660
|
+
message: error?.message,
|
|
2726
2661
|
}
|
|
2727
2662
|
: {
|
|
2728
2663
|
status: "PASSED",
|
|
@@ -2744,6 +2679,7 @@ class StableBrowser {
|
|
|
2744
2679
|
}
|
|
2745
2680
|
catch (e) {
|
|
2746
2681
|
console.log(".");
|
|
2682
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2747
2683
|
}
|
|
2748
2684
|
finally {
|
|
2749
2685
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -2758,7 +2694,7 @@ class StableBrowser {
|
|
|
2758
2694
|
status: "FAILED",
|
|
2759
2695
|
startTime,
|
|
2760
2696
|
endTime,
|
|
2761
|
-
message: error
|
|
2697
|
+
message: error?.message,
|
|
2762
2698
|
}
|
|
2763
2699
|
: {
|
|
2764
2700
|
status: "PASSED",
|
|
@@ -2943,5 +2879,10 @@ const KEYBOARD_EVENTS = [
|
|
|
2943
2879
|
"TVAntennaCable",
|
|
2944
2880
|
"TVAudioDescription",
|
|
2945
2881
|
];
|
|
2882
|
+
function unEscapeString(str) {
|
|
2883
|
+
const placeholder = "__NEWLINE__";
|
|
2884
|
+
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2885
|
+
return str;
|
|
2886
|
+
}
|
|
2946
2887
|
export { StableBrowser };
|
|
2947
2888
|
//# sourceMappingURL=stable_browser.js.map
|