automation_model 1.0.395-dev → 1.0.395-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 +37 -3
- package/lib/init_browser.js.map +1 -1
- package/lib/stable_browser.d.ts +16 -4
- package/lib/stable_browser.js +342 -165
- package/lib/stable_browser.js.map +1 -1
- package/lib/test_context.d.ts +1 -0
- package/lib/test_context.js +1 -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,24 +261,78 @@ 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
|
+
}
|
|
316
|
+
}
|
|
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");
|
|
197
334
|
}
|
|
198
|
-
|
|
335
|
+
return locatorReturn;
|
|
199
336
|
}
|
|
200
337
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
201
338
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -406,6 +543,8 @@ class StableBrowser {
|
|
|
406
543
|
if (result.foundElements.length > 0) {
|
|
407
544
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
408
545
|
await dialogCloseLocator.click();
|
|
546
|
+
// wait for the dialog to close
|
|
547
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
409
548
|
return { rerun: true };
|
|
410
549
|
}
|
|
411
550
|
}
|
|
@@ -414,7 +553,7 @@ class StableBrowser {
|
|
|
414
553
|
}
|
|
415
554
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
416
555
|
for (let i = 0; i < 3; i++) {
|
|
417
|
-
info.log += "attempt " + i + ":
|
|
556
|
+
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
418
557
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
419
558
|
let selector = selectors.locators[j];
|
|
420
559
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
@@ -434,9 +573,30 @@ class StableBrowser {
|
|
|
434
573
|
//let arrayMode = Array.isArray(selectors);
|
|
435
574
|
let scope = this.page;
|
|
436
575
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
576
|
+
const findFrame = (frame, framescope) => {
|
|
577
|
+
for (let i = 0; i < frame.selectors.length; i++) {
|
|
578
|
+
let frameLocator = frame.selectors[i];
|
|
579
|
+
if (frameLocator.css) {
|
|
580
|
+
framescope = framescope.frameLocator(frameLocator.css);
|
|
581
|
+
if (frameLocator.index) {
|
|
582
|
+
framescope = framescope.nth(frameLocator.index);
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (frame.children) {
|
|
588
|
+
return findFrame(frame.children, framescope);
|
|
589
|
+
}
|
|
590
|
+
return framescope;
|
|
591
|
+
};
|
|
437
592
|
info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
|
|
438
593
|
while (true) {
|
|
439
594
|
let frameFound = false;
|
|
595
|
+
if (selectors.nestFrmLoc) {
|
|
596
|
+
scope = findFrame(selectors.nestFrmLoc, scope);
|
|
597
|
+
frameFound = true;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
440
600
|
if (selectors.frameLocators) {
|
|
441
601
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
442
602
|
let frameLocator = selectors.frameLocators[i];
|
|
@@ -600,6 +760,9 @@ class StableBrowser {
|
|
|
600
760
|
async click(selectors, _params, options = {}, world = null) {
|
|
601
761
|
this._validateSelectors(selectors);
|
|
602
762
|
const startTime = Date.now();
|
|
763
|
+
if (options && options.context) {
|
|
764
|
+
selectors.locators[0].text = options.context;
|
|
765
|
+
}
|
|
603
766
|
const info = {};
|
|
604
767
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
605
768
|
info.operation = "click";
|
|
@@ -613,14 +776,14 @@ class StableBrowser {
|
|
|
613
776
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
614
777
|
try {
|
|
615
778
|
await this._highlightElements(element);
|
|
616
|
-
await element.click(
|
|
779
|
+
await element.click();
|
|
617
780
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
618
781
|
}
|
|
619
782
|
catch (e) {
|
|
620
783
|
// await this.closeUnexpectedPopups();
|
|
621
784
|
info.log += "click failed, will try again" + "\n";
|
|
622
785
|
element = await this._locate(selectors, info, _params);
|
|
623
|
-
await element.click
|
|
786
|
+
await element.dispatchEvent("click");
|
|
624
787
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
625
788
|
}
|
|
626
789
|
await this.waitForPageLoad();
|
|
@@ -673,7 +836,7 @@ class StableBrowser {
|
|
|
673
836
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
674
837
|
try {
|
|
675
838
|
await this._highlightElements(element);
|
|
676
|
-
await element.setChecked(checked
|
|
839
|
+
await element.setChecked(checked);
|
|
677
840
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
678
841
|
}
|
|
679
842
|
catch (e) {
|
|
@@ -737,7 +900,7 @@ class StableBrowser {
|
|
|
737
900
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
738
901
|
try {
|
|
739
902
|
await this._highlightElements(element);
|
|
740
|
-
await element.hover(
|
|
903
|
+
await element.hover();
|
|
741
904
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
742
905
|
}
|
|
743
906
|
catch (e) {
|
|
@@ -799,7 +962,7 @@ class StableBrowser {
|
|
|
799
962
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
800
963
|
try {
|
|
801
964
|
await this._highlightElements(element);
|
|
802
|
-
await element.selectOption(values
|
|
965
|
+
await element.selectOption(values);
|
|
803
966
|
}
|
|
804
967
|
catch (e) {
|
|
805
968
|
//await this.closeUnexpectedPopups();
|
|
@@ -908,71 +1071,45 @@ class StableBrowser {
|
|
|
908
1071
|
});
|
|
909
1072
|
}
|
|
910
1073
|
}
|
|
911
|
-
async
|
|
1074
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1075
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
912
1076
|
this._validateSelectors(selectors);
|
|
913
1077
|
const startTime = Date.now();
|
|
914
|
-
let error = null;
|
|
915
|
-
let screenshotId = null;
|
|
916
|
-
let screenshotPath = null;
|
|
917
1078
|
const info = {};
|
|
918
|
-
info.log = "";
|
|
919
|
-
info.operation =
|
|
1079
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
1080
|
+
info.operation = "setInputValue";
|
|
920
1081
|
info.selectors = selectors;
|
|
1082
|
+
value = this._fixUsingParams(value, _params);
|
|
921
1083
|
info.value = value;
|
|
1084
|
+
let error = null;
|
|
1085
|
+
let screenshotId = null;
|
|
1086
|
+
let screenshotPath = null;
|
|
922
1087
|
try {
|
|
923
1088
|
value = await this._replaceWithLocalData(value, this);
|
|
924
1089
|
let element = await this._locate(selectors, info, _params);
|
|
925
|
-
//insert red border around the element
|
|
926
1090
|
await this.scrollIfNeeded(element, info);
|
|
927
1091
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
928
1092
|
await this._highlightElements(element);
|
|
929
1093
|
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
|
-
}
|
|
1094
|
+
await element.evaluateHandle((el, value) => {
|
|
1095
|
+
el.value = value;
|
|
1096
|
+
}, value);
|
|
948
1097
|
}
|
|
949
1098
|
catch (error) {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1099
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1100
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
953
1101
|
info.screenshotPath = screenshotPath;
|
|
954
1102
|
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
|
-
}
|
|
1103
|
+
await element.evaluateHandle((el, value) => {
|
|
1104
|
+
el.value = value;
|
|
1105
|
+
});
|
|
973
1106
|
}
|
|
974
1107
|
}
|
|
975
|
-
catch (
|
|
1108
|
+
catch (e) {
|
|
1109
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1110
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1111
|
+
info.screenshotPath = screenshotPath;
|
|
1112
|
+
Object.assign(e, { info: info });
|
|
976
1113
|
error = e;
|
|
977
1114
|
throw e;
|
|
978
1115
|
}
|
|
@@ -980,10 +1117,10 @@ class StableBrowser {
|
|
|
980
1117
|
const endTime = Date.now();
|
|
981
1118
|
this._reportToWorld(world, {
|
|
982
1119
|
element_name: selectors.element_name,
|
|
983
|
-
type: Types.
|
|
984
|
-
|
|
1120
|
+
type: Types.SET_INPUT,
|
|
1121
|
+
text: `Set input value`,
|
|
985
1122
|
value: value,
|
|
986
|
-
|
|
1123
|
+
screenshotId,
|
|
987
1124
|
result: error
|
|
988
1125
|
? {
|
|
989
1126
|
status: "FAILED",
|
|
@@ -1000,7 +1137,7 @@ class StableBrowser {
|
|
|
1000
1137
|
});
|
|
1001
1138
|
}
|
|
1002
1139
|
}
|
|
1003
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1140
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1004
1141
|
this._validateSelectors(selectors);
|
|
1005
1142
|
const startTime = Date.now();
|
|
1006
1143
|
let error = null;
|
|
@@ -1012,6 +1149,7 @@ class StableBrowser {
|
|
|
1012
1149
|
info.selectors = selectors;
|
|
1013
1150
|
info.value = value;
|
|
1014
1151
|
try {
|
|
1152
|
+
value = await this._replaceWithLocalData(value, this);
|
|
1015
1153
|
let element = await this._locate(selectors, info, _params);
|
|
1016
1154
|
//insert red border around the element
|
|
1017
1155
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1020,28 +1158,51 @@ class StableBrowser {
|
|
|
1020
1158
|
try {
|
|
1021
1159
|
await element.click();
|
|
1022
1160
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1161
|
+
if (format) {
|
|
1162
|
+
value = dayjs(value).format(format);
|
|
1163
|
+
await element.fill(value);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1167
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1168
|
+
el.value = ""; // clear input
|
|
1169
|
+
el.value = dateTimeValue;
|
|
1170
|
+
}, dateTimeValue);
|
|
1171
|
+
}
|
|
1172
|
+
if (enter) {
|
|
1173
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1174
|
+
await this.page.keyboard.press("Enter");
|
|
1175
|
+
await this.waitForPageLoad();
|
|
1176
|
+
}
|
|
1028
1177
|
}
|
|
1029
|
-
catch (
|
|
1178
|
+
catch (err) {
|
|
1030
1179
|
//await this.closeUnexpectedPopups();
|
|
1031
1180
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1032
|
-
this.logger.info("Trying again")
|
|
1181
|
+
this.logger.info("Trying again");
|
|
1182
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1033
1183
|
info.screenshotPath = screenshotPath;
|
|
1034
|
-
Object.assign(
|
|
1184
|
+
Object.assign(err, { info: info });
|
|
1035
1185
|
await element.click();
|
|
1036
1186
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1187
|
+
if (format) {
|
|
1188
|
+
value = dayjs(value).format(format);
|
|
1189
|
+
await element.fill(value);
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1193
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1194
|
+
el.value = ""; // clear input
|
|
1195
|
+
el.value = dateTimeValue;
|
|
1196
|
+
}, dateTimeValue);
|
|
1197
|
+
}
|
|
1198
|
+
if (enter) {
|
|
1199
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1200
|
+
await this.page.keyboard.press("Enter");
|
|
1201
|
+
await this.waitForPageLoad();
|
|
1202
|
+
}
|
|
1042
1203
|
}
|
|
1043
1204
|
}
|
|
1044
|
-
catch (
|
|
1205
|
+
catch (e) {
|
|
1045
1206
|
error = e;
|
|
1046
1207
|
throw e;
|
|
1047
1208
|
}
|
|
@@ -1091,20 +1252,32 @@ class StableBrowser {
|
|
|
1091
1252
|
await this.scrollIfNeeded(element, info);
|
|
1092
1253
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1093
1254
|
await this._highlightElements(element);
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1255
|
+
if (options === null || options === undefined || !options.press) {
|
|
1256
|
+
try {
|
|
1257
|
+
let currentValue = await element.inputValue();
|
|
1258
|
+
if (currentValue) {
|
|
1259
|
+
await element.fill("");
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
catch (e) {
|
|
1263
|
+
this.logger.info("unable to clear input value");
|
|
1098
1264
|
}
|
|
1099
1265
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1266
|
+
if (options === null || options === undefined || options.press) {
|
|
1267
|
+
try {
|
|
1268
|
+
await element.click({ timeout: 5000 });
|
|
1269
|
+
}
|
|
1270
|
+
catch (e) {
|
|
1271
|
+
await element.dispatchEvent("click");
|
|
1272
|
+
}
|
|
1105
1273
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1274
|
+
else {
|
|
1275
|
+
try {
|
|
1276
|
+
await element.focus();
|
|
1277
|
+
}
|
|
1278
|
+
catch (e) {
|
|
1279
|
+
await element.dispatchEvent("focus");
|
|
1280
|
+
}
|
|
1108
1281
|
}
|
|
1109
1282
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1110
1283
|
const valueSegment = _value.split("&&");
|
|
@@ -1192,7 +1365,7 @@ class StableBrowser {
|
|
|
1192
1365
|
let element = await this._locate(selectors, info, _params);
|
|
1193
1366
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1194
1367
|
await this._highlightElements(element);
|
|
1195
|
-
await element.fill(value
|
|
1368
|
+
await element.fill(value);
|
|
1196
1369
|
await element.dispatchEvent("change");
|
|
1197
1370
|
if (enter) {
|
|
1198
1371
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -1411,7 +1584,7 @@ class StableBrowser {
|
|
|
1411
1584
|
return info;
|
|
1412
1585
|
}
|
|
1413
1586
|
catch (e) {
|
|
1414
|
-
|
|
1587
|
+
await this.closeUnexpectedPopups();
|
|
1415
1588
|
this.logger.error("verify element contains text failed " + info.log);
|
|
1416
1589
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1417
1590
|
info.screenshotPath = screenshotPath;
|
|
@@ -1459,6 +1632,29 @@ class StableBrowser {
|
|
|
1459
1632
|
}
|
|
1460
1633
|
return dataFile;
|
|
1461
1634
|
}
|
|
1635
|
+
async waitForUserInput(message, world = null) {
|
|
1636
|
+
if (!message) {
|
|
1637
|
+
message = "# Wait for user input. Press any key to continue";
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
message = "# Wait for user input. " + message;
|
|
1641
|
+
}
|
|
1642
|
+
message += "\n";
|
|
1643
|
+
const value = await new Promise((resolve) => {
|
|
1644
|
+
const rl = readline.createInterface({
|
|
1645
|
+
input: process.stdin,
|
|
1646
|
+
output: process.stdout,
|
|
1647
|
+
});
|
|
1648
|
+
rl.question(message, (answer) => {
|
|
1649
|
+
rl.close();
|
|
1650
|
+
resolve(answer);
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
if (value) {
|
|
1654
|
+
this.logger.info(`{{userInput}} was set to: ${value}`);
|
|
1655
|
+
}
|
|
1656
|
+
this.setTestData({ userInput: value }, world);
|
|
1657
|
+
}
|
|
1462
1658
|
setTestData(testData, world = null) {
|
|
1463
1659
|
if (!testData) {
|
|
1464
1660
|
return;
|
|
@@ -1486,7 +1682,7 @@ class StableBrowser {
|
|
|
1486
1682
|
const data = fs.readFileSync(filePath, "utf8");
|
|
1487
1683
|
const results = [];
|
|
1488
1684
|
return new Promise((resolve, reject) => {
|
|
1489
|
-
const readableStream = new
|
|
1685
|
+
const readableStream = new Readable();
|
|
1490
1686
|
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1491
1687
|
readableStream.push(data);
|
|
1492
1688
|
readableStream.push(null);
|
|
@@ -1666,30 +1862,29 @@ class StableBrowser {
|
|
|
1666
1862
|
])));
|
|
1667
1863
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1668
1864
|
format: "png",
|
|
1669
|
-
clip: {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
},
|
|
1865
|
+
// clip: {
|
|
1866
|
+
// x: 0,
|
|
1867
|
+
// y: 0,
|
|
1868
|
+
// width: viewportWidth,
|
|
1869
|
+
// height: viewportHeight,
|
|
1870
|
+
// scale: 1,
|
|
1871
|
+
// },
|
|
1676
1872
|
});
|
|
1677
1873
|
if (!screenshotPath) {
|
|
1678
1874
|
return data;
|
|
1679
1875
|
}
|
|
1680
1876
|
let screenshotBuffer = Buffer.from(data, "base64");
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
}
|
|
1692
|
-
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1877
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
1878
|
+
// Get the image dimensions
|
|
1879
|
+
const { width, height } = image.bitmap;
|
|
1880
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
1881
|
+
if (width > viewportWidth || height > viewportHeight) {
|
|
1882
|
+
image = image.resize({ w: viewportWidth, h: viewportHeight }); // Resize the image while maintaining aspect ratio
|
|
1883
|
+
await image.write(screenshotPath);
|
|
1884
|
+
}
|
|
1885
|
+
else {
|
|
1886
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1887
|
+
}
|
|
1693
1888
|
await client.detach();
|
|
1694
1889
|
}
|
|
1695
1890
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
@@ -1861,18 +2056,22 @@ class StableBrowser {
|
|
|
1861
2056
|
//}
|
|
1862
2057
|
if ((result && result.data, result.data.status === true)) {
|
|
1863
2058
|
let codeOrUrlFound = false;
|
|
2059
|
+
let emailCode = null;
|
|
2060
|
+
let emailUrl = null;
|
|
1864
2061
|
// check if a code is returned
|
|
1865
2062
|
if (result.data.content && result.data.content.code) {
|
|
1866
2063
|
let code = result.data.content.code;
|
|
1867
2064
|
this.setTestData({ emailCode: code }, world);
|
|
1868
|
-
this.logger.info("set test data: emailCode=" + code);
|
|
2065
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
2066
|
+
emailCode = code;
|
|
1869
2067
|
codeOrUrlFound = true;
|
|
1870
2068
|
}
|
|
1871
2069
|
// check if a url is returned
|
|
1872
2070
|
if (result.data.content && result.data.content.url) {
|
|
1873
2071
|
let url = result.data.content.url;
|
|
1874
2072
|
this.setTestData({ emailUrl: url }, world);
|
|
1875
|
-
this.logger.info("set test data: emailUrl=" + url);
|
|
2073
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
2074
|
+
emailUrl = url;
|
|
1876
2075
|
codeOrUrlFound = true;
|
|
1877
2076
|
}
|
|
1878
2077
|
if (codeOrUrlFound) {
|
|
@@ -2494,13 +2693,13 @@ class StableBrowser {
|
|
|
2494
2693
|
}
|
|
2495
2694
|
catch (e) {
|
|
2496
2695
|
if (e.label === "networkidle") {
|
|
2497
|
-
console.log("
|
|
2696
|
+
console.log("waited for the network to be idle timeout");
|
|
2498
2697
|
}
|
|
2499
2698
|
else if (e.label === "load") {
|
|
2500
|
-
console.log("
|
|
2699
|
+
console.log("waited for the load timeout");
|
|
2501
2700
|
}
|
|
2502
2701
|
else if (e.label === "domcontentloaded") {
|
|
2503
|
-
console.log("
|
|
2702
|
+
console.log("waited for the domcontent loaded timeout");
|
|
2504
2703
|
}
|
|
2505
2704
|
console.log(".");
|
|
2506
2705
|
}
|
|
@@ -2535,13 +2734,6 @@ class StableBrowser {
|
|
|
2535
2734
|
const info = {};
|
|
2536
2735
|
try {
|
|
2537
2736
|
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
2737
|
}
|
|
2546
2738
|
catch (e) {
|
|
2547
2739
|
console.log(".");
|
|
@@ -2650,33 +2842,18 @@ class StableBrowser {
|
|
|
2650
2842
|
}
|
|
2651
2843
|
async scrollIfNeeded(element, info) {
|
|
2652
2844
|
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
|
-
}
|
|
2845
|
+
await element.scrollIntoViewIfNeeded({
|
|
2846
|
+
timeout: 2000,
|
|
2670
2847
|
});
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
}
|
|
2848
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2849
|
+
if (info) {
|
|
2850
|
+
info.box = await element.boundingBox({
|
|
2851
|
+
timeout: 1000,
|
|
2852
|
+
});
|
|
2676
2853
|
}
|
|
2677
2854
|
}
|
|
2678
2855
|
catch (e) {
|
|
2679
|
-
console.log("
|
|
2856
|
+
console.log("#-#");
|
|
2680
2857
|
}
|
|
2681
2858
|
}
|
|
2682
2859
|
_reportToWorld(world, properties) {
|