automation_model 1.0.396-dev → 1.0.396-main
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.js +21 -10
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +6 -2
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +3 -2
- package/lib/browser_manager.js.map +1 -1
- package/lib/environment.d.ts +3 -0
- package/lib/environment.js +1 -0
- package/lib/environment.js.map +1 -1
- package/lib/init_browser.d.ts +2 -1
- package/lib/init_browser.js +38 -3
- package/lib/init_browser.js.map +1 -1
- package/lib/stable_browser.d.ts +16 -4
- package/lib/stable_browser.js +375 -181
- package/lib/stable_browser.js.map +1 -1
- package/lib/test_context.d.ts +3 -0
- package/lib/test_context.js +3 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.js +2 -2
- package/lib/utils.js.map +1 -1
- package/package.json +12 -7
package/lib/stable_browser.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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";
|
|
@@ -13,6 +13,9 @@ import { getTableCells, getTableData } from "./table_analyze.js";
|
|
|
13
13
|
import objectPath from "object-path";
|
|
14
14
|
import { decrypt } from "./utils.js";
|
|
15
15
|
import csv from "csv-parser";
|
|
16
|
+
import { Readable } from "node:stream";
|
|
17
|
+
import readline from "readline";
|
|
18
|
+
import { getContext } from "./init_browser.js";
|
|
16
19
|
const Types = {
|
|
17
20
|
CLICK: "click_element",
|
|
18
21
|
NAVIGATE: "navigate",
|
|
@@ -28,6 +31,7 @@ const Types = {
|
|
|
28
31
|
SELECT: "select_combobox",
|
|
29
32
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
30
33
|
TYPE_PRESS: "type_press",
|
|
34
|
+
PRESS: "press_key",
|
|
31
35
|
HOVER: "hover_element",
|
|
32
36
|
CHECK: "check_element",
|
|
33
37
|
UNCHECK: "uncheck_element",
|
|
@@ -36,16 +40,22 @@ const Types = {
|
|
|
36
40
|
SET_DATE_TIME: "set_date_time",
|
|
37
41
|
SET_VIEWPORT: "set_viewport",
|
|
38
42
|
VERIFY_VISUAL: "verify_visual",
|
|
43
|
+
LOAD_DATA: "load_data",
|
|
44
|
+
SET_INPUT: "set_input",
|
|
39
45
|
};
|
|
46
|
+
export const apps = {};
|
|
40
47
|
class StableBrowser {
|
|
41
|
-
constructor(browser, page, logger = null, context = null) {
|
|
48
|
+
constructor(browser, page, logger = null, context = null, world = null) {
|
|
42
49
|
this.browser = browser;
|
|
43
50
|
this.page = page;
|
|
44
51
|
this.logger = logger;
|
|
45
52
|
this.context = context;
|
|
53
|
+
this.world = world;
|
|
46
54
|
this.project_path = null;
|
|
47
55
|
this.webLogFile = null;
|
|
56
|
+
this.networkLogger = null;
|
|
48
57
|
this.configuration = null;
|
|
58
|
+
this.appName = "main";
|
|
49
59
|
if (!this.logger) {
|
|
50
60
|
this.logger = console;
|
|
51
61
|
}
|
|
@@ -71,19 +81,36 @@ class StableBrowser {
|
|
|
71
81
|
this.logger.error("unable to read ai_config.json");
|
|
72
82
|
}
|
|
73
83
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
74
|
-
this.
|
|
75
|
-
this.registerConsoleLogListener(page, context, this.webLogFile);
|
|
76
|
-
this.registerRequestListener();
|
|
84
|
+
this.world = world;
|
|
77
85
|
context.pages = [this.page];
|
|
78
86
|
context.pageLoading = { status: false };
|
|
87
|
+
this.registerEventListeners(this.context);
|
|
88
|
+
}
|
|
89
|
+
registerEventListeners(context) {
|
|
90
|
+
this.registerConsoleLogListener(this.page, context);
|
|
91
|
+
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
92
|
+
if (!context.pageLoading) {
|
|
93
|
+
context.pageLoading = { status: false };
|
|
94
|
+
}
|
|
79
95
|
context.playContext.on("page", async function (page) {
|
|
80
96
|
context.pageLoading.status = true;
|
|
81
97
|
this.page = page;
|
|
82
98
|
context.page = page;
|
|
83
99
|
context.pages.push(page);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
page.on("close", async () => {
|
|
101
|
+
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
102
|
+
this.context.pages.pop();
|
|
103
|
+
this.page = this.context.pages[this.context.pages.length - 1];
|
|
104
|
+
this.context.page = this.page;
|
|
105
|
+
try {
|
|
106
|
+
let title = await this.page.title();
|
|
107
|
+
console.log("Switched to page " + title);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error("Error on page close", error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
87
114
|
try {
|
|
88
115
|
await this.waitForPageLoad();
|
|
89
116
|
console.log("Switch page: " + (await page.title()));
|
|
@@ -94,6 +121,36 @@ class StableBrowser {
|
|
|
94
121
|
context.pageLoading.status = false;
|
|
95
122
|
}.bind(this));
|
|
96
123
|
}
|
|
124
|
+
async switchApp(appName) {
|
|
125
|
+
// check if the current app (this.appName) is the same as the new app
|
|
126
|
+
if (this.appName === appName) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let navigate = false;
|
|
130
|
+
if (!apps[appName]) {
|
|
131
|
+
let newContext = await getContext(null, false, this.logger, appName, false, this);
|
|
132
|
+
navigate = true;
|
|
133
|
+
apps[appName] = {
|
|
134
|
+
context: newContext,
|
|
135
|
+
browser: newContext.browser,
|
|
136
|
+
page: newContext.page,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const tempContext = {};
|
|
140
|
+
this._copyContext(this, tempContext);
|
|
141
|
+
this._copyContext(apps[appName], this);
|
|
142
|
+
apps[this.appName] = tempContext;
|
|
143
|
+
this.appName = appName;
|
|
144
|
+
if (navigate) {
|
|
145
|
+
await this.goto(this.context.environment.baseUrl);
|
|
146
|
+
await this.waitForPageLoad();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
_copyContext(from, to) {
|
|
150
|
+
to.browser = from.browser;
|
|
151
|
+
to.page = from.page;
|
|
152
|
+
to.context = from.context;
|
|
153
|
+
}
|
|
97
154
|
getWebLogFile(logFolder) {
|
|
98
155
|
if (!fs.existsSync(logFolder)) {
|
|
99
156
|
fs.mkdirSync(logFolder, { recursive: true });
|
|
@@ -105,37 +162,63 @@ class StableBrowser {
|
|
|
105
162
|
const fileName = nextIndex + ".json";
|
|
106
163
|
return path.join(logFolder, fileName);
|
|
107
164
|
}
|
|
108
|
-
registerConsoleLogListener(page, context
|
|
165
|
+
registerConsoleLogListener(page, context) {
|
|
109
166
|
if (!this.context.webLogger) {
|
|
110
167
|
this.context.webLogger = [];
|
|
111
168
|
}
|
|
112
169
|
page.on("console", async (msg) => {
|
|
113
|
-
|
|
170
|
+
var _a;
|
|
171
|
+
const obj = {
|
|
114
172
|
type: msg.type(),
|
|
115
173
|
text: msg.text(),
|
|
116
174
|
location: msg.location(),
|
|
117
175
|
time: new Date().toISOString(),
|
|
118
|
-
}
|
|
119
|
-
|
|
176
|
+
};
|
|
177
|
+
this.context.webLogger.push(obj);
|
|
178
|
+
(_a = this.world) === null || _a === void 0 ? void 0 : _a.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
120
179
|
});
|
|
121
180
|
}
|
|
122
|
-
registerRequestListener() {
|
|
123
|
-
this.
|
|
181
|
+
registerRequestListener(page, context, logFile) {
|
|
182
|
+
if (!this.context.networkLogger) {
|
|
183
|
+
this.context.networkLogger = [];
|
|
184
|
+
}
|
|
185
|
+
page.on("request", async (data) => {
|
|
186
|
+
var _a;
|
|
187
|
+
const startTime = new Date().getTime();
|
|
124
188
|
try {
|
|
125
|
-
const pageUrl = new URL(
|
|
189
|
+
const pageUrl = new URL(page.url());
|
|
126
190
|
const requestUrl = new URL(data.url());
|
|
127
191
|
if (pageUrl.hostname === requestUrl.hostname) {
|
|
128
192
|
const method = data.method();
|
|
129
|
-
if (
|
|
193
|
+
if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
130
194
|
const token = await data.headerValue("Authorization");
|
|
131
195
|
if (token) {
|
|
132
|
-
|
|
196
|
+
context.authtoken = token;
|
|
133
197
|
}
|
|
134
198
|
}
|
|
135
199
|
}
|
|
200
|
+
const response = await data.response();
|
|
201
|
+
const endTime = new Date().getTime();
|
|
202
|
+
const obj = {
|
|
203
|
+
url: data.url(),
|
|
204
|
+
method: data.method(),
|
|
205
|
+
postData: data.postData(),
|
|
206
|
+
error: data.failure() ? data.failure().errorText : null,
|
|
207
|
+
duration: endTime - startTime,
|
|
208
|
+
startTime,
|
|
209
|
+
};
|
|
210
|
+
context.networkLogger.push(obj);
|
|
211
|
+
(_a = this.world) === null || _a === void 0 ? void 0 : _a.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
136
212
|
}
|
|
137
213
|
catch (error) {
|
|
138
214
|
console.error("Error in request listener", error);
|
|
215
|
+
context.networkLogger.push({
|
|
216
|
+
error: "not able to listen",
|
|
217
|
+
message: error.message,
|
|
218
|
+
stack: error.stack,
|
|
219
|
+
time: new Date().toISOString(),
|
|
220
|
+
});
|
|
221
|
+
// await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
|
|
139
222
|
}
|
|
140
223
|
});
|
|
141
224
|
}
|
|
@@ -178,26 +261,83 @@ class StableBrowser {
|
|
|
178
261
|
}
|
|
179
262
|
return text;
|
|
180
263
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
264
|
+
_fixLocatorUsingParams(locator, _params) {
|
|
265
|
+
// check if not null
|
|
266
|
+
if (!locator) {
|
|
267
|
+
return locator;
|
|
268
|
+
}
|
|
269
|
+
// clone the locator
|
|
270
|
+
locator = JSON.parse(JSON.stringify(locator));
|
|
271
|
+
this.scanAndManipulate(locator, _params);
|
|
272
|
+
return locator;
|
|
273
|
+
}
|
|
274
|
+
_isObject(value) {
|
|
275
|
+
return value && typeof value === "object" && value.constructor === Object;
|
|
276
|
+
}
|
|
277
|
+
scanAndManipulate(currentObj, _params) {
|
|
278
|
+
for (const key in currentObj) {
|
|
279
|
+
if (typeof currentObj[key] === "string") {
|
|
280
|
+
// Perform string manipulation
|
|
281
|
+
currentObj[key] = this._fixUsingParams(currentObj[key], _params);
|
|
282
|
+
}
|
|
283
|
+
else if (this._isObject(currentObj[key])) {
|
|
284
|
+
// Recursively scan nested objects
|
|
285
|
+
this.scanAndManipulate(currentObj[key], _params);
|
|
286
|
+
}
|
|
184
287
|
}
|
|
288
|
+
}
|
|
289
|
+
_getLocator(locator, scope, _params) {
|
|
290
|
+
locator = this._fixLocatorUsingParams(locator, _params);
|
|
291
|
+
let locatorReturn;
|
|
185
292
|
if (locator.role) {
|
|
186
293
|
if (locator.role[1].nameReg) {
|
|
187
294
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
188
295
|
delete locator.role[1].nameReg;
|
|
189
296
|
}
|
|
190
|
-
if (locator.role[1].name) {
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
297
|
+
// if (locator.role[1].name) {
|
|
298
|
+
// locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
|
|
299
|
+
// }
|
|
300
|
+
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
194
301
|
}
|
|
195
302
|
if (locator.css) {
|
|
196
|
-
|
|
303
|
+
locatorReturn = scope.locator(locator.css);
|
|
304
|
+
}
|
|
305
|
+
// handle role/name locators
|
|
306
|
+
// locator.selector will be something like: textbox[name="Username"i]
|
|
307
|
+
if (locator.engine === "internal:role") {
|
|
308
|
+
// extract the role, name and the i/s flags using regex
|
|
309
|
+
const match = locator.selector.match(/(.*)\[(.*)="(.*)"(.*)\]/);
|
|
310
|
+
if (match) {
|
|
311
|
+
const role = match[1];
|
|
312
|
+
const name = match[3];
|
|
313
|
+
const flags = match[4];
|
|
314
|
+
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
315
|
+
}
|
|
197
316
|
}
|
|
198
|
-
|
|
317
|
+
if (locator === null || locator === void 0 ? void 0 : locator.engine) {
|
|
318
|
+
if (locator.engine === "css") {
|
|
319
|
+
locatorReturn = scope.locator(locator.selector);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
let selector = locator.selector;
|
|
323
|
+
if (locator.engine === "internal:attr") {
|
|
324
|
+
if (!selector.startsWith("[")) {
|
|
325
|
+
selector = `[${selector}]`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!locatorReturn) {
|
|
332
|
+
console.error(locator);
|
|
333
|
+
throw new Error("Locator undefined");
|
|
334
|
+
}
|
|
335
|
+
return locatorReturn;
|
|
199
336
|
}
|
|
200
337
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
338
|
+
if (css && css.locator) {
|
|
339
|
+
css = css.locator;
|
|
340
|
+
}
|
|
201
341
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
202
342
|
if (result.elementCount === 0) {
|
|
203
343
|
return;
|
|
@@ -225,6 +365,15 @@ class StableBrowser {
|
|
|
225
365
|
return false;
|
|
226
366
|
}
|
|
227
367
|
document.isParent = isParent;
|
|
368
|
+
function getRegex(str) {
|
|
369
|
+
const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
|
|
370
|
+
if (!match) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
let [_, pattern, flags] = match;
|
|
374
|
+
return new RegExp(pattern, flags);
|
|
375
|
+
}
|
|
376
|
+
document.getRegex = getRegex;
|
|
228
377
|
function collectAllShadowDomElements(element, result = []) {
|
|
229
378
|
// Check and add the element if it has a shadow root
|
|
230
379
|
if (element.shadowRoot) {
|
|
@@ -243,6 +392,10 @@ class StableBrowser {
|
|
|
243
392
|
if (!tag) {
|
|
244
393
|
tag = "*";
|
|
245
394
|
}
|
|
395
|
+
let regexpSearch = document.getRegex(text);
|
|
396
|
+
if (regexpSearch) {
|
|
397
|
+
regex = true;
|
|
398
|
+
}
|
|
246
399
|
let elements = Array.from(document.querySelectorAll(tag));
|
|
247
400
|
let shadowHosts = [];
|
|
248
401
|
document.collectAllShadowDomElements(document, shadowHosts);
|
|
@@ -258,7 +411,9 @@ class StableBrowser {
|
|
|
258
411
|
let randomToken = null;
|
|
259
412
|
const foundElements = [];
|
|
260
413
|
if (regex) {
|
|
261
|
-
|
|
414
|
+
if (!regexpSearch) {
|
|
415
|
+
regexpSearch = new RegExp(text, "im");
|
|
416
|
+
}
|
|
262
417
|
for (let i = 0; i < elements.length; i++) {
|
|
263
418
|
const element = elements[i];
|
|
264
419
|
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
@@ -406,6 +561,8 @@ class StableBrowser {
|
|
|
406
561
|
if (result.foundElements.length > 0) {
|
|
407
562
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
408
563
|
await dialogCloseLocator.click();
|
|
564
|
+
// wait for the dialog to close
|
|
565
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
409
566
|
return { rerun: true };
|
|
410
567
|
}
|
|
411
568
|
}
|
|
@@ -414,7 +571,7 @@ class StableBrowser {
|
|
|
414
571
|
}
|
|
415
572
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
416
573
|
for (let i = 0; i < 3; i++) {
|
|
417
|
-
info.log += "attempt " + i + ":
|
|
574
|
+
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
418
575
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
419
576
|
let selector = selectors.locators[j];
|
|
420
577
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
@@ -434,9 +591,30 @@ class StableBrowser {
|
|
|
434
591
|
//let arrayMode = Array.isArray(selectors);
|
|
435
592
|
let scope = this.page;
|
|
436
593
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
594
|
+
const findFrame = (frame, framescope) => {
|
|
595
|
+
for (let i = 0; i < frame.selectors.length; i++) {
|
|
596
|
+
let frameLocator = frame.selectors[i];
|
|
597
|
+
if (frameLocator.css) {
|
|
598
|
+
framescope = framescope.frameLocator(frameLocator.css);
|
|
599
|
+
if (frameLocator.index) {
|
|
600
|
+
framescope = framescope.nth(frameLocator.index);
|
|
601
|
+
}
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (frame.children) {
|
|
606
|
+
return findFrame(frame.children, framescope);
|
|
607
|
+
}
|
|
608
|
+
return framescope;
|
|
609
|
+
};
|
|
437
610
|
info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
|
|
438
611
|
while (true) {
|
|
439
612
|
let frameFound = false;
|
|
613
|
+
if (selectors.nestFrmLoc) {
|
|
614
|
+
scope = findFrame(selectors.nestFrmLoc, scope);
|
|
615
|
+
frameFound = true;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
440
618
|
if (selectors.frameLocators) {
|
|
441
619
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
442
620
|
let frameLocator = selectors.frameLocators[i];
|
|
@@ -600,6 +778,9 @@ class StableBrowser {
|
|
|
600
778
|
async click(selectors, _params, options = {}, world = null) {
|
|
601
779
|
this._validateSelectors(selectors);
|
|
602
780
|
const startTime = Date.now();
|
|
781
|
+
if (options && options.context) {
|
|
782
|
+
selectors.locators[0].text = options.context;
|
|
783
|
+
}
|
|
603
784
|
const info = {};
|
|
604
785
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
605
786
|
info.operation = "click";
|
|
@@ -613,14 +794,14 @@ class StableBrowser {
|
|
|
613
794
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
614
795
|
try {
|
|
615
796
|
await this._highlightElements(element);
|
|
616
|
-
await element.click(
|
|
797
|
+
await element.click();
|
|
617
798
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
618
799
|
}
|
|
619
800
|
catch (e) {
|
|
620
801
|
// await this.closeUnexpectedPopups();
|
|
621
802
|
info.log += "click failed, will try again" + "\n";
|
|
622
803
|
element = await this._locate(selectors, info, _params);
|
|
623
|
-
await element.click
|
|
804
|
+
await element.dispatchEvent("click");
|
|
624
805
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
625
806
|
}
|
|
626
807
|
await this.waitForPageLoad();
|
|
@@ -673,7 +854,7 @@ class StableBrowser {
|
|
|
673
854
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
674
855
|
try {
|
|
675
856
|
await this._highlightElements(element);
|
|
676
|
-
await element.setChecked(checked
|
|
857
|
+
await element.setChecked(checked);
|
|
677
858
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
678
859
|
}
|
|
679
860
|
catch (e) {
|
|
@@ -737,7 +918,7 @@ class StableBrowser {
|
|
|
737
918
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
738
919
|
try {
|
|
739
920
|
await this._highlightElements(element);
|
|
740
|
-
await element.hover(
|
|
921
|
+
await element.hover();
|
|
741
922
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
742
923
|
}
|
|
743
924
|
catch (e) {
|
|
@@ -799,7 +980,7 @@ class StableBrowser {
|
|
|
799
980
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
800
981
|
try {
|
|
801
982
|
await this._highlightElements(element);
|
|
802
|
-
await element.selectOption(values
|
|
983
|
+
await element.selectOption(values);
|
|
803
984
|
}
|
|
804
985
|
catch (e) {
|
|
805
986
|
//await this.closeUnexpectedPopups();
|
|
@@ -908,71 +1089,45 @@ class StableBrowser {
|
|
|
908
1089
|
});
|
|
909
1090
|
}
|
|
910
1091
|
}
|
|
911
|
-
async
|
|
1092
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1093
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
912
1094
|
this._validateSelectors(selectors);
|
|
913
1095
|
const startTime = Date.now();
|
|
914
|
-
let error = null;
|
|
915
|
-
let screenshotId = null;
|
|
916
|
-
let screenshotPath = null;
|
|
917
1096
|
const info = {};
|
|
918
|
-
info.log = "";
|
|
919
|
-
info.operation =
|
|
1097
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
1098
|
+
info.operation = "setInputValue";
|
|
920
1099
|
info.selectors = selectors;
|
|
1100
|
+
value = this._fixUsingParams(value, _params);
|
|
921
1101
|
info.value = value;
|
|
1102
|
+
let error = null;
|
|
1103
|
+
let screenshotId = null;
|
|
1104
|
+
let screenshotPath = null;
|
|
922
1105
|
try {
|
|
923
1106
|
value = await this._replaceWithLocalData(value, this);
|
|
924
1107
|
let element = await this._locate(selectors, info, _params);
|
|
925
|
-
//insert red border around the element
|
|
926
1108
|
await this.scrollIfNeeded(element, info);
|
|
927
1109
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
928
1110
|
await this._highlightElements(element);
|
|
929
1111
|
try {
|
|
930
|
-
await element.
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
value = dayjs(value).format(format);
|
|
934
|
-
await element.fill(value);
|
|
935
|
-
}
|
|
936
|
-
else {
|
|
937
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
938
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
939
|
-
el.value = ""; // clear input
|
|
940
|
-
el.value = dateTimeValue;
|
|
941
|
-
}, dateTimeValue);
|
|
942
|
-
}
|
|
943
|
-
if (enter) {
|
|
944
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
945
|
-
await this.page.keyboard.press("Enter");
|
|
946
|
-
await this.waitForPageLoad();
|
|
947
|
-
}
|
|
1112
|
+
await element.evaluateHandle((el, value) => {
|
|
1113
|
+
el.value = value;
|
|
1114
|
+
}, value);
|
|
948
1115
|
}
|
|
949
1116
|
catch (error) {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1117
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1118
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
953
1119
|
info.screenshotPath = screenshotPath;
|
|
954
1120
|
Object.assign(error, { info: info });
|
|
955
|
-
await element.
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
value = dayjs(value).format(format);
|
|
959
|
-
await element.fill(value);
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
963
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
964
|
-
el.value = ""; // clear input
|
|
965
|
-
el.value = dateTimeValue;
|
|
966
|
-
}, dateTimeValue);
|
|
967
|
-
}
|
|
968
|
-
if (enter) {
|
|
969
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
970
|
-
await this.page.keyboard.press("Enter");
|
|
971
|
-
await this.waitForPageLoad();
|
|
972
|
-
}
|
|
1121
|
+
await element.evaluateHandle((el, value) => {
|
|
1122
|
+
el.value = value;
|
|
1123
|
+
});
|
|
973
1124
|
}
|
|
974
1125
|
}
|
|
975
|
-
catch (
|
|
1126
|
+
catch (e) {
|
|
1127
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1128
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1129
|
+
info.screenshotPath = screenshotPath;
|
|
1130
|
+
Object.assign(e, { info: info });
|
|
976
1131
|
error = e;
|
|
977
1132
|
throw e;
|
|
978
1133
|
}
|
|
@@ -980,10 +1135,10 @@ class StableBrowser {
|
|
|
980
1135
|
const endTime = Date.now();
|
|
981
1136
|
this._reportToWorld(world, {
|
|
982
1137
|
element_name: selectors.element_name,
|
|
983
|
-
type: Types.
|
|
984
|
-
|
|
1138
|
+
type: Types.SET_INPUT,
|
|
1139
|
+
text: `Set input value`,
|
|
985
1140
|
value: value,
|
|
986
|
-
|
|
1141
|
+
screenshotId,
|
|
987
1142
|
result: error
|
|
988
1143
|
? {
|
|
989
1144
|
status: "FAILED",
|
|
@@ -1000,7 +1155,7 @@ class StableBrowser {
|
|
|
1000
1155
|
});
|
|
1001
1156
|
}
|
|
1002
1157
|
}
|
|
1003
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1158
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1004
1159
|
this._validateSelectors(selectors);
|
|
1005
1160
|
const startTime = Date.now();
|
|
1006
1161
|
let error = null;
|
|
@@ -1012,6 +1167,7 @@ class StableBrowser {
|
|
|
1012
1167
|
info.selectors = selectors;
|
|
1013
1168
|
info.value = value;
|
|
1014
1169
|
try {
|
|
1170
|
+
value = await this._replaceWithLocalData(value, this);
|
|
1015
1171
|
let element = await this._locate(selectors, info, _params);
|
|
1016
1172
|
//insert red border around the element
|
|
1017
1173
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1020,28 +1176,51 @@ class StableBrowser {
|
|
|
1020
1176
|
try {
|
|
1021
1177
|
await element.click();
|
|
1022
1178
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1179
|
+
if (format) {
|
|
1180
|
+
value = dayjs(value).format(format);
|
|
1181
|
+
await element.fill(value);
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1185
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1186
|
+
el.value = ""; // clear input
|
|
1187
|
+
el.value = dateTimeValue;
|
|
1188
|
+
}, dateTimeValue);
|
|
1189
|
+
}
|
|
1190
|
+
if (enter) {
|
|
1191
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1192
|
+
await this.page.keyboard.press("Enter");
|
|
1193
|
+
await this.waitForPageLoad();
|
|
1194
|
+
}
|
|
1028
1195
|
}
|
|
1029
|
-
catch (
|
|
1196
|
+
catch (err) {
|
|
1030
1197
|
//await this.closeUnexpectedPopups();
|
|
1031
1198
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1032
|
-
this.logger.info("Trying again")
|
|
1199
|
+
this.logger.info("Trying again");
|
|
1200
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1033
1201
|
info.screenshotPath = screenshotPath;
|
|
1034
|
-
Object.assign(
|
|
1202
|
+
Object.assign(err, { info: info });
|
|
1035
1203
|
await element.click();
|
|
1036
1204
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1205
|
+
if (format) {
|
|
1206
|
+
value = dayjs(value).format(format);
|
|
1207
|
+
await element.fill(value);
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1211
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1212
|
+
el.value = ""; // clear input
|
|
1213
|
+
el.value = dateTimeValue;
|
|
1214
|
+
}, dateTimeValue);
|
|
1215
|
+
}
|
|
1216
|
+
if (enter) {
|
|
1217
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1218
|
+
await this.page.keyboard.press("Enter");
|
|
1219
|
+
await this.waitForPageLoad();
|
|
1220
|
+
}
|
|
1042
1221
|
}
|
|
1043
1222
|
}
|
|
1044
|
-
catch (
|
|
1223
|
+
catch (e) {
|
|
1045
1224
|
error = e;
|
|
1046
1225
|
throw e;
|
|
1047
1226
|
}
|
|
@@ -1091,20 +1270,32 @@ class StableBrowser {
|
|
|
1091
1270
|
await this.scrollIfNeeded(element, info);
|
|
1092
1271
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1093
1272
|
await this._highlightElements(element);
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1273
|
+
if (options === null || options === undefined || !options.press) {
|
|
1274
|
+
try {
|
|
1275
|
+
let currentValue = await element.inputValue();
|
|
1276
|
+
if (currentValue) {
|
|
1277
|
+
await element.fill("");
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
catch (e) {
|
|
1281
|
+
this.logger.info("unable to clear input value");
|
|
1098
1282
|
}
|
|
1099
1283
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1284
|
+
if (options === null || options === undefined || options.press) {
|
|
1285
|
+
try {
|
|
1286
|
+
await element.click({ timeout: 5000 });
|
|
1287
|
+
}
|
|
1288
|
+
catch (e) {
|
|
1289
|
+
await element.dispatchEvent("click");
|
|
1290
|
+
}
|
|
1105
1291
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1292
|
+
else {
|
|
1293
|
+
try {
|
|
1294
|
+
await element.focus();
|
|
1295
|
+
}
|
|
1296
|
+
catch (e) {
|
|
1297
|
+
await element.dispatchEvent("focus");
|
|
1298
|
+
}
|
|
1108
1299
|
}
|
|
1109
1300
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1110
1301
|
const valueSegment = _value.split("&&");
|
|
@@ -1192,7 +1383,7 @@ class StableBrowser {
|
|
|
1192
1383
|
let element = await this._locate(selectors, info, _params);
|
|
1193
1384
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1194
1385
|
await this._highlightElements(element);
|
|
1195
|
-
await element.fill(value
|
|
1386
|
+
await element.fill(value);
|
|
1196
1387
|
await element.dispatchEvent("change");
|
|
1197
1388
|
if (enter) {
|
|
1198
1389
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -1411,7 +1602,7 @@ class StableBrowser {
|
|
|
1411
1602
|
return info;
|
|
1412
1603
|
}
|
|
1413
1604
|
catch (e) {
|
|
1414
|
-
|
|
1605
|
+
await this.closeUnexpectedPopups();
|
|
1415
1606
|
this.logger.error("verify element contains text failed " + info.log);
|
|
1416
1607
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1417
1608
|
info.screenshotPath = screenshotPath;
|
|
@@ -1459,6 +1650,29 @@ class StableBrowser {
|
|
|
1459
1650
|
}
|
|
1460
1651
|
return dataFile;
|
|
1461
1652
|
}
|
|
1653
|
+
async waitForUserInput(message, world = null) {
|
|
1654
|
+
if (!message) {
|
|
1655
|
+
message = "# Wait for user input. Press any key to continue";
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
message = "# Wait for user input. " + message;
|
|
1659
|
+
}
|
|
1660
|
+
message += "\n";
|
|
1661
|
+
const value = await new Promise((resolve) => {
|
|
1662
|
+
const rl = readline.createInterface({
|
|
1663
|
+
input: process.stdin,
|
|
1664
|
+
output: process.stdout,
|
|
1665
|
+
});
|
|
1666
|
+
rl.question(message, (answer) => {
|
|
1667
|
+
rl.close();
|
|
1668
|
+
resolve(answer);
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
if (value) {
|
|
1672
|
+
this.logger.info(`{{userInput}} was set to: ${value}`);
|
|
1673
|
+
}
|
|
1674
|
+
this.setTestData({ userInput: value }, world);
|
|
1675
|
+
}
|
|
1462
1676
|
setTestData(testData, world = null) {
|
|
1463
1677
|
if (!testData) {
|
|
1464
1678
|
return;
|
|
@@ -1486,7 +1700,7 @@ class StableBrowser {
|
|
|
1486
1700
|
const data = fs.readFileSync(filePath, "utf8");
|
|
1487
1701
|
const results = [];
|
|
1488
1702
|
return new Promise((resolve, reject) => {
|
|
1489
|
-
const readableStream = new
|
|
1703
|
+
const readableStream = new Readable();
|
|
1490
1704
|
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1491
1705
|
readableStream.push(data);
|
|
1492
1706
|
readableStream.push(null);
|
|
@@ -1646,7 +1860,6 @@ class StableBrowser {
|
|
|
1646
1860
|
}
|
|
1647
1861
|
async takeScreenshot(screenshotPath) {
|
|
1648
1862
|
const playContext = this.context.playContext;
|
|
1649
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1650
1863
|
// Using CDP to capture the screenshot
|
|
1651
1864
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1652
1865
|
document.body.scrollWidth,
|
|
@@ -1656,41 +1869,40 @@ class StableBrowser {
|
|
|
1656
1869
|
document.body.clientWidth,
|
|
1657
1870
|
document.documentElement.clientWidth,
|
|
1658
1871
|
])));
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
await client.detach();
|
|
1872
|
+
let screenshotBuffer = null;
|
|
1873
|
+
if (this.context.browserName === "chromium") {
|
|
1874
|
+
const client = await playContext.newCDPSession(this.page);
|
|
1875
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
1876
|
+
format: "png",
|
|
1877
|
+
// clip: {
|
|
1878
|
+
// x: 0,
|
|
1879
|
+
// y: 0,
|
|
1880
|
+
// width: viewportWidth,
|
|
1881
|
+
// height: viewportHeight,
|
|
1882
|
+
// scale: 1,
|
|
1883
|
+
// },
|
|
1884
|
+
});
|
|
1885
|
+
await client.detach();
|
|
1886
|
+
if (!screenshotPath) {
|
|
1887
|
+
return data;
|
|
1888
|
+
}
|
|
1889
|
+
screenshotBuffer = Buffer.from(data, "base64");
|
|
1890
|
+
}
|
|
1891
|
+
else {
|
|
1892
|
+
screenshotBuffer = await this.page.screenshot();
|
|
1893
|
+
}
|
|
1894
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
1895
|
+
// Get the image dimensions
|
|
1896
|
+
const { width, height } = image.bitmap;
|
|
1897
|
+
const resizeRatio = viewportWidth / width;
|
|
1898
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
1899
|
+
if (width > viewportWidth) {
|
|
1900
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
1901
|
+
await image.write(screenshotPath);
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1905
|
+
}
|
|
1694
1906
|
}
|
|
1695
1907
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1696
1908
|
this._validateSelectors(selectors);
|
|
@@ -1861,11 +2073,14 @@ class StableBrowser {
|
|
|
1861
2073
|
//}
|
|
1862
2074
|
if ((result && result.data, result.data.status === true)) {
|
|
1863
2075
|
let codeOrUrlFound = false;
|
|
2076
|
+
let emailCode = null;
|
|
2077
|
+
let emailUrl = null;
|
|
1864
2078
|
// check if a code is returned
|
|
1865
2079
|
if (result.data.content && result.data.content.code) {
|
|
1866
2080
|
let code = result.data.content.code;
|
|
1867
2081
|
this.setTestData({ emailCode: code }, world);
|
|
1868
2082
|
this.logger.info("set test data: emailCode = " + code);
|
|
2083
|
+
emailCode = code;
|
|
1869
2084
|
codeOrUrlFound = true;
|
|
1870
2085
|
}
|
|
1871
2086
|
// check if a url is returned
|
|
@@ -1873,6 +2088,7 @@ class StableBrowser {
|
|
|
1873
2088
|
let url = result.data.content.url;
|
|
1874
2089
|
this.setTestData({ emailUrl: url }, world);
|
|
1875
2090
|
this.logger.info("set test data: emailUrl = " + url);
|
|
2091
|
+
emailUrl = url;
|
|
1876
2092
|
codeOrUrlFound = true;
|
|
1877
2093
|
}
|
|
1878
2094
|
if (codeOrUrlFound) {
|
|
@@ -2494,13 +2710,13 @@ class StableBrowser {
|
|
|
2494
2710
|
}
|
|
2495
2711
|
catch (e) {
|
|
2496
2712
|
if (e.label === "networkidle") {
|
|
2497
|
-
console.log("
|
|
2713
|
+
console.log("waited for the network to be idle timeout");
|
|
2498
2714
|
}
|
|
2499
2715
|
else if (e.label === "load") {
|
|
2500
|
-
console.log("
|
|
2716
|
+
console.log("waited for the load timeout");
|
|
2501
2717
|
}
|
|
2502
2718
|
else if (e.label === "domcontentloaded") {
|
|
2503
|
-
console.log("
|
|
2719
|
+
console.log("waited for the domcontent loaded timeout");
|
|
2504
2720
|
}
|
|
2505
2721
|
console.log(".");
|
|
2506
2722
|
}
|
|
@@ -2535,13 +2751,6 @@ class StableBrowser {
|
|
|
2535
2751
|
const info = {};
|
|
2536
2752
|
try {
|
|
2537
2753
|
await this.page.close();
|
|
2538
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2539
|
-
this.context.pages.pop();
|
|
2540
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2541
|
-
this.context.page = this.page;
|
|
2542
|
-
let title = await this.page.title();
|
|
2543
|
-
console.log("Switched to page " + title);
|
|
2544
|
-
}
|
|
2545
2754
|
}
|
|
2546
2755
|
catch (e) {
|
|
2547
2756
|
console.log(".");
|
|
@@ -2650,33 +2859,18 @@ class StableBrowser {
|
|
|
2650
2859
|
}
|
|
2651
2860
|
async scrollIfNeeded(element, info) {
|
|
2652
2861
|
try {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
if (rect &&
|
|
2656
|
-
rect.top >= 0 &&
|
|
2657
|
-
rect.left >= 0 &&
|
|
2658
|
-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
2659
|
-
rect.right <= (window.innerWidth || document.documentElement.clientWidth)) {
|
|
2660
|
-
return false;
|
|
2661
|
-
}
|
|
2662
|
-
else {
|
|
2663
|
-
node.scrollIntoView({
|
|
2664
|
-
behavior: "smooth",
|
|
2665
|
-
block: "center",
|
|
2666
|
-
inline: "center",
|
|
2667
|
-
});
|
|
2668
|
-
return true;
|
|
2669
|
-
}
|
|
2862
|
+
await element.scrollIntoViewIfNeeded({
|
|
2863
|
+
timeout: 2000,
|
|
2670
2864
|
});
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
}
|
|
2865
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2866
|
+
if (info) {
|
|
2867
|
+
info.box = await element.boundingBox({
|
|
2868
|
+
timeout: 1000,
|
|
2869
|
+
});
|
|
2676
2870
|
}
|
|
2677
2871
|
}
|
|
2678
2872
|
catch (e) {
|
|
2679
|
-
console.log("
|
|
2873
|
+
console.log("#-#");
|
|
2680
2874
|
}
|
|
2681
2875
|
}
|
|
2682
2876
|
_reportToWorld(world, properties) {
|