automation_model 1.0.445-dev → 1.0.445-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 +183 -7
- 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 +182 -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 +581 -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 +6 -6
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 = {};
|
|
@@ -484,7 +592,10 @@ class StableBrowser {
|
|
|
484
592
|
}
|
|
485
593
|
return { rerun: false };
|
|
486
594
|
}
|
|
487
|
-
async _locate(selectors, info, _params, timeout
|
|
595
|
+
async _locate(selectors, info, _params, timeout) {
|
|
596
|
+
if (!timeout) {
|
|
597
|
+
timeout = 30000;
|
|
598
|
+
}
|
|
488
599
|
for (let i = 0; i < 3; i++) {
|
|
489
600
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
490
601
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
@@ -498,32 +609,46 @@ class StableBrowser {
|
|
|
498
609
|
}
|
|
499
610
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
500
611
|
}
|
|
501
|
-
async
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
612
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
613
|
+
if (!info) {
|
|
614
|
+
info = {};
|
|
615
|
+
info.failCause = {};
|
|
616
|
+
info.log = "";
|
|
617
|
+
}
|
|
507
618
|
let scope = this.page;
|
|
619
|
+
if (selectors.frame) {
|
|
620
|
+
return selectors.frame;
|
|
621
|
+
}
|
|
508
622
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
509
|
-
const findFrame = (frame, framescope) => {
|
|
623
|
+
const findFrame = async (frame, framescope) => {
|
|
510
624
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
511
625
|
let frameLocator = frame.selectors[i];
|
|
512
626
|
if (frameLocator.css) {
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
641
|
}
|
|
516
642
|
}
|
|
517
643
|
if (frame.children) {
|
|
518
|
-
return findFrame(frame.children, framescope);
|
|
644
|
+
return await findFrame(frame.children, framescope);
|
|
519
645
|
}
|
|
520
646
|
return framescope;
|
|
521
647
|
};
|
|
522
|
-
info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
|
|
523
648
|
while (true) {
|
|
524
649
|
let frameFound = false;
|
|
525
650
|
if (selectors.nestFrmLoc) {
|
|
526
|
-
scope = findFrame(selectors.nestFrmLoc, scope);
|
|
651
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
527
652
|
frameFound = true;
|
|
528
653
|
break;
|
|
529
654
|
}
|
|
@@ -543,6 +668,8 @@ class StableBrowser {
|
|
|
543
668
|
if (!scope) {
|
|
544
669
|
info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
545
670
|
if (performance.now() - startTime > timeout) {
|
|
671
|
+
info.failCause.iframeNotFound = true;
|
|
672
|
+
info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
|
|
546
673
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
547
674
|
}
|
|
548
675
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -552,6 +679,30 @@ class StableBrowser {
|
|
|
552
679
|
}
|
|
553
680
|
}
|
|
554
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);
|
|
555
706
|
let selectorsLocators = null;
|
|
556
707
|
selectorsLocators = selectors.locators;
|
|
557
708
|
// group selectors by priority
|
|
@@ -653,6 +804,8 @@ class StableBrowser {
|
|
|
653
804
|
}
|
|
654
805
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
655
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";
|
|
656
809
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
657
810
|
}
|
|
658
811
|
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
@@ -684,89 +837,129 @@ class StableBrowser {
|
|
|
684
837
|
});
|
|
685
838
|
result.locatorIndex = i;
|
|
686
839
|
}
|
|
840
|
+
if (foundLocators.length > 1) {
|
|
841
|
+
info.failCause.foundMultiple = true;
|
|
842
|
+
}
|
|
687
843
|
}
|
|
688
844
|
return result;
|
|
689
845
|
}
|
|
690
|
-
async
|
|
691
|
-
this._validateSelectors(selectors);
|
|
846
|
+
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
692
847
|
const startTime = Date.now();
|
|
693
|
-
|
|
694
|
-
|
|
848
|
+
let timeout = 30000;
|
|
849
|
+
if (options && options.timeout) {
|
|
850
|
+
timeout = options.timeout;
|
|
695
851
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
+
};
|
|
703
919
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
920
|
+
await _preCommand(state, this);
|
|
921
|
+
if (state.options && state.options.context) {
|
|
922
|
+
state.selectors.locators[0].text = state.options.context;
|
|
923
|
+
}
|
|
707
924
|
try {
|
|
708
|
-
await
|
|
709
|
-
await element.click();
|
|
925
|
+
await state.element.click();
|
|
710
926
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
711
927
|
}
|
|
712
928
|
catch (e) {
|
|
713
929
|
// await this.closeUnexpectedPopups();
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
await element.dispatchEvent("click");
|
|
930
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
931
|
+
await state.element.dispatchEvent("click");
|
|
717
932
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
718
933
|
}
|
|
719
934
|
await this.waitForPageLoad();
|
|
720
|
-
return info;
|
|
935
|
+
return state.info;
|
|
721
936
|
}
|
|
722
937
|
catch (e) {
|
|
723
|
-
|
|
724
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
725
|
-
info.screenshotPath = screenshotPath;
|
|
726
|
-
Object.assign(e, { info: info });
|
|
727
|
-
error = e;
|
|
728
|
-
throw e;
|
|
938
|
+
await _commandError(state, e, this);
|
|
729
939
|
}
|
|
730
940
|
finally {
|
|
731
|
-
|
|
732
|
-
this._reportToWorld(world, {
|
|
733
|
-
element_name: selectors.element_name,
|
|
734
|
-
type: Types.CLICK,
|
|
735
|
-
text: `Click element`,
|
|
736
|
-
screenshotId,
|
|
737
|
-
result: error
|
|
738
|
-
? {
|
|
739
|
-
status: "FAILED",
|
|
740
|
-
startTime,
|
|
741
|
-
endTime,
|
|
742
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
743
|
-
}
|
|
744
|
-
: {
|
|
745
|
-
status: "PASSED",
|
|
746
|
-
startTime,
|
|
747
|
-
endTime,
|
|
748
|
-
},
|
|
749
|
-
info: info,
|
|
750
|
-
});
|
|
941
|
+
_commandFinally(state, this);
|
|
751
942
|
}
|
|
752
943
|
}
|
|
753
944
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
+
};
|
|
764
955
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
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));
|
|
767
960
|
try {
|
|
768
|
-
await this._highlightElements(element);
|
|
769
|
-
await element.setChecked(checked);
|
|
961
|
+
// await this._highlightElements(element);
|
|
962
|
+
await state.element.setChecked(checked);
|
|
770
963
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
771
964
|
}
|
|
772
965
|
catch (e) {
|
|
@@ -775,179 +968,108 @@ class StableBrowser {
|
|
|
775
968
|
}
|
|
776
969
|
else {
|
|
777
970
|
//await this.closeUnexpectedPopups();
|
|
778
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
779
|
-
element = await this._locate(selectors, info, _params);
|
|
780
|
-
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 });
|
|
781
974
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
782
975
|
}
|
|
783
976
|
}
|
|
784
977
|
await this.waitForPageLoad();
|
|
785
|
-
return info;
|
|
978
|
+
return state.info;
|
|
786
979
|
}
|
|
787
980
|
catch (e) {
|
|
788
|
-
|
|
789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
790
|
-
info.screenshotPath = screenshotPath;
|
|
791
|
-
Object.assign(e, { info: info });
|
|
792
|
-
error = e;
|
|
793
|
-
throw e;
|
|
981
|
+
await _commandError(state, e, this);
|
|
794
982
|
}
|
|
795
983
|
finally {
|
|
796
|
-
|
|
797
|
-
this._reportToWorld(world, {
|
|
798
|
-
element_name: selectors.element_name,
|
|
799
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
800
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
801
|
-
screenshotId,
|
|
802
|
-
result: error
|
|
803
|
-
? {
|
|
804
|
-
status: "FAILED",
|
|
805
|
-
startTime,
|
|
806
|
-
endTime,
|
|
807
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
808
|
-
}
|
|
809
|
-
: {
|
|
810
|
-
status: "PASSED",
|
|
811
|
-
startTime,
|
|
812
|
-
endTime,
|
|
813
|
-
},
|
|
814
|
-
info: info,
|
|
815
|
-
});
|
|
984
|
+
_commandFinally(state, this);
|
|
816
985
|
}
|
|
817
986
|
}
|
|
818
987
|
async hover(selectors, _params, options = {}, world = null) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
+
};
|
|
828
998
|
try {
|
|
829
|
-
|
|
830
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
999
|
+
await _preCommand(state, this);
|
|
831
1000
|
try {
|
|
832
|
-
await
|
|
833
|
-
await element.hover();
|
|
1001
|
+
await state.element.hover();
|
|
834
1002
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
835
1003
|
}
|
|
836
1004
|
catch (e) {
|
|
837
1005
|
//await this.closeUnexpectedPopups();
|
|
838
|
-
info.log += "hover failed, will try again" + "\n";
|
|
839
|
-
element = await this._locate(selectors, info, _params);
|
|
840
|
-
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 });
|
|
841
1009
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
842
1010
|
}
|
|
843
1011
|
await this.waitForPageLoad();
|
|
844
|
-
return info;
|
|
1012
|
+
return state.info;
|
|
845
1013
|
}
|
|
846
1014
|
catch (e) {
|
|
847
|
-
|
|
848
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
849
|
-
info.screenshotPath = screenshotPath;
|
|
850
|
-
Object.assign(e, { info: info });
|
|
851
|
-
error = e;
|
|
852
|
-
throw e;
|
|
1015
|
+
await _commandError(state, e, this);
|
|
853
1016
|
}
|
|
854
1017
|
finally {
|
|
855
|
-
|
|
856
|
-
this._reportToWorld(world, {
|
|
857
|
-
element_name: selectors.element_name,
|
|
858
|
-
type: Types.HOVER,
|
|
859
|
-
text: `Hover element`,
|
|
860
|
-
screenshotId,
|
|
861
|
-
result: error
|
|
862
|
-
? {
|
|
863
|
-
status: "FAILED",
|
|
864
|
-
startTime,
|
|
865
|
-
endTime,
|
|
866
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
867
|
-
}
|
|
868
|
-
: {
|
|
869
|
-
status: "PASSED",
|
|
870
|
-
startTime,
|
|
871
|
-
endTime,
|
|
872
|
-
},
|
|
873
|
-
info: info,
|
|
874
|
-
});
|
|
1018
|
+
_commandFinally(state, this);
|
|
875
1019
|
}
|
|
876
1020
|
}
|
|
877
1021
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
878
|
-
this._validateSelectors(selectors);
|
|
879
1022
|
if (!values) {
|
|
880
1023
|
throw new Error("values is null");
|
|
881
1024
|
}
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
};
|
|
890
1036
|
try {
|
|
891
|
-
|
|
892
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1037
|
+
await _preCommand(state, this);
|
|
893
1038
|
try {
|
|
894
|
-
await
|
|
895
|
-
await element.selectOption(values);
|
|
1039
|
+
await state.element.selectOption(values);
|
|
896
1040
|
}
|
|
897
1041
|
catch (e) {
|
|
898
1042
|
//await this.closeUnexpectedPopups();
|
|
899
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
900
|
-
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 });
|
|
901
1045
|
}
|
|
902
1046
|
await this.waitForPageLoad();
|
|
903
|
-
return info;
|
|
1047
|
+
return state.info;
|
|
904
1048
|
}
|
|
905
1049
|
catch (e) {
|
|
906
|
-
|
|
907
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
908
|
-
info.screenshotPath = screenshotPath;
|
|
909
|
-
Object.assign(e, { info: info });
|
|
910
|
-
this.logger.info("click failed, will try next selector");
|
|
911
|
-
error = e;
|
|
912
|
-
throw e;
|
|
1050
|
+
await _commandError(state, e, this);
|
|
913
1051
|
}
|
|
914
1052
|
finally {
|
|
915
|
-
|
|
916
|
-
this._reportToWorld(world, {
|
|
917
|
-
element_name: selectors.element_name,
|
|
918
|
-
type: Types.SELECT,
|
|
919
|
-
text: `Select option: ${values}`,
|
|
920
|
-
value: values.toString(),
|
|
921
|
-
screenshotId,
|
|
922
|
-
result: error
|
|
923
|
-
? {
|
|
924
|
-
status: "FAILED",
|
|
925
|
-
startTime,
|
|
926
|
-
endTime,
|
|
927
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
928
|
-
}
|
|
929
|
-
: {
|
|
930
|
-
status: "PASSED",
|
|
931
|
-
startTime,
|
|
932
|
-
endTime,
|
|
933
|
-
},
|
|
934
|
-
info: info,
|
|
935
|
-
});
|
|
1053
|
+
_commandFinally(state, this);
|
|
936
1054
|
}
|
|
937
1055
|
}
|
|
938
1056
|
async type(_value, _params = null, options = {}, world = null) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
};
|
|
948
1070
|
try {
|
|
949
|
-
|
|
950
|
-
const valueSegment =
|
|
1071
|
+
await _preCommand(state, this);
|
|
1072
|
+
const valueSegment = state.value.split("&&");
|
|
951
1073
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
952
1074
|
if (i > 0) {
|
|
953
1075
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -967,108 +1089,53 @@ class StableBrowser {
|
|
|
967
1089
|
await this.page.keyboard.type(value);
|
|
968
1090
|
}
|
|
969
1091
|
}
|
|
970
|
-
return info;
|
|
1092
|
+
return state.info;
|
|
971
1093
|
}
|
|
972
1094
|
catch (e) {
|
|
973
|
-
|
|
974
|
-
this.logger.error("type failed " + info.log);
|
|
975
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
976
|
-
info.screenshotPath = screenshotPath;
|
|
977
|
-
Object.assign(e, { info: info });
|
|
978
|
-
error = e;
|
|
979
|
-
throw e;
|
|
1095
|
+
await _commandError(state, e, this);
|
|
980
1096
|
}
|
|
981
1097
|
finally {
|
|
982
|
-
|
|
983
|
-
this._reportToWorld(world, {
|
|
984
|
-
type: Types.TYPE_PRESS,
|
|
985
|
-
screenshotId,
|
|
986
|
-
value: _value,
|
|
987
|
-
text: `type value: ${_value}`,
|
|
988
|
-
result: error
|
|
989
|
-
? {
|
|
990
|
-
status: "FAILED",
|
|
991
|
-
startTime,
|
|
992
|
-
endTime,
|
|
993
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
994
|
-
}
|
|
995
|
-
: {
|
|
996
|
-
status: "PASSED",
|
|
997
|
-
startTime,
|
|
998
|
-
endTime,
|
|
999
|
-
},
|
|
1000
|
-
info: info,
|
|
1001
|
-
});
|
|
1098
|
+
_commandFinally(state, this);
|
|
1002
1099
|
}
|
|
1003
1100
|
}
|
|
1004
1101
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
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
|
+
};
|
|
1017
1113
|
try {
|
|
1018
|
-
|
|
1019
|
-
let
|
|
1020
|
-
await this.scrollIfNeeded(element, info);
|
|
1021
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1022
|
-
await this._highlightElements(element);
|
|
1114
|
+
await _preCommand(state, this);
|
|
1115
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1023
1116
|
try {
|
|
1024
|
-
await element.evaluateHandle((el, value) => {
|
|
1117
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1025
1118
|
el.value = value;
|
|
1026
1119
|
}, value);
|
|
1027
1120
|
}
|
|
1028
1121
|
catch (error) {
|
|
1029
1122
|
this.logger.error("setInputValue failed, will try again");
|
|
1030
|
-
|
|
1031
|
-
info.
|
|
1032
|
-
|
|
1033
|
-
await element.evaluateHandle((el, value) => {
|
|
1123
|
+
await _screenshot(state, this);
|
|
1124
|
+
Object.assign(error, { info: state.info });
|
|
1125
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1034
1126
|
el.value = value;
|
|
1035
1127
|
});
|
|
1036
1128
|
}
|
|
1037
1129
|
}
|
|
1038
1130
|
catch (e) {
|
|
1039
|
-
|
|
1040
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1041
|
-
info.screenshotPath = screenshotPath;
|
|
1042
|
-
Object.assign(e, { info: info });
|
|
1043
|
-
error = e;
|
|
1044
|
-
throw e;
|
|
1131
|
+
await _commandError(state, e, this);
|
|
1045
1132
|
}
|
|
1046
1133
|
finally {
|
|
1047
|
-
|
|
1048
|
-
this._reportToWorld(world, {
|
|
1049
|
-
element_name: selectors.element_name,
|
|
1050
|
-
type: Types.SET_INPUT,
|
|
1051
|
-
text: `Set input value`,
|
|
1052
|
-
value: value,
|
|
1053
|
-
screenshotId,
|
|
1054
|
-
result: error
|
|
1055
|
-
? {
|
|
1056
|
-
status: "FAILED",
|
|
1057
|
-
startTime,
|
|
1058
|
-
endTime,
|
|
1059
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1060
|
-
}
|
|
1061
|
-
: {
|
|
1062
|
-
status: "PASSED",
|
|
1063
|
-
startTime,
|
|
1064
|
-
endTime,
|
|
1065
|
-
},
|
|
1066
|
-
info: info,
|
|
1067
|
-
});
|
|
1134
|
+
_commandFinally(state, this);
|
|
1068
1135
|
}
|
|
1069
1136
|
}
|
|
1070
1137
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1071
|
-
|
|
1138
|
+
_validateSelectors(selectors);
|
|
1072
1139
|
const startTime = Date.now();
|
|
1073
1140
|
let error = null;
|
|
1074
1141
|
let screenshotId = null;
|
|
@@ -1134,7 +1201,8 @@ class StableBrowser {
|
|
|
1134
1201
|
}
|
|
1135
1202
|
catch (e) {
|
|
1136
1203
|
error = e;
|
|
1137
|
-
throw e;
|
|
1204
|
+
// throw e;
|
|
1205
|
+
await _commandError({ text: "setDateTime", operation: "setDateTime", selectors, value, info }, e, this);
|
|
1138
1206
|
}
|
|
1139
1207
|
finally {
|
|
1140
1208
|
const endTime = Date.now();
|
|
@@ -1161,32 +1229,32 @@ class StableBrowser {
|
|
|
1161
1229
|
}
|
|
1162
1230
|
}
|
|
1163
1231
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1164
|
-
|
|
1165
|
-
const startTime = Date.now();
|
|
1166
|
-
let error = null;
|
|
1167
|
-
let screenshotId = null;
|
|
1168
|
-
let screenshotPath = null;
|
|
1169
|
-
const info = {};
|
|
1170
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1171
|
-
info.operation = "clickType";
|
|
1172
|
-
info.selectors = selectors;
|
|
1232
|
+
_value = unEscapeString(_value);
|
|
1173
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
|
+
};
|
|
1174
1246
|
if (newValue !== _value) {
|
|
1175
1247
|
//this.logger.info(_value + "=" + newValue);
|
|
1176
1248
|
_value = newValue;
|
|
1177
1249
|
}
|
|
1178
|
-
info.value = _value;
|
|
1179
1250
|
try {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
await this.scrollIfNeeded(element, info);
|
|
1183
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1184
|
-
await this._highlightElements(element);
|
|
1251
|
+
await _preCommand(state, this);
|
|
1252
|
+
state.info.value = _value;
|
|
1185
1253
|
if (options === null || options === undefined || !options.press) {
|
|
1186
1254
|
try {
|
|
1187
|
-
let currentValue = await element.inputValue();
|
|
1255
|
+
let currentValue = await state.element.inputValue();
|
|
1188
1256
|
if (currentValue) {
|
|
1189
|
-
await element.fill("");
|
|
1257
|
+
await state.element.fill("");
|
|
1190
1258
|
}
|
|
1191
1259
|
}
|
|
1192
1260
|
catch (e) {
|
|
@@ -1195,22 +1263,22 @@ class StableBrowser {
|
|
|
1195
1263
|
}
|
|
1196
1264
|
if (options === null || options === undefined || options.press) {
|
|
1197
1265
|
try {
|
|
1198
|
-
await element.click({ timeout: 5000 });
|
|
1266
|
+
await state.element.click({ timeout: 5000 });
|
|
1199
1267
|
}
|
|
1200
1268
|
catch (e) {
|
|
1201
|
-
await element.dispatchEvent("click");
|
|
1269
|
+
await state.element.dispatchEvent("click");
|
|
1202
1270
|
}
|
|
1203
1271
|
}
|
|
1204
1272
|
else {
|
|
1205
1273
|
try {
|
|
1206
|
-
await element.focus();
|
|
1274
|
+
await state.element.focus();
|
|
1207
1275
|
}
|
|
1208
1276
|
catch (e) {
|
|
1209
|
-
await element.dispatchEvent("focus");
|
|
1277
|
+
await state.element.dispatchEvent("focus");
|
|
1210
1278
|
}
|
|
1211
1279
|
}
|
|
1212
1280
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1213
|
-
const valueSegment =
|
|
1281
|
+
const valueSegment = state.value.split("&&");
|
|
1214
1282
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1215
1283
|
if (i > 0) {
|
|
1216
1284
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1230,13 +1298,14 @@ class StableBrowser {
|
|
|
1230
1298
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1231
1299
|
}
|
|
1232
1300
|
}
|
|
1301
|
+
await _screenshot(state, this);
|
|
1233
1302
|
if (enter === true) {
|
|
1234
1303
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1235
1304
|
await this.page.keyboard.press("Enter");
|
|
1236
1305
|
await this.waitForPageLoad();
|
|
1237
1306
|
}
|
|
1238
1307
|
else if (enter === false) {
|
|
1239
|
-
await element.dispatchEvent("change");
|
|
1308
|
+
await state.element.dispatchEvent("change");
|
|
1240
1309
|
//await this.page.keyboard.press("Tab");
|
|
1241
1310
|
}
|
|
1242
1311
|
else {
|
|
@@ -1245,103 +1314,50 @@ class StableBrowser {
|
|
|
1245
1314
|
await this.waitForPageLoad();
|
|
1246
1315
|
}
|
|
1247
1316
|
}
|
|
1248
|
-
return info;
|
|
1317
|
+
return state.info;
|
|
1249
1318
|
}
|
|
1250
1319
|
catch (e) {
|
|
1251
|
-
|
|
1252
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1253
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1254
|
-
info.screenshotPath = screenshotPath;
|
|
1255
|
-
Object.assign(e, { info: info });
|
|
1256
|
-
error = e;
|
|
1257
|
-
throw e;
|
|
1320
|
+
await _commandError(state, e, this);
|
|
1258
1321
|
}
|
|
1259
1322
|
finally {
|
|
1260
|
-
|
|
1261
|
-
this._reportToWorld(world, {
|
|
1262
|
-
element_name: selectors.element_name,
|
|
1263
|
-
type: Types.FILL,
|
|
1264
|
-
screenshotId,
|
|
1265
|
-
value: _value,
|
|
1266
|
-
text: `clickType input with value: ${_value}`,
|
|
1267
|
-
result: error
|
|
1268
|
-
? {
|
|
1269
|
-
status: "FAILED",
|
|
1270
|
-
startTime,
|
|
1271
|
-
endTime,
|
|
1272
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1273
|
-
}
|
|
1274
|
-
: {
|
|
1275
|
-
status: "PASSED",
|
|
1276
|
-
startTime,
|
|
1277
|
-
endTime,
|
|
1278
|
-
},
|
|
1279
|
-
info: info,
|
|
1280
|
-
});
|
|
1323
|
+
_commandFinally(state, this);
|
|
1281
1324
|
}
|
|
1282
1325
|
}
|
|
1283
1326
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
+
};
|
|
1294
1338
|
try {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
await
|
|
1298
|
-
await element.fill(value);
|
|
1299
|
-
await element.dispatchEvent("change");
|
|
1339
|
+
await _preCommand(state, this);
|
|
1340
|
+
await state.element.fill(value);
|
|
1341
|
+
await state.element.dispatchEvent("change");
|
|
1300
1342
|
if (enter) {
|
|
1301
1343
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1302
1344
|
await this.page.keyboard.press("Enter");
|
|
1303
1345
|
}
|
|
1304
1346
|
await this.waitForPageLoad();
|
|
1305
|
-
return info;
|
|
1347
|
+
return state.info;
|
|
1306
1348
|
}
|
|
1307
1349
|
catch (e) {
|
|
1308
|
-
|
|
1309
|
-
this.logger.error("fill failed " + info.log);
|
|
1310
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1311
|
-
info.screenshotPath = screenshotPath;
|
|
1312
|
-
Object.assign(e, { info: info });
|
|
1313
|
-
error = e;
|
|
1314
|
-
throw e;
|
|
1350
|
+
await _commandError(state, e, this);
|
|
1315
1351
|
}
|
|
1316
1352
|
finally {
|
|
1317
|
-
|
|
1318
|
-
this._reportToWorld(world, {
|
|
1319
|
-
element_name: selectors.element_name,
|
|
1320
|
-
type: Types.FILL,
|
|
1321
|
-
screenshotId,
|
|
1322
|
-
value,
|
|
1323
|
-
text: `Fill input with value: ${value}`,
|
|
1324
|
-
result: error
|
|
1325
|
-
? {
|
|
1326
|
-
status: "FAILED",
|
|
1327
|
-
startTime,
|
|
1328
|
-
endTime,
|
|
1329
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1330
|
-
}
|
|
1331
|
-
: {
|
|
1332
|
-
status: "PASSED",
|
|
1333
|
-
startTime,
|
|
1334
|
-
endTime,
|
|
1335
|
-
},
|
|
1336
|
-
info: info,
|
|
1337
|
-
});
|
|
1353
|
+
_commandFinally(state, this);
|
|
1338
1354
|
}
|
|
1339
1355
|
}
|
|
1340
1356
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1341
1357
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1342
1358
|
}
|
|
1343
1359
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1344
|
-
|
|
1360
|
+
_validateSelectors(selectors);
|
|
1345
1361
|
let screenshotId = null;
|
|
1346
1362
|
let screenshotPath = null;
|
|
1347
1363
|
if (!info.log) {
|
|
@@ -1385,165 +1401,124 @@ class StableBrowser {
|
|
|
1385
1401
|
}
|
|
1386
1402
|
}
|
|
1387
1403
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1388
|
-
var _a;
|
|
1389
|
-
this._validateSelectors(selectors);
|
|
1390
1404
|
if (!pattern) {
|
|
1391
1405
|
throw new Error("pattern is null");
|
|
1392
1406
|
}
|
|
1393
1407
|
if (!text) {
|
|
1394
1408
|
throw new Error("text is null");
|
|
1395
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
|
+
};
|
|
1396
1426
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1397
1427
|
if (newValue !== text) {
|
|
1398
1428
|
this.logger.info(text + "=" + newValue);
|
|
1399
1429
|
text = newValue;
|
|
1400
1430
|
}
|
|
1401
|
-
const startTime = Date.now();
|
|
1402
|
-
let error = null;
|
|
1403
|
-
let screenshotId = null;
|
|
1404
|
-
let screenshotPath = null;
|
|
1405
|
-
const info = {};
|
|
1406
|
-
info.log =
|
|
1407
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1408
|
-
info.operation = "containsPattern";
|
|
1409
|
-
info.selectors = selectors;
|
|
1410
|
-
info.value = text;
|
|
1411
|
-
info.pattern = pattern;
|
|
1412
1431
|
let foundObj = null;
|
|
1413
1432
|
try {
|
|
1414
|
-
|
|
1433
|
+
await _preCommand(state, this);
|
|
1434
|
+
state.info.pattern = pattern;
|
|
1435
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1415
1436
|
if (foundObj && foundObj.element) {
|
|
1416
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1437
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1417
1438
|
}
|
|
1418
|
-
|
|
1439
|
+
await _screenshot(state, this);
|
|
1419
1440
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1420
1441
|
pattern = pattern.replace("{text}", escapedText);
|
|
1421
1442
|
let regex = new RegExp(pattern, "im");
|
|
1422
|
-
if (!regex.test(foundObj
|
|
1423
|
-
info.foundText = foundObj
|
|
1443
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1444
|
+
state.info.foundText = foundObj?.text;
|
|
1424
1445
|
throw new Error("element doesn't contain text " + text);
|
|
1425
1446
|
}
|
|
1426
|
-
return info;
|
|
1447
|
+
return state.info;
|
|
1427
1448
|
}
|
|
1428
1449
|
catch (e) {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1432
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1433
|
-
info.screenshotPath = screenshotPath;
|
|
1434
|
-
Object.assign(e, { info: info });
|
|
1435
|
-
error = e;
|
|
1436
|
-
throw e;
|
|
1450
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1451
|
+
await _commandError(state, e, this);
|
|
1437
1452
|
}
|
|
1438
1453
|
finally {
|
|
1439
|
-
|
|
1440
|
-
this._reportToWorld(world, {
|
|
1441
|
-
element_name: selectors.element_name,
|
|
1442
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1443
|
-
value: pattern,
|
|
1444
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1445
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1446
|
-
result: error
|
|
1447
|
-
? {
|
|
1448
|
-
status: "FAILED",
|
|
1449
|
-
startTime,
|
|
1450
|
-
endTime,
|
|
1451
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1452
|
-
}
|
|
1453
|
-
: {
|
|
1454
|
-
status: "PASSED",
|
|
1455
|
-
startTime,
|
|
1456
|
-
endTime,
|
|
1457
|
-
},
|
|
1458
|
-
info: info,
|
|
1459
|
-
});
|
|
1454
|
+
_commandFinally(state, this);
|
|
1460
1455
|
}
|
|
1461
1456
|
}
|
|
1462
1457
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
+
};
|
|
1465
1473
|
if (!text) {
|
|
1466
1474
|
throw new Error("text is null");
|
|
1467
1475
|
}
|
|
1468
|
-
|
|
1469
|
-
let error = null;
|
|
1470
|
-
let screenshotId = null;
|
|
1471
|
-
let screenshotPath = null;
|
|
1472
|
-
const info = {};
|
|
1473
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1474
|
-
info.operation = "containsText";
|
|
1475
|
-
info.selectors = selectors;
|
|
1476
|
+
text = unEscapeString(text);
|
|
1476
1477
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1477
1478
|
if (newValue !== text) {
|
|
1478
1479
|
this.logger.info(text + "=" + newValue);
|
|
1479
1480
|
text = newValue;
|
|
1480
1481
|
}
|
|
1481
|
-
info.value = text;
|
|
1482
1482
|
let foundObj = null;
|
|
1483
1483
|
try {
|
|
1484
|
-
|
|
1484
|
+
await _preCommand(state, this);
|
|
1485
|
+
foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
|
|
1485
1486
|
if (foundObj && foundObj.element) {
|
|
1486
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1487
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1487
1488
|
}
|
|
1488
|
-
|
|
1489
|
+
await _screenshot(state, this);
|
|
1489
1490
|
const dateAlternatives = findDateAlternatives(text);
|
|
1490
1491
|
const numberAlternatives = findNumberAlternatives(text);
|
|
1491
1492
|
if (dateAlternatives.date) {
|
|
1492
1493
|
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1493
|
-
if (
|
|
1494
|
-
|
|
1495
|
-
return info;
|
|
1494
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1495
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1496
|
+
return state.info;
|
|
1496
1497
|
}
|
|
1497
1498
|
}
|
|
1498
1499
|
throw new Error("element doesn't contain text " + text);
|
|
1499
1500
|
}
|
|
1500
1501
|
else if (numberAlternatives.number) {
|
|
1501
1502
|
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1504
|
-
return info;
|
|
1503
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1504
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1505
|
+
return state.info;
|
|
1505
1506
|
}
|
|
1506
1507
|
}
|
|
1507
1508
|
throw new Error("element doesn't contain text " + text);
|
|
1508
1509
|
}
|
|
1509
|
-
else if (!
|
|
1510
|
-
info.foundText = foundObj
|
|
1511
|
-
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;
|
|
1512
1513
|
throw new Error("element doesn't contain text " + text);
|
|
1513
1514
|
}
|
|
1514
|
-
return info;
|
|
1515
|
+
return state.info;
|
|
1515
1516
|
}
|
|
1516
1517
|
catch (e) {
|
|
1517
|
-
|
|
1518
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1519
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1520
|
-
info.screenshotPath = screenshotPath;
|
|
1521
|
-
Object.assign(e, { info: info });
|
|
1522
|
-
error = e;
|
|
1523
|
-
throw e;
|
|
1518
|
+
await _commandError(state, e, this);
|
|
1524
1519
|
}
|
|
1525
1520
|
finally {
|
|
1526
|
-
|
|
1527
|
-
this._reportToWorld(world, {
|
|
1528
|
-
element_name: selectors.element_name,
|
|
1529
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1530
|
-
text: `Verify element contains text: ${text}`,
|
|
1531
|
-
value: text,
|
|
1532
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1533
|
-
result: error
|
|
1534
|
-
? {
|
|
1535
|
-
status: "FAILED",
|
|
1536
|
-
startTime,
|
|
1537
|
-
endTime,
|
|
1538
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1539
|
-
}
|
|
1540
|
-
: {
|
|
1541
|
-
status: "PASSED",
|
|
1542
|
-
startTime,
|
|
1543
|
-
endTime,
|
|
1544
|
-
},
|
|
1545
|
-
info: info,
|
|
1546
|
-
});
|
|
1521
|
+
_commandFinally(state, this);
|
|
1547
1522
|
}
|
|
1548
1523
|
}
|
|
1549
1524
|
_getDataFile(world = null) {
|
|
@@ -1772,7 +1747,6 @@ class StableBrowser {
|
|
|
1772
1747
|
}
|
|
1773
1748
|
async takeScreenshot(screenshotPath) {
|
|
1774
1749
|
const playContext = this.context.playContext;
|
|
1775
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1776
1750
|
// Using CDP to capture the screenshot
|
|
1777
1751
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1778
1752
|
document.body.scrollWidth,
|
|
@@ -1782,97 +1756,67 @@ class StableBrowser {
|
|
|
1782
1756
|
document.body.clientWidth,
|
|
1783
1757
|
document.documentElement.clientWidth,
|
|
1784
1758
|
])));
|
|
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
|
-
|
|
1818
|
-
|
|
1819
|
-
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
|
+
}
|
|
1820
1793
|
}
|
|
1821
1794
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
+
};
|
|
1827
1805
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1828
|
-
const info = {};
|
|
1829
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1830
|
-
info.operation = "verify";
|
|
1831
|
-
info.selectors = selectors;
|
|
1832
1806
|
try {
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1837
|
-
await this._highlightElements(element);
|
|
1838
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1839
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1840
|
-
return info;
|
|
1807
|
+
await _preCommand(state, this);
|
|
1808
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
1809
|
+
return state.info;
|
|
1841
1810
|
}
|
|
1842
1811
|
catch (e) {
|
|
1843
|
-
|
|
1844
|
-
this.logger.error("verify failed " + info.log);
|
|
1845
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1846
|
-
info.screenshotPath = screenshotPath;
|
|
1847
|
-
Object.assign(e, { info: info });
|
|
1848
|
-
error = e;
|
|
1849
|
-
throw e;
|
|
1812
|
+
await _commandError(state, e, this);
|
|
1850
1813
|
}
|
|
1851
1814
|
finally {
|
|
1852
|
-
|
|
1853
|
-
this._reportToWorld(world, {
|
|
1854
|
-
element_name: selectors.element_name,
|
|
1855
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1856
|
-
text: "Verify element exists in page",
|
|
1857
|
-
screenshotId,
|
|
1858
|
-
result: error
|
|
1859
|
-
? {
|
|
1860
|
-
status: "FAILED",
|
|
1861
|
-
startTime,
|
|
1862
|
-
endTime,
|
|
1863
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1864
|
-
}
|
|
1865
|
-
: {
|
|
1866
|
-
status: "PASSED",
|
|
1867
|
-
startTime,
|
|
1868
|
-
endTime,
|
|
1869
|
-
},
|
|
1870
|
-
info: info,
|
|
1871
|
-
});
|
|
1815
|
+
_commandFinally(state, this);
|
|
1872
1816
|
}
|
|
1873
1817
|
}
|
|
1874
1818
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1875
|
-
|
|
1819
|
+
_validateSelectors(selectors);
|
|
1876
1820
|
const startTime = Date.now();
|
|
1877
1821
|
let error = null;
|
|
1878
1822
|
let screenshotId = null;
|
|
@@ -1915,7 +1859,8 @@ class StableBrowser {
|
|
|
1915
1859
|
info.screenshotPath = screenshotPath;
|
|
1916
1860
|
Object.assign(e, { info: info });
|
|
1917
1861
|
error = e;
|
|
1918
|
-
throw e;
|
|
1862
|
+
// throw e;
|
|
1863
|
+
await _commandError({ text: "extractAttribute", operation: "extractAttribute", selectors, attribute, variable }, e, this);
|
|
1919
1864
|
}
|
|
1920
1865
|
finally {
|
|
1921
1866
|
const endTime = Date.now();
|
|
@@ -1931,7 +1876,7 @@ class StableBrowser {
|
|
|
1931
1876
|
status: "FAILED",
|
|
1932
1877
|
startTime,
|
|
1933
1878
|
endTime,
|
|
1934
|
-
message: error
|
|
1879
|
+
message: error?.message,
|
|
1935
1880
|
}
|
|
1936
1881
|
: {
|
|
1937
1882
|
status: "PASSED",
|
|
@@ -2016,7 +1961,8 @@ class StableBrowser {
|
|
|
2016
1961
|
catch (e) {
|
|
2017
1962
|
errorCount++;
|
|
2018
1963
|
if (errorCount > 3) {
|
|
2019
|
-
throw e;
|
|
1964
|
+
// throw e;
|
|
1965
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2020
1966
|
}
|
|
2021
1967
|
// ignore
|
|
2022
1968
|
}
|
|
@@ -2127,7 +2073,8 @@ class StableBrowser {
|
|
|
2127
2073
|
info.screenshotPath = screenshotPath;
|
|
2128
2074
|
Object.assign(e, { info: info });
|
|
2129
2075
|
error = e;
|
|
2130
|
-
throw e;
|
|
2076
|
+
// throw e;
|
|
2077
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2131
2078
|
}
|
|
2132
2079
|
finally {
|
|
2133
2080
|
const endTime = Date.now();
|
|
@@ -2140,7 +2087,7 @@ class StableBrowser {
|
|
|
2140
2087
|
status: "FAILED",
|
|
2141
2088
|
startTime,
|
|
2142
2089
|
endTime,
|
|
2143
|
-
message: error
|
|
2090
|
+
message: error?.message,
|
|
2144
2091
|
}
|
|
2145
2092
|
: {
|
|
2146
2093
|
status: "PASSED",
|
|
@@ -2176,20 +2123,20 @@ class StableBrowser {
|
|
|
2176
2123
|
for (let i = 0; i < frames.length; i++) {
|
|
2177
2124
|
if (dateAlternatives.date) {
|
|
2178
2125
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2179
|
-
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, {});
|
|
2180
2127
|
result.frame = frames[i];
|
|
2181
2128
|
results.push(result);
|
|
2182
2129
|
}
|
|
2183
2130
|
}
|
|
2184
2131
|
else if (numberAlternatives.number) {
|
|
2185
2132
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2186
|
-
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, {});
|
|
2187
2134
|
result.frame = frames[i];
|
|
2188
2135
|
results.push(result);
|
|
2189
2136
|
}
|
|
2190
2137
|
}
|
|
2191
2138
|
else {
|
|
2192
|
-
const result = await this._locateElementByText(frames[i], text, "
|
|
2139
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
|
|
2193
2140
|
result.frame = frames[i];
|
|
2194
2141
|
results.push(result);
|
|
2195
2142
|
}
|
|
@@ -2225,7 +2172,8 @@ class StableBrowser {
|
|
|
2225
2172
|
info.screenshotPath = screenshotPath;
|
|
2226
2173
|
Object.assign(e, { info: info });
|
|
2227
2174
|
error = e;
|
|
2228
|
-
throw e;
|
|
2175
|
+
// throw e;
|
|
2176
|
+
await _commandError({ text: "verifyTextExistInPage", operation: "verifyTextExistInPage", text, info }, e, this);
|
|
2229
2177
|
}
|
|
2230
2178
|
finally {
|
|
2231
2179
|
const endTime = Date.now();
|
|
@@ -2238,7 +2186,7 @@ class StableBrowser {
|
|
|
2238
2186
|
status: "FAILED",
|
|
2239
2187
|
startTime,
|
|
2240
2188
|
endTime,
|
|
2241
|
-
message: error
|
|
2189
|
+
message: error?.message,
|
|
2242
2190
|
}
|
|
2243
2191
|
: {
|
|
2244
2192
|
status: "PASSED",
|
|
@@ -2309,7 +2257,8 @@ class StableBrowser {
|
|
|
2309
2257
|
info.screenshotPath = screenshotPath;
|
|
2310
2258
|
Object.assign(e, { info: info });
|
|
2311
2259
|
error = e;
|
|
2312
|
-
throw e;
|
|
2260
|
+
// throw e;
|
|
2261
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2313
2262
|
}
|
|
2314
2263
|
finally {
|
|
2315
2264
|
const endTime = Date.now();
|
|
@@ -2322,7 +2271,7 @@ class StableBrowser {
|
|
|
2322
2271
|
status: "FAILED",
|
|
2323
2272
|
startTime,
|
|
2324
2273
|
endTime,
|
|
2325
|
-
message: error
|
|
2274
|
+
message: error?.message,
|
|
2326
2275
|
}
|
|
2327
2276
|
: {
|
|
2328
2277
|
status: "PASSED",
|
|
@@ -2354,7 +2303,7 @@ class StableBrowser {
|
|
|
2354
2303
|
this.logger.info("Table data verified");
|
|
2355
2304
|
}
|
|
2356
2305
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2357
|
-
|
|
2306
|
+
_validateSelectors(selectors);
|
|
2358
2307
|
const startTime = Date.now();
|
|
2359
2308
|
let error = null;
|
|
2360
2309
|
let screenshotId = null;
|
|
@@ -2376,7 +2325,8 @@ class StableBrowser {
|
|
|
2376
2325
|
info.screenshotPath = screenshotPath;
|
|
2377
2326
|
Object.assign(e, { info: info });
|
|
2378
2327
|
error = e;
|
|
2379
|
-
throw e;
|
|
2328
|
+
// throw e;
|
|
2329
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2380
2330
|
}
|
|
2381
2331
|
finally {
|
|
2382
2332
|
const endTime = Date.now();
|
|
@@ -2390,7 +2340,7 @@ class StableBrowser {
|
|
|
2390
2340
|
status: "FAILED",
|
|
2391
2341
|
startTime,
|
|
2392
2342
|
endTime,
|
|
2393
|
-
message: error
|
|
2343
|
+
message: error?.message,
|
|
2394
2344
|
}
|
|
2395
2345
|
: {
|
|
2396
2346
|
status: "PASSED",
|
|
@@ -2402,7 +2352,7 @@ class StableBrowser {
|
|
|
2402
2352
|
}
|
|
2403
2353
|
}
|
|
2404
2354
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2405
|
-
|
|
2355
|
+
_validateSelectors(selectors);
|
|
2406
2356
|
if (!query) {
|
|
2407
2357
|
throw new Error("query is null");
|
|
2408
2358
|
}
|
|
@@ -2541,7 +2491,8 @@ class StableBrowser {
|
|
|
2541
2491
|
info.screenshotPath = screenshotPath;
|
|
2542
2492
|
Object.assign(e, { info: info });
|
|
2543
2493
|
error = e;
|
|
2544
|
-
throw e;
|
|
2494
|
+
// throw e;
|
|
2495
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2545
2496
|
}
|
|
2546
2497
|
finally {
|
|
2547
2498
|
const endTime = Date.now();
|
|
@@ -2555,7 +2506,7 @@ class StableBrowser {
|
|
|
2555
2506
|
status: "FAILED",
|
|
2556
2507
|
startTime,
|
|
2557
2508
|
endTime,
|
|
2558
|
-
message: error
|
|
2509
|
+
message: error?.message,
|
|
2559
2510
|
}
|
|
2560
2511
|
: {
|
|
2561
2512
|
status: "PASSED",
|
|
@@ -2567,27 +2518,7 @@ class StableBrowser {
|
|
|
2567
2518
|
}
|
|
2568
2519
|
}
|
|
2569
2520
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2570
|
-
|
|
2571
|
-
return value;
|
|
2572
|
-
}
|
|
2573
|
-
// find all the accurance of {{(.*?)}} and replace with the value
|
|
2574
|
-
let regex = /{{(.*?)}}/g;
|
|
2575
|
-
let matches = value.match(regex);
|
|
2576
|
-
if (matches) {
|
|
2577
|
-
const testData = this.getTestData(world);
|
|
2578
|
-
for (let i = 0; i < matches.length; i++) {
|
|
2579
|
-
let match = matches[i];
|
|
2580
|
-
let key = match.substring(2, match.length - 2);
|
|
2581
|
-
let newValue = objectPath.get(testData, key, null);
|
|
2582
|
-
if (newValue !== null) {
|
|
2583
|
-
value = value.replace(match, newValue);
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
|
|
2588
|
-
return await decrypt(value, null, totpWait);
|
|
2589
|
-
}
|
|
2590
|
-
return value;
|
|
2521
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2591
2522
|
}
|
|
2592
2523
|
_getLoadTimeout(options) {
|
|
2593
2524
|
let timeout = 15000;
|
|
@@ -2647,7 +2578,7 @@ class StableBrowser {
|
|
|
2647
2578
|
status: "FAILED",
|
|
2648
2579
|
startTime,
|
|
2649
2580
|
endTime,
|
|
2650
|
-
message: error
|
|
2581
|
+
message: error?.message,
|
|
2651
2582
|
}
|
|
2652
2583
|
: {
|
|
2653
2584
|
status: "PASSED",
|
|
@@ -2668,6 +2599,7 @@ class StableBrowser {
|
|
|
2668
2599
|
}
|
|
2669
2600
|
catch (e) {
|
|
2670
2601
|
console.log(".");
|
|
2602
|
+
await _commandError({ text: "closePage", operation: "closePage", info }, e, this);
|
|
2671
2603
|
}
|
|
2672
2604
|
finally {
|
|
2673
2605
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -2682,7 +2614,7 @@ class StableBrowser {
|
|
|
2682
2614
|
status: "FAILED",
|
|
2683
2615
|
startTime,
|
|
2684
2616
|
endTime,
|
|
2685
|
-
message: error
|
|
2617
|
+
message: error?.message,
|
|
2686
2618
|
}
|
|
2687
2619
|
: {
|
|
2688
2620
|
status: "PASSED",
|
|
@@ -2710,6 +2642,7 @@ class StableBrowser {
|
|
|
2710
2642
|
}
|
|
2711
2643
|
catch (e) {
|
|
2712
2644
|
console.log(".");
|
|
2645
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2713
2646
|
}
|
|
2714
2647
|
finally {
|
|
2715
2648
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -2724,7 +2657,7 @@ class StableBrowser {
|
|
|
2724
2657
|
status: "FAILED",
|
|
2725
2658
|
startTime,
|
|
2726
2659
|
endTime,
|
|
2727
|
-
message: error
|
|
2660
|
+
message: error?.message,
|
|
2728
2661
|
}
|
|
2729
2662
|
: {
|
|
2730
2663
|
status: "PASSED",
|
|
@@ -2746,6 +2679,7 @@ class StableBrowser {
|
|
|
2746
2679
|
}
|
|
2747
2680
|
catch (e) {
|
|
2748
2681
|
console.log(".");
|
|
2682
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2749
2683
|
}
|
|
2750
2684
|
finally {
|
|
2751
2685
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -2760,7 +2694,7 @@ class StableBrowser {
|
|
|
2760
2694
|
status: "FAILED",
|
|
2761
2695
|
startTime,
|
|
2762
2696
|
endTime,
|
|
2763
|
-
message: error
|
|
2697
|
+
message: error?.message,
|
|
2764
2698
|
}
|
|
2765
2699
|
: {
|
|
2766
2700
|
status: "PASSED",
|
|
@@ -2945,5 +2879,10 @@ const KEYBOARD_EVENTS = [
|
|
|
2945
2879
|
"TVAntennaCable",
|
|
2946
2880
|
"TVAudioDescription",
|
|
2947
2881
|
];
|
|
2882
|
+
function unEscapeString(str) {
|
|
2883
|
+
const placeholder = "__NEWLINE__";
|
|
2884
|
+
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2885
|
+
return str;
|
|
2886
|
+
}
|
|
2948
2887
|
export { StableBrowser };
|
|
2949
2888
|
//# sourceMappingURL=stable_browser.js.map
|