automation_model 1.0.408-dev → 1.0.408-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.
@@ -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";
@@ -14,6 +14,8 @@ import objectPath from "object-path";
14
14
  import { decrypt } from "./utils.js";
15
15
  import csv from "csv-parser";
16
16
  import { Readable } from "node:stream";
17
+ import readline from "readline";
18
+ import { getContext } from "./init_browser.js";
17
19
  const Types = {
18
20
  CLICK: "click_element",
19
21
  NAVIGATE: "navigate",
@@ -41,15 +43,19 @@ const Types = {
41
43
  LOAD_DATA: "load_data",
42
44
  SET_INPUT: "set_input",
43
45
  };
46
+ export const apps = {};
44
47
  class StableBrowser {
45
- constructor(browser, page, logger = null, context = null) {
48
+ constructor(browser, page, logger = null, context = null, world = null) {
46
49
  this.browser = browser;
47
50
  this.page = page;
48
51
  this.logger = logger;
49
52
  this.context = context;
53
+ this.world = world;
50
54
  this.project_path = null;
51
55
  this.webLogFile = null;
56
+ this.networkLogger = null;
52
57
  this.configuration = null;
58
+ this.appName = "main";
53
59
  if (!this.logger) {
54
60
  this.logger = console;
55
61
  }
@@ -75,23 +81,34 @@ class StableBrowser {
75
81
  this.logger.error("unable to read ai_config.json");
76
82
  }
77
83
  const logFolder = path.join(this.project_path, "logs", "web");
78
- this.webLogFile = this.getWebLogFile(logFolder);
79
- this.registerConsoleLogListener(page, context, this.webLogFile);
80
- this.registerRequestListener();
84
+ this.world = world;
81
85
  context.pages = [this.page];
82
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
+ }
83
95
  context.playContext.on("page", async function (page) {
84
96
  context.pageLoading.status = true;
85
97
  this.page = page;
86
98
  context.page = page;
87
99
  context.pages.push(page);
88
100
  page.on("close", async () => {
89
- if (this.context && this.context.pages && this.context.pages.length > 0) {
101
+ if (this.context && this.context.pages && this.context.pages.length > 1) {
90
102
  this.context.pages.pop();
91
103
  this.page = this.context.pages[this.context.pages.length - 1];
92
104
  this.context.page = this.page;
93
- let title = await this.page.title();
94
- console.log("Switched to page " + title);
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
+ }
95
112
  }
96
113
  });
97
114
  try {
@@ -104,6 +121,36 @@ class StableBrowser {
104
121
  context.pageLoading.status = false;
105
122
  }.bind(this));
106
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
+ }
107
154
  getWebLogFile(logFolder) {
108
155
  if (!fs.existsSync(logFolder)) {
109
156
  fs.mkdirSync(logFolder, { recursive: true });
@@ -115,37 +162,63 @@ class StableBrowser {
115
162
  const fileName = nextIndex + ".json";
116
163
  return path.join(logFolder, fileName);
117
164
  }
118
- registerConsoleLogListener(page, context, logFile) {
165
+ registerConsoleLogListener(page, context) {
119
166
  if (!this.context.webLogger) {
120
167
  this.context.webLogger = [];
121
168
  }
122
169
  page.on("console", async (msg) => {
123
- this.context.webLogger.push({
170
+ var _a;
171
+ const obj = {
124
172
  type: msg.type(),
125
173
  text: msg.text(),
126
174
  location: msg.location(),
127
175
  time: new Date().toISOString(),
128
- });
129
- await fs.promises.writeFile(logFile, JSON.stringify(this.context.webLogger, null, 2));
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" });
130
179
  });
131
180
  }
132
- registerRequestListener() {
133
- this.page.on("request", async (data) => {
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();
134
188
  try {
135
- const pageUrl = new URL(this.page.url());
189
+ const pageUrl = new URL(page.url());
136
190
  const requestUrl = new URL(data.url());
137
191
  if (pageUrl.hostname === requestUrl.hostname) {
138
192
  const method = data.method();
139
- if (method === "POST" || method === "GET" || method === "PUT" || method === "DELETE" || method === "PATCH") {
193
+ if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
140
194
  const token = await data.headerValue("Authorization");
141
195
  if (token) {
142
- this.context.authtoken = token;
196
+ context.authtoken = token;
143
197
  }
144
198
  }
145
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" });
146
212
  }
147
213
  catch (error) {
148
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));
149
222
  }
150
223
  });
151
224
  }
@@ -215,9 +288,7 @@ class StableBrowser {
215
288
  }
216
289
  _getLocator(locator, scope, _params) {
217
290
  locator = this._fixLocatorUsingParams(locator, _params);
218
- if (locator.type === "pw_selector") {
219
- return scope.locator(locator.selector);
220
- }
291
+ let locatorReturn;
221
292
  if (locator.role) {
222
293
  if (locator.role[1].nameReg) {
223
294
  locator.role[1].name = reg_parser(locator.role[1].nameReg);
@@ -226,22 +297,47 @@ class StableBrowser {
226
297
  // if (locator.role[1].name) {
227
298
  // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
228
299
  // }
229
- return scope.getByRole(locator.role[0], locator.role[1]);
300
+ locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
230
301
  }
231
302
  if (locator.css) {
232
- return scope.locator(locator.css);
303
+ locatorReturn = scope.locator(locator.css);
233
304
  }
234
- if ((locator === null || locator === void 0 ? void 0 : locator.engine) && (locator === null || locator === void 0 ? void 0 : locator.score) <= 520) {
235
- let selector = locator.selector.replace(/"/g, '\\"');
236
- if (locator.engine === "internal:att") {
237
- selector = `[${selector}]`;
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}`);
238
329
  }
239
- const locator = scope.locator(`${locator.engine}="${selector}"`);
240
- return locator;
241
330
  }
242
- throw new Error("unknown locator type");
331
+ if (!locatorReturn) {
332
+ console.error(locator);
333
+ throw new Error("Locator undefined");
334
+ }
335
+ return locatorReturn;
243
336
  }
244
337
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
338
+ if (css && css.locator) {
339
+ css = css.locator;
340
+ }
245
341
  let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
246
342
  if (result.elementCount === 0) {
247
343
  return;
@@ -450,6 +546,8 @@ class StableBrowser {
450
546
  if (result.foundElements.length > 0) {
451
547
  let dialogCloseLocator = result.foundElements[0].locator;
452
548
  await dialogCloseLocator.click();
549
+ // wait for the dialog to close
550
+ await dialogCloseLocator.waitFor({ state: "hidden" });
453
551
  return { rerun: true };
454
552
  }
455
553
  }
@@ -458,7 +556,7 @@ class StableBrowser {
458
556
  }
459
557
  async _locate(selectors, info, _params, timeout = 30000) {
460
558
  for (let i = 0; i < 3; i++) {
461
- info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
559
+ info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
462
560
  for (let j = 0; j < selectors.locators.length; j++) {
463
561
  let selector = selectors.locators[j];
464
562
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
@@ -478,9 +576,30 @@ class StableBrowser {
478
576
  //let arrayMode = Array.isArray(selectors);
479
577
  let scope = this.page;
480
578
  if (selectors.iframe_src || selectors.frameLocators) {
579
+ const findFrame = (frame, framescope) => {
580
+ for (let i = 0; i < frame.selectors.length; i++) {
581
+ let frameLocator = frame.selectors[i];
582
+ if (frameLocator.css) {
583
+ framescope = framescope.frameLocator(frameLocator.css);
584
+ if (frameLocator.index) {
585
+ framescope = framescope.nth(frameLocator.index);
586
+ }
587
+ break;
588
+ }
589
+ }
590
+ if (frame.children) {
591
+ return findFrame(frame.children, framescope);
592
+ }
593
+ return framescope;
594
+ };
481
595
  info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
482
596
  while (true) {
483
597
  let frameFound = false;
598
+ if (selectors.nestFrmLoc) {
599
+ scope = findFrame(selectors.nestFrmLoc, scope);
600
+ frameFound = true;
601
+ break;
602
+ }
484
603
  if (selectors.frameLocators) {
485
604
  for (let i = 0; i < selectors.frameLocators.length; i++) {
486
605
  let frameLocator = selectors.frameLocators[i];
@@ -644,6 +763,9 @@ class StableBrowser {
644
763
  async click(selectors, _params, options = {}, world = null) {
645
764
  this._validateSelectors(selectors);
646
765
  const startTime = Date.now();
766
+ if (options && options.context) {
767
+ selectors.locators[0].text = options.context;
768
+ }
647
769
  const info = {};
648
770
  info.log = "***** click on " + selectors.element_name + " *****\n";
649
771
  info.operation = "click";
@@ -657,14 +779,14 @@ class StableBrowser {
657
779
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
658
780
  try {
659
781
  await this._highlightElements(element);
660
- await element.click({ timeout: 5000 });
782
+ await element.click();
661
783
  await new Promise((resolve) => setTimeout(resolve, 1000));
662
784
  }
663
785
  catch (e) {
664
786
  // await this.closeUnexpectedPopups();
665
787
  info.log += "click failed, will try again" + "\n";
666
788
  element = await this._locate(selectors, info, _params);
667
- await element.click({ timeout: 10000, force: true });
789
+ await element.dispatchEvent("click");
668
790
  await new Promise((resolve) => setTimeout(resolve, 1000));
669
791
  }
670
792
  await this.waitForPageLoad();
@@ -717,7 +839,7 @@ class StableBrowser {
717
839
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
718
840
  try {
719
841
  await this._highlightElements(element);
720
- await element.setChecked(checked, { timeout: 5000 });
842
+ await element.setChecked(checked);
721
843
  await new Promise((resolve) => setTimeout(resolve, 1000));
722
844
  }
723
845
  catch (e) {
@@ -781,7 +903,7 @@ class StableBrowser {
781
903
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
782
904
  try {
783
905
  await this._highlightElements(element);
784
- await element.hover({ timeout: 10000 });
906
+ await element.hover();
785
907
  await new Promise((resolve) => setTimeout(resolve, 1000));
786
908
  }
787
909
  catch (e) {
@@ -843,7 +965,7 @@ class StableBrowser {
843
965
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
844
966
  try {
845
967
  await this._highlightElements(element);
846
- await element.selectOption(values, { timeout: 5000 });
968
+ await element.selectOption(values);
847
969
  }
848
970
  catch (e) {
849
971
  //await this.closeUnexpectedPopups();
@@ -1246,7 +1368,7 @@ class StableBrowser {
1246
1368
  let element = await this._locate(selectors, info, _params);
1247
1369
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1248
1370
  await this._highlightElements(element);
1249
- await element.fill(value, { timeout: 10000 });
1371
+ await element.fill(value);
1250
1372
  await element.dispatchEvent("change");
1251
1373
  if (enter) {
1252
1374
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -1465,7 +1587,7 @@ class StableBrowser {
1465
1587
  return info;
1466
1588
  }
1467
1589
  catch (e) {
1468
- //await this.closeUnexpectedPopups();
1590
+ await this.closeUnexpectedPopups();
1469
1591
  this.logger.error("verify element contains text failed " + info.log);
1470
1592
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1471
1593
  info.screenshotPath = screenshotPath;
@@ -1513,6 +1635,29 @@ class StableBrowser {
1513
1635
  }
1514
1636
  return dataFile;
1515
1637
  }
1638
+ async waitForUserInput(message, world = null) {
1639
+ if (!message) {
1640
+ message = "# Wait for user input. Press any key to continue";
1641
+ }
1642
+ else {
1643
+ message = "# Wait for user input. " + message;
1644
+ }
1645
+ message += "\n";
1646
+ const value = await new Promise((resolve) => {
1647
+ const rl = readline.createInterface({
1648
+ input: process.stdin,
1649
+ output: process.stdout,
1650
+ });
1651
+ rl.question(message, (answer) => {
1652
+ rl.close();
1653
+ resolve(answer);
1654
+ });
1655
+ });
1656
+ if (value) {
1657
+ this.logger.info(`{{userInput}} was set to: ${value}`);
1658
+ }
1659
+ this.setTestData({ userInput: value }, world);
1660
+ }
1516
1661
  setTestData(testData, world = null) {
1517
1662
  if (!testData) {
1518
1663
  return;
@@ -1720,30 +1865,29 @@ class StableBrowser {
1720
1865
  ])));
1721
1866
  const { data } = await client.send("Page.captureScreenshot", {
1722
1867
  format: "png",
1723
- clip: {
1724
- x: 0,
1725
- y: 0,
1726
- width: viewportWidth,
1727
- height: viewportHeight,
1728
- scale: 1,
1729
- },
1868
+ // clip: {
1869
+ // x: 0,
1870
+ // y: 0,
1871
+ // width: viewportWidth,
1872
+ // height: viewportHeight,
1873
+ // scale: 1,
1874
+ // },
1730
1875
  });
1731
1876
  if (!screenshotPath) {
1732
1877
  return data;
1733
1878
  }
1734
1879
  let screenshotBuffer = Buffer.from(data, "base64");
1735
- const sharpBuffer = sharp(screenshotBuffer);
1736
- const metadata = await sharpBuffer.metadata();
1737
- //check if you are on retina display and reduce the quality of the image
1738
- if (metadata.width > viewportWidth || metadata.height > viewportHeight) {
1739
- screenshotBuffer = await sharpBuffer
1740
- .resize(viewportWidth, viewportHeight, {
1741
- fit: sharp.fit.inside,
1742
- withoutEnlargement: true,
1743
- })
1744
- .toBuffer();
1745
- }
1746
- fs.writeFileSync(screenshotPath, screenshotBuffer);
1880
+ let image = await Jimp.read(screenshotBuffer);
1881
+ // Get the image dimensions
1882
+ const { width, height } = image.bitmap;
1883
+ // Resize the image to fit within the viewport dimensions without enlarging
1884
+ if (width > viewportWidth || height > viewportHeight) {
1885
+ image = image.resize({ w: viewportWidth, h: viewportHeight }); // Resize the image while maintaining aspect ratio
1886
+ await image.write(screenshotPath);
1887
+ }
1888
+ else {
1889
+ fs.writeFileSync(screenshotPath, screenshotBuffer);
1890
+ }
1747
1891
  await client.detach();
1748
1892
  }
1749
1893
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
@@ -2552,13 +2696,13 @@ class StableBrowser {
2552
2696
  }
2553
2697
  catch (e) {
2554
2698
  if (e.label === "networkidle") {
2555
- console.log("waitted for the network to be idle timeout");
2699
+ console.log("waited for the network to be idle timeout");
2556
2700
  }
2557
2701
  else if (e.label === "load") {
2558
- console.log("waitted for the load timeout");
2702
+ console.log("waited for the load timeout");
2559
2703
  }
2560
2704
  else if (e.label === "domcontentloaded") {
2561
- console.log("waitted for the domcontent loaded timeout");
2705
+ console.log("waited for the domcontent loaded timeout");
2562
2706
  }
2563
2707
  console.log(".");
2564
2708
  }
@@ -2701,33 +2845,18 @@ class StableBrowser {
2701
2845
  }
2702
2846
  async scrollIfNeeded(element, info) {
2703
2847
  try {
2704
- let didScroll = await element.evaluate((node) => {
2705
- const rect = node.getBoundingClientRect();
2706
- if (rect &&
2707
- rect.top >= 0 &&
2708
- rect.left >= 0 &&
2709
- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
2710
- rect.right <= (window.innerWidth || document.documentElement.clientWidth)) {
2711
- return false;
2712
- }
2713
- else {
2714
- node.scrollIntoView({
2715
- behavior: "smooth",
2716
- block: "center",
2717
- inline: "center",
2718
- });
2719
- return true;
2720
- }
2848
+ await element.scrollIntoViewIfNeeded({
2849
+ timeout: 2000,
2721
2850
  });
2722
- if (didScroll) {
2723
- await new Promise((resolve) => setTimeout(resolve, 500));
2724
- if (info) {
2725
- info.box = await element.boundingBox();
2726
- }
2851
+ await new Promise((resolve) => setTimeout(resolve, 500));
2852
+ if (info) {
2853
+ info.box = await element.boundingBox({
2854
+ timeout: 1000,
2855
+ });
2727
2856
  }
2728
2857
  }
2729
2858
  catch (e) {
2730
- console.log("scroll failed");
2859
+ console.log("#-#");
2731
2860
  }
2732
2861
  }
2733
2862
  _reportToWorld(world, properties) {