automation_model 1.0.442-dev → 1.0.442

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.
Files changed (71) hide show
  1. package/README.md +130 -0
  2. package/lib/analyze_helper.js.map +1 -1
  3. package/lib/api.d.ts +43 -2
  4. package/lib/api.js +240 -42
  5. package/lib/api.js.map +1 -1
  6. package/lib/auto_page.d.ts +5 -2
  7. package/lib/auto_page.js +215 -49
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.d.ts +7 -3
  10. package/lib/browser_manager.js +172 -48
  11. package/lib/browser_manager.js.map +1 -1
  12. package/lib/bruno.d.ts +2 -0
  13. package/lib/bruno.js +381 -0
  14. package/lib/bruno.js.map +1 -0
  15. package/lib/command_common.d.ts +6 -0
  16. package/lib/command_common.js +202 -0
  17. package/lib/command_common.js.map +1 -0
  18. package/lib/date_time.js.map +1 -1
  19. package/lib/drawRect.js.map +1 -1
  20. package/lib/environment.d.ts +4 -0
  21. package/lib/environment.js +6 -2
  22. package/lib/environment.js.map +1 -1
  23. package/lib/error-messages.d.ts +6 -0
  24. package/lib/error-messages.js +206 -0
  25. package/lib/error-messages.js.map +1 -0
  26. package/lib/file_checker.d.ts +1 -0
  27. package/lib/file_checker.js +61 -0
  28. package/lib/file_checker.js.map +1 -0
  29. package/lib/find_function.js.map +1 -1
  30. package/lib/generation_scripts.d.ts +4 -0
  31. package/lib/generation_scripts.js +2 -0
  32. package/lib/generation_scripts.js.map +1 -0
  33. package/lib/index.d.ts +3 -0
  34. package/lib/index.js +3 -0
  35. package/lib/index.js.map +1 -1
  36. package/lib/init_browser.d.ts +5 -2
  37. package/lib/init_browser.js +128 -11
  38. package/lib/init_browser.js.map +1 -1
  39. package/lib/locate_element.d.ts +7 -0
  40. package/lib/locate_element.js +215 -0
  41. package/lib/locate_element.js.map +1 -0
  42. package/lib/locator.d.ts +36 -0
  43. package/lib/locator.js +165 -0
  44. package/lib/locator.js.map +1 -1
  45. package/lib/locator_log.d.ts +26 -0
  46. package/lib/locator_log.js +69 -0
  47. package/lib/locator_log.js.map +1 -0
  48. package/lib/network.d.ts +3 -0
  49. package/lib/network.js +183 -0
  50. package/lib/network.js.map +1 -0
  51. package/lib/scripts/axe.mini.js +12 -0
  52. package/lib/snapshot_validation.d.ts +37 -0
  53. package/lib/snapshot_validation.js +357 -0
  54. package/lib/snapshot_validation.js.map +1 -0
  55. package/lib/stable_browser.d.ts +140 -56
  56. package/lib/stable_browser.js +2169 -1303
  57. package/lib/stable_browser.js.map +1 -1
  58. package/lib/table.d.ts +13 -0
  59. package/lib/table.js +187 -0
  60. package/lib/table.js.map +1 -0
  61. package/lib/table_analyze.js.map +1 -1
  62. package/lib/table_helper.d.ts +19 -0
  63. package/lib/table_helper.js +116 -0
  64. package/lib/table_helper.js.map +1 -0
  65. package/lib/test_context.d.ts +7 -0
  66. package/lib/test_context.js +15 -10
  67. package/lib/test_context.js.map +1 -1
  68. package/lib/utils.d.ts +22 -2
  69. package/lib/utils.js +672 -11
  70. package/lib/utils.js.map +1 -1
  71. package/package.json +19 -12
@@ -2,33 +2,47 @@
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 objectPath from "object-path";
14
- import { decrypt } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
15
14
  import csv from "csv-parser";
16
15
  import { Readable } from "node:stream";
17
16
  import readline from "readline";
18
- const Types = {
17
+ import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
19
+ import { locate_element } from "./locate_element.js";
20
+ import { randomUUID } from "crypto";
21
+ import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
22
+ import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
23
+ import { LocatorLog } from "./locator_log.js";
24
+ import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { snapshotValidation } from "./snapshot_validation.js";
27
+ import { loadBrunoParams } from "./bruno.js";
28
+ export const Types = {
19
29
  CLICK: "click_element",
20
- NAVIGATE: "navigate",
30
+ WAIT_ELEMENT: "wait_element",
31
+ NAVIGATE: "navigate", ///
21
32
  FILL: "fill_element",
22
- EXECUTE: "execute_page_method",
23
- OPEN: "open_environment",
33
+ EXECUTE: "execute_page_method", //
34
+ OPEN: "open_environment", //
24
35
  COMPLETE: "step_complete",
25
36
  ASK: "information_needed",
26
- GET_PAGE_STATUS: "get_page_status",
27
- CLICK_ROW_ACTION: "click_row_action",
37
+ GET_PAGE_STATUS: "get_page_status", ///
38
+ CLICK_ROW_ACTION: "click_row_action", //
28
39
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
40
+ VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
41
+ VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
29
42
  ANALYZE_TABLE: "analyze_table",
30
- SELECT: "select_combobox",
43
+ SELECT: "select_combobox", //
31
44
  VERIFY_PAGE_PATH: "verify_page_path",
45
+ VERIFY_PAGE_TITLE: "verify_page_title",
32
46
  TYPE_PRESS: "type_press",
33
47
  PRESS: "press_key",
34
48
  HOVER: "hover_element",
@@ -36,21 +50,46 @@ const Types = {
36
50
  UNCHECK: "uncheck_element",
37
51
  EXTRACT: "extract_attribute",
38
52
  CLOSE_PAGE: "close_page",
53
+ TABLE_OPERATION: "table_operation",
39
54
  SET_DATE_TIME: "set_date_time",
40
55
  SET_VIEWPORT: "set_viewport",
41
56
  VERIFY_VISUAL: "verify_visual",
42
57
  LOAD_DATA: "load_data",
43
58
  SET_INPUT: "set_input",
59
+ WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
60
+ VERIFY_ATTRIBUTE: "verify_element_attribute",
61
+ VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
62
+ BRUNO: "bruno",
63
+ VERIFY_FILE_EXISTS: "verify_file_exists",
64
+ SET_INPUT_FILES: "set_input_files",
65
+ SNAPSHOT_VALIDATION: "snapshot_validation",
66
+ REPORT_COMMAND: "report_command",
67
+ STEP_COMPLETE: "step_complete",
68
+ };
69
+ export const apps = {};
70
+ const formatElementName = (elementName) => {
71
+ return elementName ? JSON.stringify(elementName) : "element";
44
72
  };
45
73
  class StableBrowser {
46
- constructor(browser, page, logger = null, context = null) {
74
+ browser;
75
+ page;
76
+ logger;
77
+ context;
78
+ world;
79
+ project_path = null;
80
+ webLogFile = null;
81
+ networkLogger = null;
82
+ configuration = null;
83
+ appName = "main";
84
+ tags = null;
85
+ isRecording = false;
86
+ initSnapshotTaken = false;
87
+ constructor(browser, page, logger = null, context = null, world = null) {
47
88
  this.browser = browser;
48
89
  this.page = page;
49
90
  this.logger = logger;
50
91
  this.context = context;
51
- this.project_path = null;
52
- this.webLogFile = null;
53
- this.configuration = null;
92
+ this.world = world;
54
93
  if (!this.logger) {
55
94
  this.logger = console;
56
95
  }
@@ -75,17 +114,43 @@ class StableBrowser {
75
114
  catch (e) {
76
115
  this.logger.error("unable to read ai_config.json");
77
116
  }
78
- const logFolder = path.join(this.project_path, "logs", "web");
79
- this.webLogFile = this.getWebLogFile(logFolder);
80
- this.registerConsoleLogListener(page, context, this.webLogFile);
81
- this.registerRequestListener();
82
- context.pages = [this.page];
83
117
  context.pageLoading = { status: false };
118
+ context.pages = [this.page];
119
+ const logFolder = path.join(this.project_path, "logs", "web");
120
+ this.world = world;
121
+ this.registerEventListeners(this.context);
122
+ registerNetworkEvents(this.world, this, this.context, this.page);
123
+ registerDownloadEvent(this.page, this.world, this.context);
124
+ }
125
+ registerEventListeners(context) {
126
+ this.registerConsoleLogListener(this.page, context);
127
+ // this.registerRequestListener(this.page, context, this.webLogFile);
128
+ if (!context.pageLoading) {
129
+ context.pageLoading = { status: false };
130
+ }
131
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
132
+ this.page.on("dialog", (dialog) => dialog.accept());
133
+ }
84
134
  context.playContext.on("page", async function (page) {
135
+ if (this.configuration && this.configuration.closePopups === true) {
136
+ console.log("close unexpected popups");
137
+ await page.close();
138
+ return;
139
+ }
85
140
  context.pageLoading.status = true;
86
141
  this.page = page;
142
+ try {
143
+ if (this.configuration && this.configuration.acceptDialog) {
144
+ await page.on("dialog", (dialog) => dialog.accept());
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.error("Error on dialog accept registration", error);
149
+ }
87
150
  context.page = page;
88
151
  context.pages.push(page);
152
+ registerNetworkEvents(this.world, this, context, this.page);
153
+ registerDownloadEvent(this.page, this.world, context);
89
154
  page.on("close", async () => {
90
155
  if (this.context && this.context.pages && this.context.pages.length > 1) {
91
156
  this.context.pages.pop();
@@ -110,117 +175,165 @@ class StableBrowser {
110
175
  context.pageLoading.status = false;
111
176
  }.bind(this));
112
177
  }
113
- getWebLogFile(logFolder) {
114
- if (!fs.existsSync(logFolder)) {
115
- fs.mkdirSync(logFolder, { recursive: true });
178
+ async switchApp(appName) {
179
+ // check if the current app (this.appName) is the same as the new app
180
+ if (this.appName === appName) {
181
+ return;
182
+ }
183
+ let newContextCreated = false;
184
+ if (!apps[appName]) {
185
+ let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
186
+ newContextCreated = true;
187
+ apps[appName] = {
188
+ context: newContext,
189
+ browser: newContext.browser,
190
+ page: newContext.page,
191
+ };
192
+ }
193
+ const tempContext = {};
194
+ _copyContext(this, tempContext);
195
+ _copyContext(apps[appName], this);
196
+ apps[this.appName] = tempContext;
197
+ this.appName = appName;
198
+ if (newContextCreated) {
199
+ this.registerEventListeners(this.context);
200
+ await this.goto(this.context.environment.baseUrl);
201
+ await this.waitForPageLoad();
202
+ }
203
+ }
204
+ async switchTab(tabTitleOrIndex) {
205
+ // first check if the tabNameOrIndex is a number
206
+ let index = parseInt(tabTitleOrIndex);
207
+ if (!isNaN(index)) {
208
+ if (index >= 0 && index < this.context.pages.length) {
209
+ this.page = this.context.pages[index];
210
+ this.context.page = this.page;
211
+ await this.page.bringToFront();
212
+ return;
213
+ }
116
214
  }
117
- let nextIndex = 1;
118
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
119
- nextIndex++;
215
+ // if the tabNameOrIndex is a string, find the tab by name
216
+ for (let i = 0; i < this.context.pages.length; i++) {
217
+ let page = this.context.pages[i];
218
+ let title = await page.title();
219
+ if (title.includes(tabTitleOrIndex)) {
220
+ this.page = page;
221
+ this.context.page = this.page;
222
+ await this.page.bringToFront();
223
+ return;
224
+ }
120
225
  }
121
- const fileName = nextIndex + ".json";
122
- return path.join(logFolder, fileName);
226
+ throw new Error("Tab not found: " + tabTitleOrIndex);
123
227
  }
124
- registerConsoleLogListener(page, context, logFile) {
228
+ registerConsoleLogListener(page, context) {
125
229
  if (!this.context.webLogger) {
126
230
  this.context.webLogger = [];
127
231
  }
128
232
  page.on("console", async (msg) => {
129
- this.context.webLogger.push({
233
+ const obj = {
130
234
  type: msg.type(),
131
235
  text: msg.text(),
132
236
  location: msg.location(),
133
237
  time: new Date().toISOString(),
134
- });
135
- await fs.promises.writeFile(logFile, JSON.stringify(this.context.webLogger, null, 2));
238
+ };
239
+ this.context.webLogger.push(obj);
240
+ if (msg.type() === "error") {
241
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
242
+ }
136
243
  });
137
244
  }
138
- registerRequestListener() {
139
- this.page.on("request", async (data) => {
245
+ registerRequestListener(page, context, logFile) {
246
+ if (!this.context.networkLogger) {
247
+ this.context.networkLogger = [];
248
+ }
249
+ page.on("request", async (data) => {
250
+ const startTime = new Date().getTime();
140
251
  try {
141
- const pageUrl = new URL(this.page.url());
252
+ const pageUrl = new URL(page.url());
142
253
  const requestUrl = new URL(data.url());
143
254
  if (pageUrl.hostname === requestUrl.hostname) {
144
255
  const method = data.method();
145
- if (method === "POST" || method === "GET" || method === "PUT" || method === "DELETE" || method === "PATCH") {
256
+ if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
146
257
  const token = await data.headerValue("Authorization");
147
258
  if (token) {
148
- this.context.authtoken = token;
259
+ context.authtoken = token;
149
260
  }
150
261
  }
151
262
  }
263
+ const response = await data.response();
264
+ const endTime = new Date().getTime();
265
+ const obj = {
266
+ url: data.url(),
267
+ method: data.method(),
268
+ postData: data.postData(),
269
+ error: data.failure() ? data.failure().errorText : null,
270
+ duration: endTime - startTime,
271
+ startTime,
272
+ };
273
+ context.networkLogger.push(obj);
274
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
152
275
  }
153
276
  catch (error) {
154
- console.error("Error in request listener", error);
277
+ // console.error("Error in request listener", error);
278
+ context.networkLogger.push({
279
+ error: "not able to listen",
280
+ message: error.message,
281
+ stack: error.stack,
282
+ time: new Date().toISOString(),
283
+ });
284
+ // await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
155
285
  }
156
286
  });
157
287
  }
158
288
  // async closeUnexpectedPopups() {
159
289
  // await closeUnexpectedPopups(this.page);
160
290
  // }
161
- async goto(url) {
291
+ async goto(url, world = null) {
292
+ if (!url) {
293
+ throw new Error("url is null, verify that the environment file is correct");
294
+ }
295
+ url = await this._replaceWithLocalData(url, this.world);
162
296
  if (!url.startsWith("http")) {
163
297
  url = "https://" + url;
164
298
  }
165
- await this.page.goto(url, {
166
- timeout: 60000,
167
- });
168
- }
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
- _fixUsingParams(text, _params) {
184
- if (!_params || typeof text !== "string") {
185
- return text;
299
+ const state = {
300
+ value: url,
301
+ world: world,
302
+ type: Types.NAVIGATE,
303
+ text: `Navigate Page to: ${url}`,
304
+ operation: "goto",
305
+ log: "***** navigate page to " + url + " *****\n",
306
+ info: {},
307
+ locate: false,
308
+ scroll: false,
309
+ screenshot: false,
310
+ highlight: false,
311
+ };
312
+ try {
313
+ await _preCommand(state, this);
314
+ await this.page.goto(url, {
315
+ timeout: 60000,
316
+ });
317
+ await _screenshot(state, this);
186
318
  }
187
- for (let key in _params) {
188
- let regValue = key;
189
- if (key.startsWith("_")) {
190
- // remove the _ prefix
191
- regValue = key.substring(1);
192
- }
193
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
319
+ catch (error) {
320
+ console.error("Error on goto", error);
321
+ _commandError(state, error, this);
194
322
  }
195
- return text;
196
- }
197
- _fixLocatorUsingParams(locator, _params) {
198
- // check if not null
199
- if (!locator) {
200
- return locator;
323
+ finally {
324
+ await _commandFinally(state, this);
201
325
  }
202
- // clone the locator
203
- locator = JSON.parse(JSON.stringify(locator));
204
- this.scanAndManipulate(locator, _params);
205
- return locator;
206
326
  }
207
- _isObject(value) {
208
- return value && typeof value === "object" && value.constructor === Object;
209
- }
210
- scanAndManipulate(currentObj, _params) {
211
- for (const key in currentObj) {
212
- if (typeof currentObj[key] === "string") {
213
- // Perform string manipulation
214
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
215
- }
216
- else if (this._isObject(currentObj[key])) {
217
- // Recursively scan nested objects
218
- this.scanAndManipulate(currentObj[key], _params);
327
+ async _getLocator(locator, scope, _params) {
328
+ locator = _fixLocatorUsingParams(locator, _params);
329
+ // locator = await this._replaceWithLocalData(locator);
330
+ for (let key in locator) {
331
+ if (typeof locator[key] !== "string")
332
+ continue;
333
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
334
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
219
335
  }
220
336
  }
221
- }
222
- _getLocator(locator, scope, _params) {
223
- locator = this._fixLocatorUsingParams(locator, _params);
224
337
  let locatorReturn;
225
338
  if (locator.role) {
226
339
  if (locator.role[1].nameReg) {
@@ -228,7 +341,7 @@ class StableBrowser {
228
341
  delete locator.role[1].nameReg;
229
342
  }
230
343
  // if (locator.role[1].name) {
231
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
344
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
232
345
  // }
233
346
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
234
347
  }
@@ -247,7 +360,7 @@ class StableBrowser {
247
360
  locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
248
361
  }
249
362
  }
250
- if (locator === null || locator === void 0 ? void 0 : locator.engine) {
363
+ if (locator?.engine) {
251
364
  if (locator.engine === "css") {
252
365
  locatorReturn = scope.locator(locator.selector);
253
366
  }
@@ -268,192 +381,181 @@ class StableBrowser {
268
381
  return locatorReturn;
269
382
  }
270
383
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
271
- let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
384
+ if (css && css.locator) {
385
+ css = css.locator;
386
+ }
387
+ let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
272
388
  if (result.elementCount === 0) {
273
389
  return;
274
390
  }
275
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
391
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
276
392
  // css climb to parent element
277
393
  const climbArray = [];
278
394
  for (let i = 0; i < climb; i++) {
279
395
  climbArray.push("..");
280
396
  }
281
397
  let climbXpath = "xpath=" + climbArray.join("/");
282
- return textElementCss + " >> " + climbXpath + " >> " + css;
283
- }
284
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
285
- //const stringifyText = JSON.stringify(text);
286
- return await scope.evaluate(([text, tag, regex, partial]) => {
287
- function isParent(parent, child) {
288
- let currentNode = child.parentNode;
289
- while (currentNode !== null) {
290
- if (currentNode === parent) {
291
- return true;
292
- }
293
- currentNode = currentNode.parentNode;
294
- }
295
- return false;
296
- }
297
- document.isParent = isParent;
298
- function collectAllShadowDomElements(element, result = []) {
299
- // Check and add the element if it has a shadow root
300
- if (element.shadowRoot) {
301
- result.push(element);
302
- // Also search within the shadow root
303
- document.collectAllShadowDomElements(element.shadowRoot, result);
304
- }
305
- // Iterate over child nodes
306
- element.childNodes.forEach((child) => {
307
- // Recursively call the function for each child node
308
- document.collectAllShadowDomElements(child, result);
309
- });
310
- return result;
311
- }
312
- document.collectAllShadowDomElements = collectAllShadowDomElements;
313
- if (!tag) {
314
- tag = "*";
315
- }
316
- let elements = Array.from(document.querySelectorAll(tag));
317
- let shadowHosts = [];
318
- document.collectAllShadowDomElements(document, shadowHosts);
319
- for (let i = 0; i < shadowHosts.length; i++) {
320
- let shadowElement = shadowHosts[i].shadowRoot;
321
- if (!shadowElement) {
322
- console.log("shadowElement is null, for host " + shadowHosts[i]);
323
- continue;
324
- }
325
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
326
- elements = elements.concat(shadowElements);
327
- }
328
- let randomToken = null;
329
- const foundElements = [];
330
- if (regex) {
331
- let regexpSearch = new RegExp(text, "im");
332
- for (let i = 0; i < elements.length; i++) {
333
- const element = elements[i];
334
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
335
- (element.value && regexpSearch.test(element.value))) {
336
- foundElements.push(element);
337
- }
338
- }
339
- }
340
- else {
341
- text = text.trim();
342
- for (let i = 0; i < elements.length; i++) {
343
- const element = elements[i];
344
- if (partial) {
345
- if ((element.innerText && element.innerText.trim().includes(text)) ||
346
- (element.value && element.value.includes(text))) {
347
- foundElements.push(element);
348
- }
349
- }
350
- else {
351
- if ((element.innerText && element.innerText.trim() === text) ||
352
- (element.value && element.value === text)) {
353
- foundElements.push(element);
354
- }
355
- }
356
- }
357
- }
358
- let noChildElements = [];
359
- for (let i = 0; i < foundElements.length; i++) {
360
- let element = foundElements[i];
361
- let hasChild = false;
362
- for (let j = 0; j < foundElements.length; j++) {
363
- if (i === j) {
364
- continue;
365
- }
366
- if (isParent(element, foundElements[j])) {
367
- hasChild = true;
368
- break;
369
- }
370
- }
371
- if (!hasChild) {
372
- noChildElements.push(element);
373
- }
374
- }
375
- let elementCount = 0;
376
- if (noChildElements.length > 0) {
377
- for (let i = 0; i < noChildElements.length; i++) {
378
- if (randomToken === null) {
379
- randomToken = Math.random().toString(36).substring(7);
380
- }
381
- let element = noChildElements[i];
382
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
383
- elementCount++;
384
- }
398
+ let resultCss = textElementCss + " >> " + climbXpath;
399
+ if (css) {
400
+ resultCss = resultCss + " >> " + css;
401
+ }
402
+ return resultCss;
403
+ }
404
+ async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
405
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
406
+ const locator = scope.locator(query);
407
+ const count = await locator.count();
408
+ if (!tag1) {
409
+ tag1 = "*";
410
+ }
411
+ const randomToken = Math.random().toString(36).substring(7);
412
+ let tagCount = 0;
413
+ for (let i = 0; i < count; i++) {
414
+ const element = locator.nth(i);
415
+ // check if the tag matches
416
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
417
+ if (!tag.startsWith("*")) {
418
+ if (el.tagName.toLowerCase() !== tag) {
419
+ return false;
420
+ }
421
+ }
422
+ if (!el.setAttribute) {
423
+ el = el.parentElement;
424
+ }
425
+ // remove any attributes start with data-blinq-id
426
+ // for (let i = 0; i < el.attributes.length; i++) {
427
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
428
+ // el.removeAttribute(el.attributes[i].name);
429
+ // }
430
+ // }
431
+ el.setAttribute("data-blinq-id-" + randomToken, "");
432
+ return true;
433
+ }, [tag1, randomToken]))) {
434
+ continue;
385
435
  }
386
- return { elementCount: elementCount, randomToken: randomToken };
387
- }, [text1, tag1, regex1, partial1]);
436
+ tagCount++;
437
+ }
438
+ return { elementCount: tagCount, randomToken };
388
439
  }
389
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
440
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
441
+ if (!info) {
442
+ info = {};
443
+ }
444
+ if (!info.failCause) {
445
+ info.failCause = {};
446
+ }
447
+ if (!info.log) {
448
+ info.log = "";
449
+ info.locatorLog = new LocatorLog(selectorHierarchy);
450
+ }
390
451
  let locatorSearch = selectorHierarchy[index];
452
+ let originalLocatorSearch = "";
453
+ try {
454
+ originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
455
+ locatorSearch = JSON.parse(originalLocatorSearch);
456
+ }
457
+ catch (e) {
458
+ console.error(e);
459
+ }
391
460
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
392
461
  let locator = null;
393
462
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
394
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
463
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
464
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
395
465
  if (!locatorString) {
466
+ info.failCause.textNotFound = true;
467
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
396
468
  return;
397
469
  }
398
- locator = this._getLocator({ css: locatorString }, scope, _params);
470
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
399
471
  }
400
472
  else if (locatorSearch.text) {
401
- let result = await this._locateElementByText(scope, this._fixUsingParams(locatorSearch.text, _params), locatorSearch.tag, false, locatorSearch.partial === true, _params);
473
+ let text = _fixUsingParams(locatorSearch.text, _params);
474
+ let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
402
475
  if (result.elementCount === 0) {
476
+ info.failCause.textNotFound = true;
477
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
403
478
  return;
404
479
  }
405
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
480
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
406
481
  if (locatorSearch.childCss) {
407
482
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
408
483
  }
409
- locator = this._getLocator(locatorSearch, scope, _params);
484
+ locator = await this._getLocator(locatorSearch, scope, _params);
410
485
  }
411
486
  else {
412
- locator = this._getLocator(locatorSearch, scope, _params);
487
+ locator = await this._getLocator(locatorSearch, scope, _params);
413
488
  }
414
489
  // let cssHref = false;
415
490
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
416
491
  // cssHref = true;
417
492
  // }
418
493
  let count = await locator.count();
494
+ if (count > 0 && !info.failCause.count) {
495
+ info.failCause.count = count;
496
+ }
419
497
  //info.log += "total elements found " + count + "\n";
420
498
  //let visibleCount = 0;
421
499
  let visibleLocator = null;
422
- if (locatorSearch.index && locatorSearch.index < count) {
500
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
423
501
  foundLocators.push(locator.nth(locatorSearch.index));
502
+ if (info.locatorLog) {
503
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
504
+ }
424
505
  return;
425
506
  }
507
+ if (info.locatorLog && count === 0 && logErrors) {
508
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
509
+ }
426
510
  for (let j = 0; j < count; j++) {
427
511
  let visible = await locator.nth(j).isVisible();
428
512
  const enabled = await locator.nth(j).isEnabled();
429
513
  if (!visibleOnly) {
430
514
  visible = true;
431
515
  }
432
- if (visible && enabled) {
516
+ if (visible && (allowDisabled || enabled)) {
433
517
  foundLocators.push(locator.nth(j));
518
+ if (info.locatorLog) {
519
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
520
+ }
434
521
  }
435
- else {
522
+ else if (logErrors) {
523
+ info.failCause.visible = visible;
524
+ info.failCause.enabled = enabled;
436
525
  if (!info.printMessages) {
437
526
  info.printMessages = {};
438
527
  }
528
+ if (info.locatorLog && !visible) {
529
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
530
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
531
+ }
532
+ if (info.locatorLog && !enabled) {
533
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
534
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
535
+ }
439
536
  if (!info.printMessages[j.toString()]) {
440
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
537
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
441
538
  info.printMessages[j.toString()] = true;
442
539
  }
443
540
  }
444
541
  }
445
542
  }
446
543
  async closeUnexpectedPopups(info, _params) {
544
+ if (!info) {
545
+ info = {};
546
+ info.failCause = {};
547
+ info.log = "";
548
+ }
447
549
  if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
448
550
  if (!info) {
449
551
  info = {};
450
552
  }
451
- info.log += "scan for popup handlers" + "\n";
553
+ //info.log += "scan for popup handlers" + "\n";
452
554
  const handlerGroup = [];
453
555
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
454
556
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
455
557
  }
456
- const scopes = [this.page, ...this.page.frames()];
558
+ const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
457
559
  let result = null;
458
560
  let scope = null;
459
561
  for (let i = 0; i < scopes.length; i++) {
@@ -475,53 +577,108 @@ class StableBrowser {
475
577
  }
476
578
  if (result.foundElements.length > 0) {
477
579
  let dialogCloseLocator = result.foundElements[0].locator;
478
- await dialogCloseLocator.click();
580
+ try {
581
+ await scope?.evaluate(() => {
582
+ window.__isClosingPopups = true;
583
+ });
584
+ await dialogCloseLocator.click();
585
+ // wait for the dialog to close
586
+ await dialogCloseLocator.waitFor({ state: "hidden" });
587
+ }
588
+ catch (e) {
589
+ }
590
+ finally {
591
+ await scope?.evaluate(() => {
592
+ window.__isClosingPopups = false;
593
+ });
594
+ }
479
595
  return { rerun: true };
480
596
  }
481
597
  }
482
598
  }
483
599
  return { rerun: false };
484
600
  }
485
- async _locate(selectors, info, _params, timeout = 30000) {
601
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
602
+ if (!timeout) {
603
+ timeout = 30000;
604
+ }
486
605
  for (let i = 0; i < 3; i++) {
487
606
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
488
607
  for (let j = 0; j < selectors.locators.length; j++) {
489
608
  let selector = selectors.locators[j];
490
609
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
491
610
  }
492
- let element = await this._locate_internal(selectors, info, _params, timeout);
611
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
493
612
  if (!element.rerun) {
494
- return element;
613
+ const randomToken = Math.random().toString(36).substring(7);
614
+ await element.evaluate((el, randomToken) => {
615
+ el.setAttribute("data-blinq-id-" + randomToken, "");
616
+ }, randomToken);
617
+ // if (element._frame) {
618
+ // return element;
619
+ // }
620
+ const scope = element._frame ?? element.page();
621
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
622
+ let prefixSelector = "";
623
+ const frameControlSelector = " >> internal:control=enter-frame";
624
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
625
+ if (frameSelectorIndex !== -1) {
626
+ // remove everything after the >> internal:control=enter-frame
627
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
628
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
629
+ }
630
+ // if (element?._frame?._selector) {
631
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
632
+ // }
633
+ const newSelector = prefixSelector + newElementSelector;
634
+ return scope.locator(newSelector);
495
635
  }
496
636
  }
497
637
  throw new Error("unable to locate element " + JSON.stringify(selectors));
498
638
  }
499
- async _locate_internal(selectors, info, _params, timeout = 30000) {
500
- let highPriorityTimeout = 5000;
501
- let visibleOnlyTimeout = 6000;
502
- let startTime = performance.now();
503
- let locatorsCount = 0;
504
- //let arrayMode = Array.isArray(selectors);
639
+ async _findFrameScope(selectors, timeout = 30000, info) {
640
+ if (!info) {
641
+ info = {};
642
+ info.failCause = {};
643
+ info.log = "";
644
+ }
645
+ let startTime = Date.now();
505
646
  let scope = this.page;
647
+ if (selectors.frame) {
648
+ return selectors.frame;
649
+ }
506
650
  if (selectors.iframe_src || selectors.frameLocators) {
507
- const findFrame = (frame, framescope) => {
651
+ const findFrame = async (frame, framescope) => {
508
652
  for (let i = 0; i < frame.selectors.length; i++) {
509
653
  let frameLocator = frame.selectors[i];
510
654
  if (frameLocator.css) {
511
- framescope = framescope.frameLocator(frameLocator.css);
512
- break;
655
+ let testframescope = framescope.frameLocator(frameLocator.css);
656
+ if (frameLocator.index) {
657
+ testframescope = framescope.nth(frameLocator.index);
658
+ }
659
+ try {
660
+ await testframescope.owner().evaluateHandle(() => true, null, {
661
+ timeout: 5000,
662
+ });
663
+ framescope = testframescope;
664
+ break;
665
+ }
666
+ catch (error) {
667
+ console.error("frame not found " + frameLocator.css);
668
+ }
513
669
  }
514
670
  }
515
671
  if (frame.children) {
516
- return findFrame(frame.children, framescope);
672
+ return await findFrame(frame.children, framescope);
517
673
  }
518
674
  return framescope;
519
675
  };
520
- info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
676
+ let fLocator = null;
521
677
  while (true) {
522
678
  let frameFound = false;
523
679
  if (selectors.nestFrmLoc) {
524
- scope = findFrame(selectors.nestFrmLoc, scope);
680
+ fLocator = selectors.nestFrmLoc;
681
+ scope = await findFrame(selectors.nestFrmLoc, scope);
525
682
  frameFound = true;
526
683
  break;
527
684
  }
@@ -529,6 +686,7 @@ class StableBrowser {
529
686
  for (let i = 0; i < selectors.frameLocators.length; i++) {
530
687
  let frameLocator = selectors.frameLocators[i];
531
688
  if (frameLocator.css) {
689
+ fLocator = frameLocator.css;
532
690
  scope = scope.frameLocator(frameLocator.css);
533
691
  frameFound = true;
534
692
  break;
@@ -536,20 +694,55 @@ class StableBrowser {
536
694
  }
537
695
  }
538
696
  if (!frameFound && selectors.iframe_src) {
697
+ fLocator = selectors.iframe_src;
539
698
  scope = this.page.frame({ url: selectors.iframe_src });
540
699
  }
541
700
  if (!scope) {
542
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
543
- if (performance.now() - startTime > timeout) {
701
+ if (info && info.locatorLog) {
702
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
703
+ }
704
+ //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
705
+ if (Date.now() - startTime > timeout) {
706
+ info.failCause.iframeNotFound = true;
707
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
544
708
  throw new Error("unable to locate iframe " + selectors.iframe_src);
545
709
  }
546
710
  await new Promise((resolve) => setTimeout(resolve, 1000));
547
711
  }
548
712
  else {
713
+ if (info && info.locatorLog) {
714
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
715
+ }
549
716
  break;
550
717
  }
551
718
  }
552
719
  }
720
+ if (!scope) {
721
+ scope = this.page;
722
+ }
723
+ return scope;
724
+ }
725
+ async _getDocumentBody(selectors, timeout = 30000, info) {
726
+ let scope = await this._findFrameScope(selectors, timeout, info);
727
+ return scope.evaluate(() => {
728
+ var bodyContent = document.body.innerHTML;
729
+ return bodyContent;
730
+ });
731
+ }
732
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
733
+ if (!info) {
734
+ info = {};
735
+ info.failCause = {};
736
+ info.log = "";
737
+ info.locatorLog = new LocatorLog(selectors);
738
+ }
739
+ let highPriorityTimeout = 5000;
740
+ let visibleOnlyTimeout = 6000;
741
+ let startTime = Date.now();
742
+ let locatorsCount = 0;
743
+ let lazy_scroll = false;
744
+ //let arrayMode = Array.isArray(selectors);
745
+ let scope = await this._findFrameScope(selectors, timeout, info);
553
746
  let selectorsLocators = null;
554
747
  selectorsLocators = selectors.locators;
555
748
  // group selectors by priority
@@ -585,18 +778,13 @@ class StableBrowser {
585
778
  }
586
779
  // info.log += "scanning locators in priority 1" + "\n";
587
780
  let onlyPriority3 = selectorsLocators[0].priority === 3;
588
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
781
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
589
782
  if (result.foundElements.length === 0) {
590
783
  // info.log += "scanning locators in priority 2" + "\n";
591
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
784
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
592
785
  }
593
- if (result.foundElements.length === 0 && onlyPriority3) {
594
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
595
- }
596
- else {
597
- if (result.foundElements.length === 0 && !highPriorityOnly) {
598
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
599
- }
786
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
787
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
600
788
  }
601
789
  let foundElements = result.foundElements;
602
790
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -636,24 +824,43 @@ class StableBrowser {
636
824
  return maxCountElement.locator;
637
825
  }
638
826
  }
639
- if (performance.now() - startTime > timeout) {
827
+ if (Date.now() - startTime > timeout) {
640
828
  break;
641
829
  }
642
- if (performance.now() - startTime > highPriorityTimeout) {
643
- info.log += "high priority timeout, will try all elements" + "\n";
830
+ if (Date.now() - startTime > highPriorityTimeout) {
831
+ //info.log += "high priority timeout, will try all elements" + "\n";
644
832
  highPriorityOnly = false;
833
+ if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
834
+ lazy_scroll = true;
835
+ await scrollPageToLoadLazyElements(this.page);
836
+ }
645
837
  }
646
- if (performance.now() - startTime > visibleOnlyTimeout) {
647
- info.log += "visible only timeout, will try all elements" + "\n";
838
+ if (Date.now() - startTime > visibleOnlyTimeout) {
839
+ //info.log += "visible only timeout, will try all elements" + "\n";
648
840
  visibleOnly = false;
649
841
  }
650
842
  await new Promise((resolve) => setTimeout(resolve, 1000));
843
+ // sheck of more of half of the timeout has passed
844
+ if (Date.now() - startTime > timeout / 2) {
845
+ highPriorityOnly = false;
846
+ visibleOnly = false;
847
+ }
651
848
  }
652
849
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
653
- info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
850
+ // if (info.locatorLog) {
851
+ // const lines = info.locatorLog.toString().split("\n");
852
+ // for (let line of lines) {
853
+ // this.logger.debug(line);
854
+ // }
855
+ // }
856
+ //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
857
+ info.failCause.locatorNotFound = true;
858
+ if (!info?.failCause?.lastError) {
859
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
860
+ }
654
861
  throw new Error("failed to locate first element no elements found, " + info.log);
655
862
  }
656
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
863
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
657
864
  let foundElements = [];
658
865
  const result = {
659
866
  foundElements: foundElements,
@@ -661,17 +868,20 @@ class StableBrowser {
661
868
  for (let i = 0; i < locatorsGroup.length; i++) {
662
869
  let foundLocators = [];
663
870
  try {
664
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
871
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
665
872
  }
666
873
  catch (e) {
667
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
668
- this.logger.debug(e);
874
+ // this call can fail it the browser is navigating
875
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
876
+ // this.logger.debug(e);
669
877
  foundLocators = [];
670
878
  try {
671
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
879
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
672
880
  }
673
881
  catch (e) {
674
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
882
+ if (logErrors) {
883
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
884
+ }
675
885
  }
676
886
  }
677
887
  if (foundLocators.length === 1) {
@@ -682,270 +892,348 @@ class StableBrowser {
682
892
  });
683
893
  result.locatorIndex = i;
684
894
  }
895
+ if (foundLocators.length > 1) {
896
+ // remove elements that consume the same space with 10 pixels tolerance
897
+ const boxes = [];
898
+ for (let j = 0; j < foundLocators.length; j++) {
899
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
900
+ }
901
+ for (let j = 0; j < boxes.length; j++) {
902
+ for (let k = 0; k < boxes.length; k++) {
903
+ if (j === k) {
904
+ continue;
905
+ }
906
+ // check if x, y, width, height are the same with 10 pixels tolerance
907
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
908
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
909
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
910
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
911
+ // as the element is not unique, will remove it
912
+ boxes.splice(k, 1);
913
+ k--;
914
+ }
915
+ }
916
+ }
917
+ if (boxes.length === 1) {
918
+ result.foundElements.push({
919
+ locator: boxes[0].locator.first(),
920
+ box: boxes[0].box,
921
+ unique: true,
922
+ });
923
+ result.locatorIndex = i;
924
+ }
925
+ else if (logErrors) {
926
+ info.failCause.foundMultiple = true;
927
+ if (info.locatorLog) {
928
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
929
+ }
930
+ }
931
+ }
685
932
  }
686
933
  return result;
687
934
  }
688
- async click(selectors, _params, options = {}, world = null) {
689
- this._validateSelectors(selectors);
935
+ async simpleClick(elementDescription, _params, options = {}, world = null) {
936
+ const state = {
937
+ locate: false,
938
+ scroll: false,
939
+ highlight: false,
940
+ _params,
941
+ options,
942
+ world,
943
+ type: Types.CLICK,
944
+ text: "Click element",
945
+ operation: "simpleClick",
946
+ log: "***** click on " + elementDescription + " *****\n",
947
+ };
948
+ _preCommand(state, this);
690
949
  const startTime = Date.now();
691
- if (options && options.context) {
692
- selectors.locators[0].text = options.context;
950
+ let timeout = 30000;
951
+ if (options && options.timeout) {
952
+ timeout = options.timeout;
693
953
  }
694
- const info = {};
695
- info.log = "***** click on " + selectors.element_name + " *****\n";
696
- info.operation = "click";
697
- info.selectors = selectors;
698
- let error = null;
699
- let screenshotId = null;
700
- let screenshotPath = null;
701
- try {
702
- let element = await this._locate(selectors, info, _params);
703
- await this.scrollIfNeeded(element, info);
704
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
954
+ while (true) {
705
955
  try {
706
- await this._highlightElements(element);
707
- await element.click();
708
- await new Promise((resolve) => setTimeout(resolve, 1000));
956
+ const result = await locate_element(this.context, elementDescription, "click");
957
+ if (result?.elementNumber >= 0) {
958
+ const selectors = {
959
+ frame: result?.frame,
960
+ locators: [
961
+ {
962
+ css: result?.css,
963
+ },
964
+ ],
965
+ };
966
+ await this.click(selectors, _params, options, world);
967
+ return;
968
+ }
709
969
  }
710
970
  catch (e) {
711
- // await this.closeUnexpectedPopups();
712
- info.log += "click failed, will try again" + "\n";
713
- element = await this._locate(selectors, info, _params);
714
- await element.dispatchEvent("click");
715
- await new Promise((resolve) => setTimeout(resolve, 1000));
971
+ if (performance.now() - startTime > timeout) {
972
+ // throw e;
973
+ try {
974
+ await _commandError(state, "timeout looking for " + elementDescription, this);
975
+ }
976
+ finally {
977
+ await _commandFinally(state, this);
978
+ }
979
+ }
980
+ }
981
+ await new Promise((resolve) => setTimeout(resolve, 3000));
982
+ }
983
+ }
984
+ async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
985
+ const state = {
986
+ locate: false,
987
+ scroll: false,
988
+ highlight: false,
989
+ _params,
990
+ options,
991
+ world,
992
+ type: Types.FILL,
993
+ text: "Fill element",
994
+ operation: "simpleClickType",
995
+ log: "***** click type on " + elementDescription + " *****\n",
996
+ };
997
+ _preCommand(state, this);
998
+ const startTime = Date.now();
999
+ let timeout = 30000;
1000
+ if (options && options.timeout) {
1001
+ timeout = options.timeout;
1002
+ }
1003
+ while (true) {
1004
+ try {
1005
+ const result = await locate_element(this.context, elementDescription, "fill", value);
1006
+ if (result?.elementNumber >= 0) {
1007
+ const selectors = {
1008
+ frame: result?.frame,
1009
+ locators: [
1010
+ {
1011
+ css: result?.css,
1012
+ },
1013
+ ],
1014
+ };
1015
+ await this.clickType(selectors, value, false, _params, options, world);
1016
+ return;
1017
+ }
1018
+ }
1019
+ catch (e) {
1020
+ if (performance.now() - startTime > timeout) {
1021
+ // throw e;
1022
+ try {
1023
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1024
+ }
1025
+ finally {
1026
+ await _commandFinally(state, this);
1027
+ }
1028
+ }
716
1029
  }
1030
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1031
+ }
1032
+ }
1033
+ async click(selectors, _params, options = {}, world = null) {
1034
+ const state = {
1035
+ selectors,
1036
+ _params,
1037
+ options,
1038
+ world,
1039
+ text: "Click element",
1040
+ _text: "Click on " + selectors.element_name,
1041
+ type: Types.CLICK,
1042
+ operation: "click",
1043
+ log: "***** click on " + selectors.element_name + " *****\n",
1044
+ };
1045
+ try {
1046
+ await _preCommand(state, this);
1047
+ await performAction("click", state.element, options, this, state, _params);
717
1048
  await this.waitForPageLoad();
718
- return info;
1049
+ return state.info;
719
1050
  }
720
1051
  catch (e) {
721
- this.logger.error("click failed " + info.log);
722
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
723
- info.screenshotPath = screenshotPath;
724
- Object.assign(e, { info: info });
725
- error = e;
726
- throw e;
1052
+ await _commandError(state, e, this);
727
1053
  }
728
1054
  finally {
729
- const endTime = Date.now();
730
- this._reportToWorld(world, {
731
- element_name: selectors.element_name,
732
- type: Types.CLICK,
733
- text: `Click element`,
734
- screenshotId,
735
- result: error
736
- ? {
737
- status: "FAILED",
738
- startTime,
739
- endTime,
740
- message: error === null || error === void 0 ? void 0 : error.message,
741
- }
742
- : {
743
- status: "PASSED",
744
- startTime,
745
- endTime,
746
- },
747
- info: info,
748
- });
1055
+ await _commandFinally(state, this);
1056
+ }
1057
+ }
1058
+ async waitForElement(selectors, _params, options = {}, world = null) {
1059
+ const timeout = this._getFindElementTimeout(options);
1060
+ const state = {
1061
+ selectors,
1062
+ _params,
1063
+ options,
1064
+ world,
1065
+ text: "Wait for element",
1066
+ _text: "Wait for " + selectors.element_name,
1067
+ type: Types.WAIT_ELEMENT,
1068
+ operation: "waitForElement",
1069
+ log: "***** wait for " + selectors.element_name + " *****\n",
1070
+ };
1071
+ let found = false;
1072
+ try {
1073
+ await _preCommand(state, this);
1074
+ // if (state.options && state.options.context) {
1075
+ // state.selectors.locators[0].text = state.options.context;
1076
+ // }
1077
+ await state.element.waitFor({ timeout: timeout });
1078
+ found = true;
1079
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1080
+ }
1081
+ catch (e) {
1082
+ console.error("Error on waitForElement", e);
1083
+ // await _commandError(state, e, this);
1084
+ }
1085
+ finally {
1086
+ await _commandFinally(state, this);
749
1087
  }
1088
+ return found;
750
1089
  }
751
1090
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
752
- this._validateSelectors(selectors);
753
- const startTime = Date.now();
754
- const info = {};
755
- info.log = "";
756
- info.operation = "setCheck";
757
- info.checked = checked;
758
- info.selectors = selectors;
759
- let error = null;
760
- let screenshotId = null;
761
- let screenshotPath = null;
1091
+ const state = {
1092
+ selectors,
1093
+ _params,
1094
+ options,
1095
+ world,
1096
+ type: checked ? Types.CHECK : Types.UNCHECK,
1097
+ text: checked ? `Check element` : `Uncheck element`,
1098
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
1099
+ operation: "setCheck",
1100
+ log: "***** check " + selectors.element_name + " *****\n",
1101
+ };
762
1102
  try {
763
- let element = await this._locate(selectors, info, _params);
764
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1103
+ await _preCommand(state, this);
1104
+ state.info.checked = checked;
1105
+ // let element = await this._locate(selectors, info, _params);
1106
+ // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
765
1107
  try {
766
- await this._highlightElements(element);
767
- await element.setChecked(checked);
1108
+ // if (world && world.screenshot && !world.screenshotPath) {
1109
+ // console.log(`Highlighting while running from recorder`);
1110
+ await this._highlightElements(state.element);
1111
+ await state.element.setChecked(checked, { timeout: 2000 });
768
1112
  await new Promise((resolve) => setTimeout(resolve, 1000));
1113
+ // await this._unHighlightElements(element);
1114
+ // }
1115
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1116
+ // await this._unHighlightElements(element);
769
1117
  }
770
1118
  catch (e) {
771
1119
  if (e.message && e.message.includes("did not change its state")) {
772
1120
  this.logger.info("element did not change its state, ignoring...");
773
1121
  }
774
1122
  else {
775
- //await this.closeUnexpectedPopups();
776
- info.log += "setCheck failed, will try again" + "\n";
777
- element = await this._locate(selectors, info, _params);
778
- await element.setChecked(checked, { timeout: 5000, force: true });
779
1123
  await new Promise((resolve) => setTimeout(resolve, 1000));
1124
+ //await this.closeUnexpectedPopups();
1125
+ state.info.log += "setCheck failed, will try again" + "\n";
1126
+ state.element_found = false;
1127
+ try {
1128
+ state.element = await this._locate(selectors, state.info, _params, 100);
1129
+ state.element_found = true;
1130
+ // check the check state
1131
+ }
1132
+ catch (error) {
1133
+ // element dismissed
1134
+ }
1135
+ if (state.element_found) {
1136
+ const isChecked = await state.element.isChecked();
1137
+ if (isChecked !== checked) {
1138
+ // perform click
1139
+ await state.element.click({ timeout: 2000, force: true });
1140
+ }
1141
+ else {
1142
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1143
+ }
1144
+ }
780
1145
  }
781
1146
  }
782
1147
  await this.waitForPageLoad();
783
- return info;
1148
+ return state.info;
784
1149
  }
785
1150
  catch (e) {
786
- this.logger.error("setCheck failed " + info.log);
787
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
788
- info.screenshotPath = screenshotPath;
789
- Object.assign(e, { info: info });
790
- error = e;
791
- throw e;
1151
+ await _commandError(state, e, this);
792
1152
  }
793
1153
  finally {
794
- const endTime = Date.now();
795
- this._reportToWorld(world, {
796
- element_name: selectors.element_name,
797
- type: checked ? Types.CHECK : Types.UNCHECK,
798
- text: checked ? `Check element` : `Uncheck element`,
799
- screenshotId,
800
- result: error
801
- ? {
802
- status: "FAILED",
803
- startTime,
804
- endTime,
805
- message: error === null || error === void 0 ? void 0 : error.message,
806
- }
807
- : {
808
- status: "PASSED",
809
- startTime,
810
- endTime,
811
- },
812
- info: info,
813
- });
1154
+ await _commandFinally(state, this);
814
1155
  }
815
1156
  }
816
1157
  async hover(selectors, _params, options = {}, world = null) {
817
- this._validateSelectors(selectors);
818
- const startTime = Date.now();
819
- const info = {};
820
- info.log = "";
821
- info.operation = "hover";
822
- info.selectors = selectors;
823
- let error = null;
824
- let screenshotId = null;
825
- let screenshotPath = null;
1158
+ const state = {
1159
+ selectors,
1160
+ _params,
1161
+ options,
1162
+ world,
1163
+ type: Types.HOVER,
1164
+ text: `Hover element`,
1165
+ _text: `Hover on ${selectors.element_name}`,
1166
+ operation: "hover",
1167
+ log: "***** hover " + selectors.element_name + " *****\n",
1168
+ };
826
1169
  try {
827
- let element = await this._locate(selectors, info, _params);
828
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
829
- try {
830
- await this._highlightElements(element);
831
- await element.hover();
832
- await new Promise((resolve) => setTimeout(resolve, 1000));
833
- }
834
- catch (e) {
835
- //await this.closeUnexpectedPopups();
836
- info.log += "hover failed, will try again" + "\n";
837
- element = await this._locate(selectors, info, _params);
838
- await element.hover({ timeout: 10000 });
839
- await new Promise((resolve) => setTimeout(resolve, 1000));
840
- }
1170
+ await _preCommand(state, this);
1171
+ await performAction("hover", state.element, options, this, state, _params);
1172
+ await _screenshot(state, this);
841
1173
  await this.waitForPageLoad();
842
- return info;
1174
+ return state.info;
843
1175
  }
844
1176
  catch (e) {
845
- this.logger.error("hover failed " + info.log);
846
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
847
- info.screenshotPath = screenshotPath;
848
- Object.assign(e, { info: info });
849
- error = e;
850
- throw e;
1177
+ await _commandError(state, e, this);
851
1178
  }
852
1179
  finally {
853
- const endTime = Date.now();
854
- this._reportToWorld(world, {
855
- element_name: selectors.element_name,
856
- type: Types.HOVER,
857
- text: `Hover element`,
858
- screenshotId,
859
- result: error
860
- ? {
861
- status: "FAILED",
862
- startTime,
863
- endTime,
864
- message: error === null || error === void 0 ? void 0 : error.message,
865
- }
866
- : {
867
- status: "PASSED",
868
- startTime,
869
- endTime,
870
- },
871
- info: info,
872
- });
1180
+ await _commandFinally(state, this);
873
1181
  }
874
1182
  }
875
1183
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
876
- this._validateSelectors(selectors);
877
1184
  if (!values) {
878
1185
  throw new Error("values is null");
879
1186
  }
880
- const startTime = Date.now();
881
- let error = null;
882
- let screenshotId = null;
883
- let screenshotPath = null;
884
- const info = {};
885
- info.log = "";
886
- info.operation = "selectOptions";
887
- info.selectors = selectors;
1187
+ const state = {
1188
+ selectors,
1189
+ _params,
1190
+ options,
1191
+ world,
1192
+ value: values.toString(),
1193
+ type: Types.SELECT,
1194
+ text: `Select option: ${values}`,
1195
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1196
+ operation: "selectOption",
1197
+ log: "***** select option " + selectors.element_name + " *****\n",
1198
+ };
888
1199
  try {
889
- let element = await this._locate(selectors, info, _params);
890
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1200
+ await _preCommand(state, this);
891
1201
  try {
892
- await this._highlightElements(element);
893
- await element.selectOption(values);
1202
+ await state.element.selectOption(values);
894
1203
  }
895
1204
  catch (e) {
896
1205
  //await this.closeUnexpectedPopups();
897
- info.log += "selectOption failed, will try force" + "\n";
898
- await element.selectOption(values, { timeout: 10000, force: true });
1206
+ state.info.log += "selectOption failed, will try force" + "\n";
1207
+ await state.element.selectOption(values, { timeout: 10000, force: true });
899
1208
  }
900
1209
  await this.waitForPageLoad();
901
- return info;
1210
+ return state.info;
902
1211
  }
903
1212
  catch (e) {
904
- this.logger.error("selectOption failed " + info.log);
905
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
906
- info.screenshotPath = screenshotPath;
907
- Object.assign(e, { info: info });
908
- this.logger.info("click failed, will try next selector");
909
- error = e;
910
- throw e;
1213
+ await _commandError(state, e, this);
911
1214
  }
912
1215
  finally {
913
- const endTime = Date.now();
914
- this._reportToWorld(world, {
915
- element_name: selectors.element_name,
916
- type: Types.SELECT,
917
- text: `Select option: ${values}`,
918
- value: values.toString(),
919
- screenshotId,
920
- result: error
921
- ? {
922
- status: "FAILED",
923
- startTime,
924
- endTime,
925
- message: error === null || error === void 0 ? void 0 : error.message,
926
- }
927
- : {
928
- status: "PASSED",
929
- startTime,
930
- endTime,
931
- },
932
- info: info,
933
- });
1216
+ await _commandFinally(state, this);
934
1217
  }
935
1218
  }
936
1219
  async type(_value, _params = null, options = {}, world = null) {
937
- const startTime = Date.now();
938
- let error = null;
939
- let screenshotId = null;
940
- let screenshotPath = null;
941
- const info = {};
942
- info.log = "";
943
- info.operation = "type";
944
- _value = this._fixUsingParams(_value, _params);
945
- info.value = _value;
1220
+ const state = {
1221
+ value: _value,
1222
+ _params,
1223
+ options,
1224
+ world,
1225
+ locate: false,
1226
+ scroll: false,
1227
+ highlight: false,
1228
+ type: Types.TYPE_PRESS,
1229
+ text: `Type value: ${_value}`,
1230
+ _text: `Type value: ${_value}`,
1231
+ operation: "type",
1232
+ log: "",
1233
+ };
946
1234
  try {
947
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
948
- const valueSegment = _value.split("&&");
1235
+ await _preCommand(state, this);
1236
+ const valueSegment = state.value.split("&&");
949
1237
  for (let i = 0; i < valueSegment.length; i++) {
950
1238
  if (i > 0) {
951
1239
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -965,134 +1253,77 @@ class StableBrowser {
965
1253
  await this.page.keyboard.type(value);
966
1254
  }
967
1255
  }
968
- return info;
1256
+ return state.info;
969
1257
  }
970
1258
  catch (e) {
971
- //await this.closeUnexpectedPopups();
972
- this.logger.error("type failed " + info.log);
973
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
974
- info.screenshotPath = screenshotPath;
975
- Object.assign(e, { info: info });
976
- error = e;
977
- throw e;
1259
+ await _commandError(state, e, this);
978
1260
  }
979
1261
  finally {
980
- const endTime = Date.now();
981
- this._reportToWorld(world, {
982
- type: Types.TYPE_PRESS,
983
- screenshotId,
984
- value: _value,
985
- text: `type value: ${_value}`,
986
- result: error
987
- ? {
988
- status: "FAILED",
989
- startTime,
990
- endTime,
991
- message: error === null || error === void 0 ? void 0 : error.message,
992
- }
993
- : {
994
- status: "PASSED",
995
- startTime,
996
- endTime,
997
- },
998
- info: info,
999
- });
1262
+ await _commandFinally(state, this);
1000
1263
  }
1001
1264
  }
1002
1265
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
1003
- // set input value for non fillable inputs like date, time, range, color, etc.
1004
- this._validateSelectors(selectors);
1005
- const startTime = Date.now();
1006
- const info = {};
1007
- info.log = "***** set input value " + selectors.element_name + " *****\n";
1008
- info.operation = "setInputValue";
1009
- info.selectors = selectors;
1010
- value = this._fixUsingParams(value, _params);
1011
- info.value = value;
1012
- let error = null;
1013
- let screenshotId = null;
1014
- let screenshotPath = null;
1266
+ const state = {
1267
+ selectors,
1268
+ _params,
1269
+ value,
1270
+ options,
1271
+ world,
1272
+ type: Types.SET_INPUT,
1273
+ text: `Set input value`,
1274
+ operation: "setInputValue",
1275
+ log: "***** set input value " + selectors.element_name + " *****\n",
1276
+ };
1015
1277
  try {
1016
- value = await this._replaceWithLocalData(value, this);
1017
- let element = await this._locate(selectors, info, _params);
1018
- await this.scrollIfNeeded(element, info);
1019
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1020
- await this._highlightElements(element);
1278
+ await _preCommand(state, this);
1279
+ let value = await this._replaceWithLocalData(state.value, this);
1021
1280
  try {
1022
- await element.evaluateHandle((el, value) => {
1281
+ await state.element.evaluateHandle((el, value) => {
1023
1282
  el.value = value;
1024
1283
  }, value);
1025
1284
  }
1026
1285
  catch (error) {
1027
1286
  this.logger.error("setInputValue failed, will try again");
1028
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1029
- info.screenshotPath = screenshotPath;
1030
- Object.assign(error, { info: info });
1031
- await element.evaluateHandle((el, value) => {
1287
+ await _screenshot(state, this);
1288
+ Object.assign(error, { info: state.info });
1289
+ await state.element.evaluateHandle((el, value) => {
1032
1290
  el.value = value;
1033
1291
  });
1034
1292
  }
1035
1293
  }
1036
1294
  catch (e) {
1037
- this.logger.error("setInputValue failed " + info.log);
1038
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1039
- info.screenshotPath = screenshotPath;
1040
- Object.assign(e, { info: info });
1041
- error = e;
1042
- throw e;
1295
+ await _commandError(state, e, this);
1043
1296
  }
1044
1297
  finally {
1045
- const endTime = Date.now();
1046
- this._reportToWorld(world, {
1047
- element_name: selectors.element_name,
1048
- type: Types.SET_INPUT,
1049
- text: `Set input value`,
1050
- value: value,
1051
- screenshotId,
1052
- result: error
1053
- ? {
1054
- status: "FAILED",
1055
- startTime,
1056
- endTime,
1057
- message: error === null || error === void 0 ? void 0 : error.message,
1058
- }
1059
- : {
1060
- status: "PASSED",
1061
- startTime,
1062
- endTime,
1063
- },
1064
- info: info,
1065
- });
1298
+ await _commandFinally(state, this);
1066
1299
  }
1067
1300
  }
1068
1301
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1069
- this._validateSelectors(selectors);
1070
- const startTime = Date.now();
1071
- let error = null;
1072
- let screenshotId = null;
1073
- let screenshotPath = null;
1074
- const info = {};
1075
- info.log = "";
1076
- info.operation = Types.SET_DATE_TIME;
1077
- info.selectors = selectors;
1078
- info.value = value;
1302
+ const state = {
1303
+ selectors,
1304
+ _params,
1305
+ value: await this._replaceWithLocalData(value, this),
1306
+ options,
1307
+ world,
1308
+ type: Types.SET_DATE_TIME,
1309
+ text: `Set date time value: ${value}`,
1310
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1311
+ operation: "setDateTime",
1312
+ log: "***** set date time value " + selectors.element_name + " *****\n",
1313
+ throwError: false,
1314
+ };
1079
1315
  try {
1080
- value = await this._replaceWithLocalData(value, this);
1081
- let element = await this._locate(selectors, info, _params);
1082
- //insert red border around the element
1083
- await this.scrollIfNeeded(element, info);
1084
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1085
- await this._highlightElements(element);
1316
+ await _preCommand(state, this);
1086
1317
  try {
1087
- await element.click();
1318
+ await performAction("click", state.element, options, this, state, _params);
1088
1319
  await new Promise((resolve) => setTimeout(resolve, 500));
1089
1320
  if (format) {
1090
- value = dayjs(value).format(format);
1091
- await element.fill(value);
1321
+ state.value = dayjs(state.value).format(format);
1322
+ await state.element.fill(state.value);
1092
1323
  }
1093
1324
  else {
1094
- const dateTimeValue = await getDateTimeValue({ value, element });
1095
- await element.evaluateHandle((el, dateTimeValue) => {
1325
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1326
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1096
1327
  el.value = ""; // clear input
1097
1328
  el.value = dateTimeValue;
1098
1329
  }, dateTimeValue);
@@ -1105,20 +1336,19 @@ class StableBrowser {
1105
1336
  }
1106
1337
  catch (err) {
1107
1338
  //await this.closeUnexpectedPopups();
1108
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1339
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1109
1340
  this.logger.info("Trying again");
1110
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1111
- info.screenshotPath = screenshotPath;
1112
- Object.assign(err, { info: info });
1341
+ await _screenshot(state, this);
1342
+ Object.assign(err, { info: state.info });
1113
1343
  await element.click();
1114
1344
  await new Promise((resolve) => setTimeout(resolve, 500));
1115
1345
  if (format) {
1116
- value = dayjs(value).format(format);
1117
- await element.fill(value);
1346
+ state.value = dayjs(state.value).format(format);
1347
+ await state.element.fill(state.value);
1118
1348
  }
1119
1349
  else {
1120
- const dateTimeValue = await getDateTimeValue({ value, element });
1121
- await element.evaluateHandle((el, dateTimeValue) => {
1350
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1351
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1122
1352
  el.value = ""; // clear input
1123
1353
  el.value = dateTimeValue;
1124
1354
  }, dateTimeValue);
@@ -1131,84 +1361,63 @@ class StableBrowser {
1131
1361
  }
1132
1362
  }
1133
1363
  catch (e) {
1134
- error = e;
1135
- throw e;
1364
+ await _commandError(state, e, this);
1136
1365
  }
1137
1366
  finally {
1138
- const endTime = Date.now();
1139
- this._reportToWorld(world, {
1140
- element_name: selectors.element_name,
1141
- type: Types.SET_DATE_TIME,
1142
- screenshotId,
1143
- value: value,
1144
- text: `setDateTime input with value: ${value}`,
1145
- result: error
1146
- ? {
1147
- status: "FAILED",
1148
- startTime,
1149
- endTime,
1150
- message: error === null || error === void 0 ? void 0 : error.message,
1151
- }
1152
- : {
1153
- status: "PASSED",
1154
- startTime,
1155
- endTime,
1156
- },
1157
- info: info,
1158
- });
1367
+ await _commandFinally(state, this);
1159
1368
  }
1160
1369
  }
1161
1370
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
1162
- this._validateSelectors(selectors);
1163
- const startTime = Date.now();
1164
- let error = null;
1165
- let screenshotId = null;
1166
- let screenshotPath = null;
1167
- const info = {};
1168
- info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1169
- info.operation = "clickType";
1170
- info.selectors = selectors;
1371
+ _value = unEscapeString(_value);
1171
1372
  const newValue = await this._replaceWithLocalData(_value, world);
1373
+ const state = {
1374
+ selectors,
1375
+ _params,
1376
+ value: newValue,
1377
+ originalValue: _value,
1378
+ options,
1379
+ world,
1380
+ type: Types.FILL,
1381
+ text: `Click type input with value: ${_value}`,
1382
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1383
+ operation: "clickType",
1384
+ log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1385
+ };
1386
+ if (!options) {
1387
+ options = {};
1388
+ }
1172
1389
  if (newValue !== _value) {
1173
1390
  //this.logger.info(_value + "=" + newValue);
1174
1391
  _value = newValue;
1175
1392
  }
1176
- info.value = _value;
1177
1393
  try {
1178
- let element = await this._locate(selectors, info, _params);
1179
- //insert red border around the element
1180
- await this.scrollIfNeeded(element, info);
1181
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1182
- await this._highlightElements(element);
1183
- if (options === null || options === undefined || !options.press) {
1394
+ await _preCommand(state, this);
1395
+ state.info.value = _value;
1396
+ if (!options.press) {
1184
1397
  try {
1185
- let currentValue = await element.inputValue();
1398
+ let currentValue = await state.element.inputValue();
1186
1399
  if (currentValue) {
1187
- await element.fill("");
1400
+ await state.element.fill("");
1188
1401
  }
1189
1402
  }
1190
1403
  catch (e) {
1191
1404
  this.logger.info("unable to clear input value");
1192
1405
  }
1193
1406
  }
1194
- if (options === null || options === undefined || options.press) {
1195
- try {
1196
- await element.click({ timeout: 5000 });
1197
- }
1198
- catch (e) {
1199
- await element.dispatchEvent("click");
1200
- }
1407
+ if (options.press) {
1408
+ options.timeout = 5000;
1409
+ await performAction("click", state.element, options, this, state, _params);
1201
1410
  }
1202
1411
  else {
1203
1412
  try {
1204
- await element.focus();
1413
+ await state.element.focus();
1205
1414
  }
1206
1415
  catch (e) {
1207
- await element.dispatchEvent("focus");
1416
+ await state.element.dispatchEvent("focus");
1208
1417
  }
1209
1418
  }
1210
1419
  await new Promise((resolve) => setTimeout(resolve, 500));
1211
- const valueSegment = _value.split("&&");
1420
+ const valueSegment = state.value.split("&&");
1212
1421
  for (let i = 0; i < valueSegment.length; i++) {
1213
1422
  if (i > 0) {
1214
1423
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1228,13 +1437,19 @@ class StableBrowser {
1228
1437
  await new Promise((resolve) => setTimeout(resolve, 500));
1229
1438
  }
1230
1439
  }
1440
+ await _screenshot(state, this);
1231
1441
  if (enter === true) {
1232
1442
  await new Promise((resolve) => setTimeout(resolve, 2000));
1233
1443
  await this.page.keyboard.press("Enter");
1234
1444
  await this.waitForPageLoad();
1235
1445
  }
1236
1446
  else if (enter === false) {
1237
- await element.dispatchEvent("change");
1447
+ try {
1448
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1449
+ }
1450
+ catch (e) {
1451
+ // ignore
1452
+ }
1238
1453
  //await this.page.keyboard.press("Tab");
1239
1454
  }
1240
1455
  else {
@@ -1243,111 +1458,95 @@ class StableBrowser {
1243
1458
  await this.waitForPageLoad();
1244
1459
  }
1245
1460
  }
1246
- return info;
1461
+ return state.info;
1247
1462
  }
1248
1463
  catch (e) {
1249
- //await this.closeUnexpectedPopups();
1250
- this.logger.error("fill failed " + JSON.stringify(info));
1251
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1252
- info.screenshotPath = screenshotPath;
1253
- Object.assign(e, { info: info });
1254
- error = e;
1255
- throw e;
1464
+ await _commandError(state, e, this);
1256
1465
  }
1257
1466
  finally {
1258
- const endTime = Date.now();
1259
- this._reportToWorld(world, {
1260
- element_name: selectors.element_name,
1261
- type: Types.FILL,
1262
- screenshotId,
1263
- value: _value,
1264
- text: `clickType input with value: ${_value}`,
1265
- result: error
1266
- ? {
1267
- status: "FAILED",
1268
- startTime,
1269
- endTime,
1270
- message: error === null || error === void 0 ? void 0 : error.message,
1271
- }
1272
- : {
1273
- status: "PASSED",
1274
- startTime,
1275
- endTime,
1276
- },
1277
- info: info,
1278
- });
1467
+ await _commandFinally(state, this);
1279
1468
  }
1280
1469
  }
1281
1470
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
1282
- this._validateSelectors(selectors);
1283
- const startTime = Date.now();
1284
- let error = null;
1285
- let screenshotId = null;
1286
- let screenshotPath = null;
1287
- const info = {};
1288
- info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1289
- info.operation = "fill";
1290
- info.selectors = selectors;
1291
- info.value = value;
1471
+ const state = {
1472
+ selectors,
1473
+ _params,
1474
+ value: unEscapeString(value),
1475
+ options,
1476
+ world,
1477
+ type: Types.FILL,
1478
+ text: `Fill input with value: ${value}`,
1479
+ operation: "fill",
1480
+ log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
1481
+ };
1292
1482
  try {
1293
- let element = await this._locate(selectors, info, _params);
1294
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1295
- await this._highlightElements(element);
1296
- await element.fill(value);
1297
- await element.dispatchEvent("change");
1483
+ await _preCommand(state, this);
1484
+ await state.element.fill(value);
1485
+ await state.element.dispatchEvent("change");
1298
1486
  if (enter) {
1299
1487
  await new Promise((resolve) => setTimeout(resolve, 2000));
1300
1488
  await this.page.keyboard.press("Enter");
1301
1489
  }
1302
1490
  await this.waitForPageLoad();
1303
- return info;
1491
+ return state.info;
1304
1492
  }
1305
1493
  catch (e) {
1306
- //await this.closeUnexpectedPopups();
1307
- this.logger.error("fill failed " + info.log);
1308
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1309
- info.screenshotPath = screenshotPath;
1310
- Object.assign(e, { info: info });
1311
- error = e;
1312
- throw e;
1494
+ await _commandError(state, e, this);
1313
1495
  }
1314
1496
  finally {
1315
- const endTime = Date.now();
1316
- this._reportToWorld(world, {
1317
- element_name: selectors.element_name,
1318
- type: Types.FILL,
1319
- screenshotId,
1320
- value,
1321
- text: `Fill input with value: ${value}`,
1322
- result: error
1323
- ? {
1324
- status: "FAILED",
1325
- startTime,
1326
- endTime,
1327
- message: error === null || error === void 0 ? void 0 : error.message,
1328
- }
1329
- : {
1330
- status: "PASSED",
1331
- startTime,
1332
- endTime,
1333
- },
1334
- info: info,
1335
- });
1497
+ await _commandFinally(state, this);
1498
+ }
1499
+ }
1500
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1501
+ const state = {
1502
+ selectors,
1503
+ _params,
1504
+ files,
1505
+ value: '"' + files.join('", "') + '"',
1506
+ options,
1507
+ world,
1508
+ type: Types.SET_INPUT_FILES,
1509
+ text: `Set input files`,
1510
+ _text: `Set input files on ${selectors.element_name}`,
1511
+ operation: "setInputFiles",
1512
+ log: "***** set input files " + selectors.element_name + " *****\n",
1513
+ };
1514
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1515
+ try {
1516
+ await _preCommand(state, this);
1517
+ for (let i = 0; i < files.length; i++) {
1518
+ const file = files[i];
1519
+ const filePath = path.join(uploadsFolder, file);
1520
+ if (!fs.existsSync(filePath)) {
1521
+ throw new Error(`File not found: ${filePath}`);
1522
+ }
1523
+ state.files[i] = filePath;
1524
+ }
1525
+ await state.element.setInputFiles(files);
1526
+ return state.info;
1527
+ }
1528
+ catch (e) {
1529
+ await _commandError(state, e, this);
1530
+ }
1531
+ finally {
1532
+ await _commandFinally(state, this);
1336
1533
  }
1337
1534
  }
1338
1535
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1339
1536
  return await this._getText(selectors, 0, _params, options, info, world);
1340
1537
  }
1341
1538
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1342
- this._validateSelectors(selectors);
1539
+ const timeout = this._getFindElementTimeout(options);
1540
+ _validateSelectors(selectors);
1343
1541
  let screenshotId = null;
1344
1542
  let screenshotPath = null;
1345
1543
  if (!info.log) {
1346
1544
  info.log = "";
1545
+ info.locatorLog = new LocatorLog(selectors);
1347
1546
  }
1348
1547
  info.operation = "getText";
1349
1548
  info.selectors = selectors;
1350
- let element = await this._locate(selectors, info, _params);
1549
+ let element = await this._locate(selectors, info, _params, timeout);
1351
1550
  if (climb > 0) {
1352
1551
  const climbArray = [];
1353
1552
  for (let i = 0; i < climb; i++) {
@@ -1366,6 +1565,18 @@ class StableBrowser {
1366
1565
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1367
1566
  try {
1368
1567
  await this._highlightElements(element);
1568
+ // if (world && world.screenshot && !world.screenshotPath) {
1569
+ // // console.log(`Highlighting for get text while running from recorder`);
1570
+ // this._highlightElements(element)
1571
+ // .then(async () => {
1572
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1573
+ // this._unhighlightElements(element).then(
1574
+ // () => {}
1575
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1576
+ // );
1577
+ // })
1578
+ // .catch(e);
1579
+ // }
1369
1580
  const elementText = await element.innerText();
1370
1581
  return {
1371
1582
  text: elementText,
@@ -1377,188 +1588,214 @@ class StableBrowser {
1377
1588
  }
1378
1589
  catch (e) {
1379
1590
  //await this.closeUnexpectedPopups();
1380
- this.logger.info("no innerText will use textContent");
1591
+ this.logger.info("no innerText, will use textContent");
1381
1592
  const elementText = await element.textContent();
1382
1593
  return { text: elementText, screenshotId, screenshotPath, value: value };
1383
1594
  }
1384
1595
  }
1385
1596
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1386
- var _a;
1387
- this._validateSelectors(selectors);
1388
1597
  if (!pattern) {
1389
1598
  throw new Error("pattern is null");
1390
1599
  }
1391
1600
  if (!text) {
1392
1601
  throw new Error("text is null");
1393
1602
  }
1603
+ const state = {
1604
+ selectors,
1605
+ _params,
1606
+ pattern,
1607
+ value: pattern,
1608
+ options,
1609
+ world,
1610
+ locate: false,
1611
+ scroll: false,
1612
+ screenshot: false,
1613
+ highlight: false,
1614
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1615
+ text: `Verify element contains pattern: ${pattern}`,
1616
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1617
+ operation: "containsPattern",
1618
+ log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1619
+ };
1394
1620
  const newValue = await this._replaceWithLocalData(text, world);
1395
1621
  if (newValue !== text) {
1396
1622
  this.logger.info(text + "=" + newValue);
1397
1623
  text = newValue;
1398
1624
  }
1399
- const startTime = Date.now();
1400
- let error = null;
1401
- let screenshotId = null;
1402
- let screenshotPath = null;
1403
- const info = {};
1404
- info.log =
1405
- "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1406
- info.operation = "containsPattern";
1407
- info.selectors = selectors;
1408
- info.value = text;
1409
- info.pattern = pattern;
1410
1625
  let foundObj = null;
1411
1626
  try {
1412
- foundObj = await this._getText(selectors, 0, _params, options, info, world);
1627
+ await _preCommand(state, this);
1628
+ state.info.pattern = pattern;
1629
+ foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
1413
1630
  if (foundObj && foundObj.element) {
1414
- await this.scrollIfNeeded(foundObj.element, info);
1631
+ await this.scrollIfNeeded(foundObj.element, state.info);
1415
1632
  }
1416
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1633
+ await _screenshot(state, this);
1417
1634
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1418
1635
  pattern = pattern.replace("{text}", escapedText);
1419
1636
  let regex = new RegExp(pattern, "im");
1420
- if (!regex.test(foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) && !((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(text))) {
1421
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1637
+ if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
1638
+ state.info.foundText = foundObj?.text;
1422
1639
  throw new Error("element doesn't contain text " + text);
1423
1640
  }
1424
- return info;
1641
+ return state.info;
1425
1642
  }
1426
1643
  catch (e) {
1427
- //await this.closeUnexpectedPopups();
1428
- this.logger.error("verify element contains text failed " + info.log);
1429
- this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
1430
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1431
- info.screenshotPath = screenshotPath;
1432
- Object.assign(e, { info: info });
1433
- error = e;
1434
- throw e;
1644
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1645
+ await _commandError(state, e, this);
1435
1646
  }
1436
1647
  finally {
1437
- const endTime = Date.now();
1438
- this._reportToWorld(world, {
1439
- element_name: selectors.element_name,
1440
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1441
- value: pattern,
1442
- text: `Verify element contains pattern: ${pattern}`,
1443
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1444
- result: error
1445
- ? {
1446
- status: "FAILED",
1447
- startTime,
1448
- endTime,
1449
- message: error === null || error === void 0 ? void 0 : error.message,
1450
- }
1451
- : {
1452
- status: "PASSED",
1453
- startTime,
1454
- endTime,
1455
- },
1456
- info: info,
1457
- });
1648
+ await _commandFinally(state, this);
1458
1649
  }
1459
1650
  }
1460
1651
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1461
- var _a, _b, _c;
1462
- this._validateSelectors(selectors);
1652
+ const timeout = this._getFindElementTimeout(options);
1653
+ const startTime = Date.now();
1654
+ const state = {
1655
+ selectors,
1656
+ _params,
1657
+ value: text,
1658
+ options,
1659
+ world,
1660
+ locate: false,
1661
+ scroll: false,
1662
+ screenshot: false,
1663
+ highlight: false,
1664
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1665
+ text: `Verify element contains text: ${text}`,
1666
+ operation: "containsText",
1667
+ log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
1668
+ };
1463
1669
  if (!text) {
1464
1670
  throw new Error("text is null");
1465
1671
  }
1466
- const startTime = Date.now();
1467
- let error = null;
1468
- let screenshotId = null;
1469
- let screenshotPath = null;
1470
- const info = {};
1471
- info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1472
- info.operation = "containsText";
1473
- info.selectors = selectors;
1672
+ text = unEscapeString(text);
1474
1673
  const newValue = await this._replaceWithLocalData(text, world);
1475
1674
  if (newValue !== text) {
1476
1675
  this.logger.info(text + "=" + newValue);
1477
1676
  text = newValue;
1478
1677
  }
1479
- info.value = text;
1480
1678
  let foundObj = null;
1481
1679
  try {
1482
- foundObj = await this._getText(selectors, climb, _params, options, info, world);
1483
- if (foundObj && foundObj.element) {
1484
- await this.scrollIfNeeded(foundObj.element, info);
1485
- }
1486
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1487
- const dateAlternatives = findDateAlternatives(text);
1488
- const numberAlternatives = findNumberAlternatives(text);
1489
- if (dateAlternatives.date) {
1490
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1491
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
1492
- ((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
1493
- return info;
1680
+ while (Date.now() - startTime < timeout) {
1681
+ try {
1682
+ await _preCommand(state, this);
1683
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1684
+ if (foundObj && foundObj.element) {
1685
+ await this.scrollIfNeeded(foundObj.element, state.info);
1686
+ }
1687
+ await _screenshot(state, this);
1688
+ const dateAlternatives = findDateAlternatives(text);
1689
+ const numberAlternatives = findNumberAlternatives(text);
1690
+ if (dateAlternatives.date) {
1691
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1692
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1693
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1694
+ return state.info;
1695
+ }
1696
+ }
1494
1697
  }
1495
- }
1496
- throw new Error("element doesn't contain text " + text);
1497
- }
1498
- else if (numberAlternatives.number) {
1499
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1500
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(numberAlternatives.numbers[i])) ||
1501
- ((_b = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _b === void 0 ? void 0 : _b.includes(numberAlternatives.numbers[i]))) {
1502
- return info;
1698
+ else if (numberAlternatives.number) {
1699
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1700
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1701
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1702
+ return state.info;
1703
+ }
1704
+ }
1705
+ }
1706
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1707
+ return state.info;
1503
1708
  }
1504
1709
  }
1505
- throw new Error("element doesn't contain text " + text);
1506
- }
1507
- else if (!(foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(text)) && !((_c = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _c === void 0 ? void 0 : _c.includes(text))) {
1508
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1509
- info.value = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value;
1510
- throw new Error("element doesn't contain text " + text);
1710
+ catch (e) {
1711
+ // Log error but continue retrying until timeout is reached
1712
+ this.logger.warn("Retrying containsText due to: " + e.message);
1713
+ }
1714
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1511
1715
  }
1512
- return info;
1716
+ state.info.foundText = foundObj?.text;
1717
+ state.info.value = foundObj?.value;
1718
+ throw new Error("element doesn't contain text " + text);
1513
1719
  }
1514
1720
  catch (e) {
1515
- //await this.closeUnexpectedPopups();
1516
- this.logger.error("verify element contains text failed " + info.log);
1517
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1518
- info.screenshotPath = screenshotPath;
1519
- Object.assign(e, { info: info });
1520
- error = e;
1721
+ await _commandError(state, e, this);
1521
1722
  throw e;
1522
1723
  }
1523
1724
  finally {
1524
- const endTime = Date.now();
1525
- this._reportToWorld(world, {
1526
- element_name: selectors.element_name,
1527
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1528
- text: `Verify element contains text: ${text}`,
1529
- value: text,
1530
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1531
- result: error
1532
- ? {
1533
- status: "FAILED",
1534
- startTime,
1535
- endTime,
1536
- message: error === null || error === void 0 ? void 0 : error.message,
1537
- }
1538
- : {
1539
- status: "PASSED",
1540
- startTime,
1541
- endTime,
1542
- },
1543
- info: info,
1544
- });
1725
+ await _commandFinally(state, this);
1545
1726
  }
1546
1727
  }
1547
- _getDataFile(world = null) {
1548
- let dataFile = null;
1549
- if (world && world.reportFolder) {
1550
- dataFile = path.join(world.reportFolder, "data.json");
1728
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1729
+ const timeout = this._getFindElementTimeout(options);
1730
+ const startTime = Date.now();
1731
+ const state = {
1732
+ _params,
1733
+ value: referanceSnapshot,
1734
+ options,
1735
+ world,
1736
+ locate: false,
1737
+ scroll: false,
1738
+ screenshot: true,
1739
+ highlight: false,
1740
+ type: Types.SNAPSHOT_VALIDATION,
1741
+ text: `verify snapshot: ${referanceSnapshot}`,
1742
+ operation: "snapshotValidation",
1743
+ log: "***** verify snapshot *****\n",
1744
+ };
1745
+ if (!referanceSnapshot) {
1746
+ throw new Error("referanceSnapshot is null");
1747
+ }
1748
+ let text = null;
1749
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1750
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1551
1751
  }
1552
- else if (this.reportFolder) {
1553
- dataFile = path.join(this.reportFolder, "data.json");
1752
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1753
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1554
1754
  }
1555
- else if (this.context && this.context.reportFolder) {
1556
- dataFile = path.join(this.context.reportFolder, "data.json");
1755
+ else if (referanceSnapshot.startsWith("yaml:")) {
1756
+ text = referanceSnapshot.substring(5);
1557
1757
  }
1558
1758
  else {
1559
- dataFile = "data.json";
1759
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1760
+ }
1761
+ state.text = text;
1762
+ const newValue = await this._replaceWithLocalData(text, world);
1763
+ await _preCommand(state, this);
1764
+ let foundObj = null;
1765
+ try {
1766
+ let matchResult = null;
1767
+ while (Date.now() - startTime < timeout) {
1768
+ try {
1769
+ let scope = null;
1770
+ if (!frameSelectors) {
1771
+ scope = this.page;
1772
+ }
1773
+ else {
1774
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1775
+ }
1776
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1777
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1778
+ if (matchResult.errorLine !== -1) {
1779
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1780
+ }
1781
+ // highlight and screenshot
1782
+ return state.info;
1783
+ }
1784
+ catch (e) {
1785
+ // Log error but continue retrying until timeout is reached
1786
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1787
+ }
1788
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1789
+ }
1790
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1791
+ }
1792
+ catch (e) {
1793
+ await _commandError(state, e, this);
1794
+ throw e;
1795
+ }
1796
+ finally {
1797
+ await _commandFinally(state, this);
1560
1798
  }
1561
- return dataFile;
1562
1799
  }
1563
1800
  async waitForUserInput(message, world = null) {
1564
1801
  if (!message) {
@@ -1588,13 +1825,22 @@ class StableBrowser {
1588
1825
  return;
1589
1826
  }
1590
1827
  // if data file exists, load it
1591
- const dataFile = this._getDataFile(world);
1828
+ const dataFile = _getDataFile(world, this.context, this);
1592
1829
  let data = this.getTestData(world);
1593
1830
  // merge the testData with the existing data
1594
1831
  Object.assign(data, testData);
1595
1832
  // save the data to the file
1596
1833
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1597
1834
  }
1835
+ overwriteTestData(testData, world = null) {
1836
+ if (!testData) {
1837
+ return;
1838
+ }
1839
+ // if data file exists, load it
1840
+ const dataFile = _getDataFile(world, this.context, this);
1841
+ // save the data to the file
1842
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1843
+ }
1598
1844
  _getDataFilePath(fileName) {
1599
1845
  let dataFile = path.join(this.project_path, "data", fileName);
1600
1846
  if (fs.existsSync(dataFile)) {
@@ -1691,7 +1937,7 @@ class StableBrowser {
1691
1937
  }
1692
1938
  }
1693
1939
  getTestData(world = null) {
1694
- const dataFile = this._getDataFile(world);
1940
+ const dataFile = _getDataFile(world, this.context, this);
1695
1941
  let data = {};
1696
1942
  if (fs.existsSync(dataFile)) {
1697
1943
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1723,11 +1969,9 @@ class StableBrowser {
1723
1969
  if (!fs.existsSync(world.screenshotPath)) {
1724
1970
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1725
1971
  }
1726
- let nextIndex = 1;
1727
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1728
- nextIndex++;
1729
- }
1730
- const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
1972
+ // to make sure the path doesn't start with -
1973
+ const uuidStr = "id_" + randomUUID();
1974
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1731
1975
  try {
1732
1976
  await this.takeScreenshot(screenshotPath);
1733
1977
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1737,15 +1981,15 @@ class StableBrowser {
1737
1981
  // this.logger.info("unable to save screenshot " + screenshotPath);
1738
1982
  // }
1739
1983
  // });
1984
+ result.screenshotId = uuidStr;
1985
+ result.screenshotPath = screenshotPath;
1986
+ if (info && info.box) {
1987
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1988
+ }
1740
1989
  }
1741
1990
  catch (e) {
1742
1991
  this.logger.info("unable to take screenshot, ignored");
1743
1992
  }
1744
- result.screenshotId = nextIndex;
1745
- result.screenshotPath = screenshotPath;
1746
- if (info && info.box) {
1747
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1748
- }
1749
1993
  }
1750
1994
  else if (options && options.screenshot) {
1751
1995
  result.screenshotPath = options.screenshotPath;
@@ -1770,7 +2014,6 @@ class StableBrowser {
1770
2014
  }
1771
2015
  async takeScreenshot(screenshotPath) {
1772
2016
  const playContext = this.context.playContext;
1773
- const client = await playContext.newCDPSession(this.page);
1774
2017
  // Using CDP to capture the screenshot
1775
2018
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
1776
2019
  document.body.scrollWidth,
@@ -1780,164 +2023,241 @@ class StableBrowser {
1780
2023
  document.body.clientWidth,
1781
2024
  document.documentElement.clientWidth,
1782
2025
  ])));
1783
- const viewportHeight = Math.max(...(await this.page.evaluate(() => [
1784
- document.body.scrollHeight,
1785
- document.documentElement.scrollHeight,
1786
- document.body.offsetHeight,
1787
- document.documentElement.offsetHeight,
1788
- document.body.clientHeight,
1789
- document.documentElement.clientHeight,
1790
- ])));
1791
- const { data } = await client.send("Page.captureScreenshot", {
1792
- format: "png",
1793
- // clip: {
1794
- // x: 0,
1795
- // y: 0,
1796
- // width: viewportWidth,
1797
- // height: viewportHeight,
1798
- // scale: 1,
1799
- // },
1800
- });
1801
- if (!screenshotPath) {
1802
- return data;
1803
- }
1804
- let screenshotBuffer = Buffer.from(data, "base64");
1805
- const sharpBuffer = sharp(screenshotBuffer);
1806
- const metadata = await sharpBuffer.metadata();
1807
- //check if you are on retina display and reduce the quality of the image
1808
- if (metadata.width > viewportWidth || metadata.height > viewportHeight) {
1809
- screenshotBuffer = await sharpBuffer
1810
- .resize(viewportWidth, viewportHeight, {
1811
- fit: sharp.fit.inside,
1812
- withoutEnlargement: true,
1813
- })
1814
- .toBuffer();
1815
- }
1816
- fs.writeFileSync(screenshotPath, screenshotBuffer);
1817
- await client.detach();
2026
+ let screenshotBuffer = null;
2027
+ // if (focusedElement) {
2028
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2029
+ // await this._unhighlightElements(focusedElement);
2030
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2031
+ // console.log(`Unhighlighted previous element`);
2032
+ // }
2033
+ // if (focusedElement) {
2034
+ // await this._highlightElements(focusedElement);
2035
+ // }
2036
+ if (this.context.browserName === "chromium") {
2037
+ const client = await playContext.newCDPSession(this.page);
2038
+ const { data } = await client.send("Page.captureScreenshot", {
2039
+ format: "png",
2040
+ // clip: {
2041
+ // x: 0,
2042
+ // y: 0,
2043
+ // width: viewportWidth,
2044
+ // height: viewportHeight,
2045
+ // scale: 1,
2046
+ // },
2047
+ });
2048
+ await client.detach();
2049
+ if (!screenshotPath) {
2050
+ return data;
2051
+ }
2052
+ screenshotBuffer = Buffer.from(data, "base64");
2053
+ }
2054
+ else {
2055
+ screenshotBuffer = await this.page.screenshot();
2056
+ }
2057
+ // if (focusedElement) {
2058
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2059
+ // await this._unhighlightElements(focusedElement);
2060
+ // }
2061
+ let image = await Jimp.read(screenshotBuffer);
2062
+ // Get the image dimensions
2063
+ const { width, height } = image.bitmap;
2064
+ const resizeRatio = viewportWidth / width;
2065
+ // Resize the image to fit within the viewport dimensions without enlarging
2066
+ if (width > viewportWidth) {
2067
+ image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
2068
+ await image.write(screenshotPath);
2069
+ }
2070
+ else {
2071
+ fs.writeFileSync(screenshotPath, screenshotBuffer);
2072
+ }
2073
+ return screenshotBuffer;
1818
2074
  }
1819
2075
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1820
- this._validateSelectors(selectors);
1821
- const startTime = Date.now();
1822
- let error = null;
1823
- let screenshotId = null;
1824
- let screenshotPath = null;
2076
+ const state = {
2077
+ selectors,
2078
+ _params,
2079
+ options,
2080
+ world,
2081
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2082
+ text: `Verify element exists in page`,
2083
+ operation: "verifyElementExistInPage",
2084
+ log: "***** verify element " + selectors.element_name + " exists in page *****\n",
2085
+ };
1825
2086
  await new Promise((resolve) => setTimeout(resolve, 2000));
1826
- const info = {};
1827
- info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1828
- info.operation = "verify";
1829
- info.selectors = selectors;
1830
2087
  try {
1831
- const element = await this._locate(selectors, info, _params);
1832
- if (element) {
1833
- await this.scrollIfNeeded(element, info);
1834
- }
1835
- await this._highlightElements(element);
1836
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1837
- await expect(element).toHaveCount(1, { timeout: 10000 });
1838
- return info;
2088
+ await _preCommand(state, this);
2089
+ await expect(state.element).toHaveCount(1, { timeout: 10000 });
2090
+ return state.info;
1839
2091
  }
1840
2092
  catch (e) {
1841
- //await this.closeUnexpectedPopups();
1842
- this.logger.error("verify failed " + info.log);
1843
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1844
- info.screenshotPath = screenshotPath;
1845
- Object.assign(e, { info: info });
1846
- error = e;
1847
- throw e;
2093
+ await _commandError(state, e, this);
1848
2094
  }
1849
2095
  finally {
1850
- const endTime = Date.now();
1851
- this._reportToWorld(world, {
1852
- element_name: selectors.element_name,
1853
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1854
- text: "Verify element exists in page",
1855
- screenshotId,
1856
- result: error
1857
- ? {
1858
- status: "FAILED",
1859
- startTime,
1860
- endTime,
1861
- message: error === null || error === void 0 ? void 0 : error.message,
1862
- }
1863
- : {
1864
- status: "PASSED",
1865
- startTime,
1866
- endTime,
1867
- },
1868
- info: info,
1869
- });
2096
+ await _commandFinally(state, this);
1870
2097
  }
1871
2098
  }
1872
2099
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1873
- this._validateSelectors(selectors);
1874
- const startTime = Date.now();
1875
- let error = null;
1876
- let screenshotId = null;
1877
- let screenshotPath = null;
2100
+ const state = {
2101
+ selectors,
2102
+ _params,
2103
+ attribute,
2104
+ variable,
2105
+ options,
2106
+ world,
2107
+ type: Types.EXTRACT,
2108
+ text: `Extract attribute from element`,
2109
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
2110
+ operation: "extractAttribute",
2111
+ log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
2112
+ allowDisabled: true,
2113
+ };
1878
2114
  await new Promise((resolve) => setTimeout(resolve, 2000));
1879
- const info = {};
1880
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1881
- info.operation = "extract";
1882
- info.selectors = selectors;
1883
2115
  try {
1884
- const element = await this._locate(selectors, info, _params);
1885
- await this._highlightElements(element);
1886
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2116
+ await _preCommand(state, this);
1887
2117
  switch (attribute) {
1888
2118
  case "inner_text":
1889
- info.value = await element.innerText();
2119
+ state.value = await state.element.innerText();
1890
2120
  break;
1891
2121
  case "href":
1892
- info.value = await element.getAttribute("href");
2122
+ state.value = await state.element.getAttribute("href");
1893
2123
  break;
1894
2124
  case "value":
1895
- info.value = await element.inputValue();
2125
+ state.value = await state.element.inputValue();
2126
+ break;
2127
+ case "text":
2128
+ state.value = await state.element.textContent();
1896
2129
  break;
1897
2130
  default:
1898
- info.value = await element.getAttribute(attribute);
2131
+ state.value = await state.element.getAttribute(attribute);
1899
2132
  break;
1900
2133
  }
1901
- this[variable] = info.value;
1902
- if (world) {
1903
- world[variable] = info.value;
2134
+ if (options !== null) {
2135
+ if (options.regex && options.regex !== "") {
2136
+ // Construct a regex pattern from the provided string
2137
+ const regex = options.regex.slice(1, -1);
2138
+ const regexPattern = new RegExp(regex, "g");
2139
+ const matches = state.value.match(regexPattern);
2140
+ if (matches) {
2141
+ let newValue = "";
2142
+ for (const match of matches) {
2143
+ newValue += match;
2144
+ }
2145
+ state.value = newValue;
2146
+ }
2147
+ }
2148
+ if (options.trimSpaces && options.trimSpaces === true) {
2149
+ state.value = state.value.trim();
2150
+ }
1904
2151
  }
1905
- this.setTestData({ [variable]: info.value }, world);
1906
- this.logger.info("set test data: " + variable + "=" + info.value);
1907
- return info;
2152
+ state.info.value = state.value;
2153
+ this.setTestData({ [variable]: state.value }, world);
2154
+ this.logger.info("set test data: " + variable + "=" + state.value);
2155
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2156
+ return state.info;
1908
2157
  }
1909
2158
  catch (e) {
1910
- //await this.closeUnexpectedPopups();
1911
- this.logger.error("extract failed " + info.log);
1912
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1913
- info.screenshotPath = screenshotPath;
1914
- Object.assign(e, { info: info });
1915
- error = e;
1916
- throw e;
2159
+ await _commandError(state, e, this);
1917
2160
  }
1918
2161
  finally {
1919
- const endTime = Date.now();
1920
- this._reportToWorld(world, {
1921
- element_name: selectors.element_name,
1922
- type: Types.EXTRACT_ATTRIBUTE,
1923
- variable: variable,
1924
- value: info.value,
1925
- text: "Extract attribute from element",
1926
- screenshotId,
1927
- result: error
1928
- ? {
1929
- status: "FAILED",
1930
- startTime,
1931
- endTime,
1932
- message: error === null || error === void 0 ? void 0 : error.message,
2162
+ await _commandFinally(state, this);
2163
+ }
2164
+ }
2165
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2166
+ const state = {
2167
+ selectors,
2168
+ _params,
2169
+ attribute,
2170
+ value,
2171
+ options,
2172
+ world,
2173
+ type: Types.VERIFY_ATTRIBUTE,
2174
+ highlight: true,
2175
+ screenshot: true,
2176
+ text: `Verify element attribute`,
2177
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2178
+ operation: "verifyAttribute",
2179
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2180
+ allowDisabled: true,
2181
+ };
2182
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2183
+ let val;
2184
+ let expectedValue;
2185
+ try {
2186
+ await _preCommand(state, this);
2187
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2188
+ state.info.expectedValue = expectedValue;
2189
+ switch (attribute) {
2190
+ case "innerText":
2191
+ val = String(await state.element.innerText());
2192
+ break;
2193
+ case "text":
2194
+ val = String(await state.element.textContent());
2195
+ break;
2196
+ case "value":
2197
+ val = String(await state.element.inputValue());
2198
+ break;
2199
+ case "checked":
2200
+ val = String(await state.element.isChecked());
2201
+ break;
2202
+ case "disabled":
2203
+ val = String(await state.element.isDisabled());
2204
+ break;
2205
+ case "readOnly":
2206
+ const isEditable = await state.element.isEditable();
2207
+ val = String(!isEditable);
2208
+ break;
2209
+ default:
2210
+ val = String(await state.element.getAttribute(attribute));
2211
+ break;
2212
+ }
2213
+ state.info.value = val;
2214
+ let regex;
2215
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2216
+ const patternBody = expectedValue.slice(1, -1);
2217
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2218
+ regex = new RegExp(processedPattern, "gs");
2219
+ state.info.regex = true;
2220
+ }
2221
+ else {
2222
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2223
+ regex = new RegExp(escapedPattern, "g");
2224
+ }
2225
+ if (attribute === "innerText") {
2226
+ if (state.info.regex) {
2227
+ if (!regex.test(val)) {
2228
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2229
+ state.info.failCause.assertionFailed = true;
2230
+ state.info.failCause.lastError = errorMessage;
2231
+ throw new Error(errorMessage);
1933
2232
  }
1934
- : {
1935
- status: "PASSED",
1936
- startTime,
1937
- endTime,
1938
- },
1939
- info: info,
1940
- });
2233
+ }
2234
+ else {
2235
+ const valLines = val.split("\n");
2236
+ const expectedLines = expectedValue.split("\n");
2237
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2238
+ if (!isPart) {
2239
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2240
+ state.info.failCause.assertionFailed = true;
2241
+ state.info.failCause.lastError = errorMessage;
2242
+ throw new Error(errorMessage);
2243
+ }
2244
+ }
2245
+ }
2246
+ else {
2247
+ if (!val.match(regex)) {
2248
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2249
+ state.info.failCause.assertionFailed = true;
2250
+ state.info.failCause.lastError = errorMessage;
2251
+ throw new Error(errorMessage);
2252
+ }
2253
+ }
2254
+ return state.info;
2255
+ }
2256
+ catch (e) {
2257
+ await _commandError(state, e, this);
2258
+ }
2259
+ finally {
2260
+ await _commandFinally(state, this);
1941
2261
  }
1942
2262
  }
1943
2263
  async extractEmailData(emailAddress, options, world) {
@@ -1958,7 +2278,7 @@ class StableBrowser {
1958
2278
  if (options && options.timeout) {
1959
2279
  timeout = options.timeout;
1960
2280
  }
1961
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2281
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1962
2282
  const request = {
1963
2283
  method: "POST",
1964
2284
  url: serviceUrl,
@@ -2014,7 +2334,8 @@ class StableBrowser {
2014
2334
  catch (e) {
2015
2335
  errorCount++;
2016
2336
  if (errorCount > 3) {
2017
- throw e;
2337
+ // throw e;
2338
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
2018
2339
  }
2019
2340
  // ignore
2020
2341
  }
@@ -2028,27 +2349,32 @@ class StableBrowser {
2028
2349
  async _highlightElements(scope, css) {
2029
2350
  try {
2030
2351
  if (!scope) {
2352
+ // console.log(`Scope is not defined`);
2031
2353
  return;
2032
2354
  }
2033
2355
  if (!css) {
2034
2356
  scope
2035
2357
  .evaluate((node) => {
2036
2358
  if (node && node.style) {
2037
- let originalBorder = node.style.border;
2038
- node.style.border = "2px solid red";
2359
+ let originalOutline = node.style.outline;
2360
+ // console.log(`Original outline was: ${originalOutline}`);
2361
+ // node.__previousOutline = originalOutline;
2362
+ node.style.outline = "2px solid red";
2363
+ // console.log(`New outline is: ${node.style.outline}`);
2039
2364
  if (window) {
2040
2365
  window.addEventListener("beforeunload", function (e) {
2041
- node.style.border = originalBorder;
2366
+ node.style.outline = originalOutline;
2042
2367
  });
2043
2368
  }
2044
2369
  setTimeout(function () {
2045
- node.style.border = originalBorder;
2370
+ node.style.outline = originalOutline;
2046
2371
  }, 2000);
2047
2372
  }
2048
2373
  })
2049
2374
  .then(() => { })
2050
2375
  .catch((e) => {
2051
2376
  // ignore
2377
+ // console.error(`Could not highlight node : ${e}`);
2052
2378
  });
2053
2379
  }
2054
2380
  else {
@@ -2064,17 +2390,18 @@ class StableBrowser {
2064
2390
  if (!element.style) {
2065
2391
  return;
2066
2392
  }
2067
- var originalBorder = element.style.border;
2393
+ let originalOutline = element.style.outline;
2394
+ element.__previousOutline = originalOutline;
2068
2395
  // Set the new border to be red and 2px solid
2069
- element.style.border = "2px solid red";
2396
+ element.style.outline = "2px solid red";
2070
2397
  if (window) {
2071
2398
  window.addEventListener("beforeunload", function (e) {
2072
- element.style.border = originalBorder;
2399
+ element.style.outline = originalOutline;
2073
2400
  });
2074
2401
  }
2075
2402
  // Set a timeout to revert to the original border after 2 seconds
2076
2403
  setTimeout(function () {
2077
- element.style.border = originalBorder;
2404
+ element.style.outline = originalOutline;
2078
2405
  }, 2000);
2079
2406
  }
2080
2407
  return;
@@ -2082,6 +2409,7 @@ class StableBrowser {
2082
2409
  .then(() => { })
2083
2410
  .catch((e) => {
2084
2411
  // ignore
2412
+ // console.error(`Could not highlight css: ${e}`);
2085
2413
  });
2086
2414
  }
2087
2415
  }
@@ -2089,173 +2417,563 @@ class StableBrowser {
2089
2417
  console.debug(error);
2090
2418
  }
2091
2419
  }
2092
- async verifyPagePath(pathPart, options = {}, world = null) {
2093
- const startTime = Date.now();
2094
- let error = null;
2095
- let screenshotId = null;
2096
- let screenshotPath = null;
2420
+ _matcher(text) {
2421
+ if (!text) {
2422
+ return { matcher: "contains", queryText: "" };
2423
+ }
2424
+ if (text.length < 2) {
2425
+ return { matcher: "contains", queryText: text };
2426
+ }
2427
+ const split = text.split(":");
2428
+ const matcher = split[0].toLowerCase();
2429
+ const queryText = split.slice(1).join(":").trim();
2430
+ return { matcher, queryText };
2431
+ }
2432
+ _getDomain(url) {
2433
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2434
+ return "";
2435
+ }
2436
+ let hostnameFragments = url.split("/")[2].split(".");
2437
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2438
+ return hostnameFragments.join("-").split(":").join("-");
2439
+ }
2440
+ let n = hostnameFragments.length;
2441
+ let fragments = [...hostnameFragments];
2442
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2443
+ hostnameFragments.pop();
2444
+ n = hostnameFragments.length;
2445
+ }
2446
+ if (n == 0) {
2447
+ if (fragments[0] === "www")
2448
+ fragments = fragments.slice(1);
2449
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2450
+ }
2451
+ if (hostnameFragments[0] === "www")
2452
+ hostnameFragments = hostnameFragments.slice(1);
2453
+ return hostnameFragments.join(".");
2454
+ }
2455
+ /**
2456
+ * Verify the page path matches the given path.
2457
+ * @param {string} pathPart - The path to verify.
2458
+ * @param {object} options - Options for verification.
2459
+ * @param {object} world - The world context.
2460
+ * @returns {Promise<object>} - The state info after verification.
2461
+ */
2462
+ async verifyPagePath(pathPart, options = {}, world = null) {
2463
+ let error = null;
2464
+ let screenshotId = null;
2465
+ let screenshotPath = null;
2466
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2467
+ const info = {};
2468
+ info.log = "***** verify page path " + pathPart + " *****\n";
2469
+ info.operation = "verifyPagePath";
2470
+ const newValue = await this._replaceWithLocalData(pathPart, world);
2471
+ if (newValue !== pathPart) {
2472
+ this.logger.info(pathPart + "=" + newValue);
2473
+ pathPart = newValue;
2474
+ }
2475
+ info.pathPart = pathPart;
2476
+ const { matcher, queryText } = this._matcher(pathPart);
2477
+ const state = {
2478
+ text_search: queryText,
2479
+ options,
2480
+ world,
2481
+ locate: false,
2482
+ scroll: false,
2483
+ highlight: false,
2484
+ type: Types.VERIFY_PAGE_PATH,
2485
+ text: `Verify the page url is ${queryText}`,
2486
+ _text: `Verify the page url is ${queryText}`,
2487
+ operation: "verifyPagePath",
2488
+ log: "***** verify page url is " + queryText + " *****\n",
2489
+ };
2490
+ try {
2491
+ await _preCommand(state, this);
2492
+ state.info.text = queryText;
2493
+ for (let i = 0; i < 30; i++) {
2494
+ const url = await this.page.url();
2495
+ switch (matcher) {
2496
+ case "exact":
2497
+ if (url !== queryText) {
2498
+ if (i === 29) {
2499
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2500
+ }
2501
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2502
+ continue;
2503
+ }
2504
+ break;
2505
+ case "contains":
2506
+ if (!url.includes(queryText)) {
2507
+ if (i === 29) {
2508
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2509
+ }
2510
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2511
+ continue;
2512
+ }
2513
+ break;
2514
+ case "starts-with":
2515
+ {
2516
+ const domain = this._getDomain(url);
2517
+ if (domain.length > 0 && domain !== queryText) {
2518
+ if (i === 29) {
2519
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2520
+ }
2521
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2522
+ continue;
2523
+ }
2524
+ }
2525
+ break;
2526
+ case "ends-with":
2527
+ {
2528
+ const urlObj = new URL(url);
2529
+ let route = "/";
2530
+ if (urlObj.pathname !== "/") {
2531
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2532
+ }
2533
+ else {
2534
+ route = "/";
2535
+ }
2536
+ if (route !== queryText) {
2537
+ if (i === 29) {
2538
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2539
+ }
2540
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2541
+ continue;
2542
+ }
2543
+ }
2544
+ break;
2545
+ case "regex":
2546
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2547
+ if (!regex.test(url)) {
2548
+ if (i === 29) {
2549
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2550
+ }
2551
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2552
+ continue;
2553
+ }
2554
+ break;
2555
+ default:
2556
+ console.log("Unknown matching type, defaulting to contains matching");
2557
+ if (!url.includes(pathPart)) {
2558
+ if (i === 29) {
2559
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2560
+ }
2561
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2562
+ continue;
2563
+ }
2564
+ }
2565
+ await _screenshot(state, this);
2566
+ return state.info;
2567
+ }
2568
+ }
2569
+ catch (e) {
2570
+ state.info.failCause.lastError = e.message;
2571
+ state.info.failCause.assertionFailed = true;
2572
+ await _commandError(state, e, this);
2573
+ }
2574
+ finally {
2575
+ await _commandFinally(state, this);
2576
+ }
2577
+ }
2578
+ /**
2579
+ * Verify the page title matches the given title.
2580
+ * @param {string} title - The title to verify.
2581
+ * @param {object} options - Options for verification.
2582
+ * @param {object} world - The world context.
2583
+ * @returns {Promise<object>} - The state info after verification.
2584
+ */
2585
+ async verifyPageTitle(title, options = {}, world = null) {
2586
+ let error = null;
2587
+ let screenshotId = null;
2588
+ let screenshotPath = null;
2589
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2590
+ const newValue = await this._replaceWithLocalData(title, world);
2591
+ if (newValue !== title) {
2592
+ this.logger.info(title + "=" + newValue);
2593
+ title = newValue;
2594
+ }
2595
+ const { matcher, queryText } = this._matcher(title);
2596
+ const state = {
2597
+ text_search: queryText,
2598
+ options,
2599
+ world,
2600
+ locate: false,
2601
+ scroll: false,
2602
+ highlight: false,
2603
+ type: Types.VERIFY_PAGE_TITLE,
2604
+ text: `Verify the page title is ${queryText}`,
2605
+ _text: `Verify the page title is ${queryText}`,
2606
+ operation: "verifyPageTitle",
2607
+ log: "***** verify page title is " + queryText + " *****\n",
2608
+ };
2609
+ try {
2610
+ await _preCommand(state, this);
2611
+ state.info.text = queryText;
2612
+ for (let i = 0; i < 30; i++) {
2613
+ const foundTitle = await this.page.title();
2614
+ switch (matcher) {
2615
+ case "exact":
2616
+ if (foundTitle !== queryText) {
2617
+ if (i === 29) {
2618
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2619
+ }
2620
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2621
+ continue;
2622
+ }
2623
+ break;
2624
+ case "contains":
2625
+ if (!foundTitle.includes(queryText)) {
2626
+ if (i === 29) {
2627
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2628
+ }
2629
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2630
+ continue;
2631
+ }
2632
+ break;
2633
+ case "starts-with":
2634
+ if (!foundTitle.startsWith(queryText)) {
2635
+ if (i === 29) {
2636
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2637
+ }
2638
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2639
+ continue;
2640
+ }
2641
+ break;
2642
+ case "ends-with":
2643
+ if (!foundTitle.endsWith(queryText)) {
2644
+ if (i === 29) {
2645
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2646
+ }
2647
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2648
+ continue;
2649
+ }
2650
+ break;
2651
+ case "regex":
2652
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2653
+ if (!regex.test(foundTitle)) {
2654
+ if (i === 29) {
2655
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2656
+ }
2657
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2658
+ continue;
2659
+ }
2660
+ break;
2661
+ default:
2662
+ console.log("Unknown matching type, defaulting to contains matching");
2663
+ if (!foundTitle.includes(title)) {
2664
+ if (i === 29) {
2665
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2666
+ }
2667
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2668
+ continue;
2669
+ }
2670
+ }
2671
+ await _screenshot(state, this);
2672
+ return state.info;
2673
+ }
2674
+ }
2675
+ catch (e) {
2676
+ state.info.failCause.lastError = e.message;
2677
+ state.info.failCause.assertionFailed = true;
2678
+ await _commandError(state, e, this);
2679
+ }
2680
+ finally {
2681
+ await _commandFinally(state, this);
2682
+ }
2683
+ }
2684
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2685
+ const frames = this.page.frames();
2686
+ let results = [];
2687
+ // let ignoreCase = false;
2688
+ for (let i = 0; i < frames.length; i++) {
2689
+ if (dateAlternatives.date) {
2690
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
2691
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2692
+ result.frame = frames[i];
2693
+ results.push(result);
2694
+ }
2695
+ }
2696
+ else if (numberAlternatives.number) {
2697
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2698
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2699
+ result.frame = frames[i];
2700
+ results.push(result);
2701
+ }
2702
+ }
2703
+ else {
2704
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2705
+ result.frame = frames[i];
2706
+ results.push(result);
2707
+ }
2708
+ }
2709
+ state.info.results = results;
2710
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2711
+ return resultWithElementsFound;
2712
+ }
2713
+ async verifyTextExistInPage(text, options = {}, world = null) {
2714
+ text = unEscapeString(text);
2715
+ const state = {
2716
+ text_search: text,
2717
+ options,
2718
+ world,
2719
+ locate: false,
2720
+ scroll: false,
2721
+ highlight: false,
2722
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2723
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2724
+ _text: `Verify the text '${text}' exists in page`,
2725
+ operation: "verifyTextExistInPage",
2726
+ log: "***** verify text " + text + " exists in page *****\n",
2727
+ };
2728
+ if (testForRegex(text)) {
2729
+ text = text.replace(/\\"/g, '"');
2730
+ }
2731
+ const timeout = this._getFindElementTimeout(options);
2097
2732
  await new Promise((resolve) => setTimeout(resolve, 2000));
2098
- const info = {};
2099
- info.log = "***** verify page path " + pathPart + " *****\n";
2100
- info.operation = "verifyPagePath";
2101
- const newValue = await this._replaceWithLocalData(pathPart, world);
2102
- if (newValue !== pathPart) {
2103
- this.logger.info(pathPart + "=" + newValue);
2104
- pathPart = newValue;
2733
+ const newValue = await this._replaceWithLocalData(text, world);
2734
+ if (newValue !== text) {
2735
+ this.logger.info(text + "=" + newValue);
2736
+ text = newValue;
2105
2737
  }
2106
- info.pathPart = pathPart;
2738
+ let dateAlternatives = findDateAlternatives(text);
2739
+ let numberAlternatives = findNumberAlternatives(text);
2107
2740
  try {
2108
- for (let i = 0; i < 30; i++) {
2109
- const url = await this.page.url();
2110
- if (!url.includes(pathPart)) {
2111
- if (i === 29) {
2112
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2741
+ await _preCommand(state, this);
2742
+ state.info.text = text;
2743
+ while (true) {
2744
+ let resultWithElementsFound = {
2745
+ length: 0,
2746
+ };
2747
+ try {
2748
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2749
+ }
2750
+ catch (error) {
2751
+ // ignore
2752
+ }
2753
+ if (resultWithElementsFound.length === 0) {
2754
+ if (Date.now() - state.startTime > timeout) {
2755
+ throw new Error(`Text ${text} not found in page`);
2113
2756
  }
2114
2757
  await new Promise((resolve) => setTimeout(resolve, 1000));
2115
2758
  continue;
2116
2759
  }
2117
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2118
- return info;
2760
+ try {
2761
+ if (resultWithElementsFound[0].randomToken) {
2762
+ const frame = resultWithElementsFound[0].frame;
2763
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2764
+ await this._highlightElements(frame, dataAttribute);
2765
+ const element = await frame.locator(dataAttribute).first();
2766
+ if (element) {
2767
+ await this.scrollIfNeeded(element, state.info);
2768
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2769
+ }
2770
+ }
2771
+ await _screenshot(state, this);
2772
+ return state.info;
2773
+ }
2774
+ catch (error) {
2775
+ console.error(error);
2776
+ }
2119
2777
  }
2120
2778
  }
2121
2779
  catch (e) {
2122
- //await this.closeUnexpectedPopups();
2123
- this.logger.error("verify page path failed " + info.log);
2124
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2125
- info.screenshotPath = screenshotPath;
2126
- Object.assign(e, { info: info });
2127
- error = e;
2128
- throw e;
2780
+ await _commandError(state, e, this);
2129
2781
  }
2130
2782
  finally {
2131
- const endTime = Date.now();
2132
- this._reportToWorld(world, {
2133
- type: Types.VERIFY_PAGE_PATH,
2134
- text: "Verify page path",
2135
- screenshotId,
2136
- result: error
2137
- ? {
2138
- status: "FAILED",
2139
- startTime,
2140
- endTime,
2141
- message: error === null || error === void 0 ? void 0 : error.message,
2142
- }
2143
- : {
2144
- status: "PASSED",
2145
- startTime,
2146
- endTime,
2147
- },
2148
- info: info,
2149
- });
2783
+ await _commandFinally(state, this);
2150
2784
  }
2151
2785
  }
2152
- async verifyTextExistInPage(text, options = {}, world = null) {
2153
- const startTime = Date.now();
2154
- const timeout = this._getLoadTimeout(options);
2155
- let error = null;
2156
- let screenshotId = null;
2157
- let screenshotPath = null;
2786
+ async waitForTextToDisappear(text, options = {}, world = null) {
2787
+ text = unEscapeString(text);
2788
+ const state = {
2789
+ text_search: text,
2790
+ options,
2791
+ world,
2792
+ locate: false,
2793
+ scroll: false,
2794
+ highlight: false,
2795
+ type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2796
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2797
+ _text: `Verify the text '${text}' does not exist in page`,
2798
+ operation: "verifyTextNotExistInPage",
2799
+ log: "***** verify text " + text + " does not exist in page *****\n",
2800
+ };
2801
+ if (testForRegex(text)) {
2802
+ text = text.replace(/\\"/g, '"');
2803
+ }
2804
+ const timeout = this._getFindElementTimeout(options);
2158
2805
  await new Promise((resolve) => setTimeout(resolve, 2000));
2159
- const info = {};
2160
- info.log = "***** verify text " + text + " exists in page *****\n";
2161
- info.operation = "verifyTextExistInPage";
2162
2806
  const newValue = await this._replaceWithLocalData(text, world);
2163
2807
  if (newValue !== text) {
2164
2808
  this.logger.info(text + "=" + newValue);
2165
2809
  text = newValue;
2166
2810
  }
2167
- info.text = text;
2168
2811
  let dateAlternatives = findDateAlternatives(text);
2169
2812
  let numberAlternatives = findNumberAlternatives(text);
2170
2813
  try {
2814
+ await _preCommand(state, this);
2815
+ state.info.text = text;
2816
+ let resultWithElementsFound = {
2817
+ length: null, // initial cannot be 0
2818
+ };
2171
2819
  while (true) {
2172
- const frames = this.page.frames();
2173
- let results = [];
2174
- for (let i = 0; i < frames.length; i++) {
2175
- if (dateAlternatives.date) {
2176
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2177
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
2178
- result.frame = frames[i];
2179
- results.push(result);
2180
- }
2181
- }
2182
- else if (numberAlternatives.number) {
2183
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2184
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
2185
- result.frame = frames[i];
2186
- results.push(result);
2187
- }
2188
- }
2189
- else {
2190
- const result = await this._locateElementByText(frames[i], text, "*", true, {});
2191
- result.frame = frames[i];
2192
- results.push(result);
2193
- }
2820
+ try {
2821
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2822
+ }
2823
+ catch (error) {
2824
+ // ignore
2194
2825
  }
2195
- info.results = results;
2196
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2197
2826
  if (resultWithElementsFound.length === 0) {
2198
- if (Date.now() - startTime > timeout) {
2199
- throw new Error(`Text ${text} not found in page`);
2827
+ await _screenshot(state, this);
2828
+ return state.info;
2829
+ }
2830
+ if (Date.now() - state.startTime > timeout) {
2831
+ throw new Error(`Text ${text} found in page`);
2832
+ }
2833
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2834
+ }
2835
+ }
2836
+ catch (e) {
2837
+ await _commandError(state, e, this);
2838
+ }
2839
+ finally {
2840
+ await _commandFinally(state, this);
2841
+ }
2842
+ }
2843
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
2844
+ textAnchor = unEscapeString(textAnchor);
2845
+ textToVerify = unEscapeString(textToVerify);
2846
+ const state = {
2847
+ text_search: textToVerify,
2848
+ options,
2849
+ world,
2850
+ locate: false,
2851
+ scroll: false,
2852
+ highlight: false,
2853
+ type: Types.VERIFY_TEXT_WITH_RELATION,
2854
+ text: `Verify text with relation to another text`,
2855
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2856
+ operation: "verify_text_with_relation",
2857
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2858
+ };
2859
+ const timeout = this._getFindElementTimeout(options);
2860
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2861
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
2862
+ if (newValue !== textAnchor) {
2863
+ this.logger.info(textAnchor + "=" + newValue);
2864
+ textAnchor = newValue;
2865
+ }
2866
+ newValue = await this._replaceWithLocalData(textToVerify, world);
2867
+ if (newValue !== textToVerify) {
2868
+ this.logger.info(textToVerify + "=" + newValue);
2869
+ textToVerify = newValue;
2870
+ }
2871
+ let dateAlternatives = findDateAlternatives(textToVerify);
2872
+ let numberAlternatives = findNumberAlternatives(textToVerify);
2873
+ let foundAncore = false;
2874
+ try {
2875
+ await _preCommand(state, this);
2876
+ state.info.text = textToVerify;
2877
+ let resultWithElementsFound = {
2878
+ length: 0,
2879
+ };
2880
+ while (true) {
2881
+ try {
2882
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2883
+ }
2884
+ catch (error) {
2885
+ // ignore
2886
+ }
2887
+ if (resultWithElementsFound.length === 0) {
2888
+ if (Date.now() - state.startTime > timeout) {
2889
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2200
2890
  }
2201
2891
  await new Promise((resolve) => setTimeout(resolve, 1000));
2202
2892
  continue;
2203
2893
  }
2204
- if (resultWithElementsFound[0].randomToken) {
2205
- const frame = resultWithElementsFound[0].frame;
2206
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2207
- await this._highlightElements(frame, dataAttribute);
2208
- const element = await frame.$(dataAttribute);
2209
- if (element) {
2210
- await this.scrollIfNeeded(element, info);
2211
- await element.dispatchEvent("bvt_verify_page_contains_text");
2894
+ try {
2895
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2896
+ foundAncore = true;
2897
+ const result = resultWithElementsFound[i];
2898
+ const token = result.randomToken;
2899
+ const frame = result.frame;
2900
+ let css = `[data-blinq-id-${token}]`;
2901
+ const climbArray1 = [];
2902
+ for (let i = 0; i < climb; i++) {
2903
+ climbArray1.push("..");
2904
+ }
2905
+ let climbXpath = "xpath=" + climbArray1.join("/");
2906
+ css = css + " >> " + climbXpath;
2907
+ const count = await frame.locator(css).count();
2908
+ for (let j = 0; j < count; j++) {
2909
+ const continer = await frame.locator(css).nth(j);
2910
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2911
+ if (result.elementCount > 0) {
2912
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2913
+ await this._highlightElements(frame, dataAttribute);
2914
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2915
+ // if (world && world.screenshot && !world.screenshotPath) {
2916
+ // console.log(`Highlighting for vtrt while running from recorder`);
2917
+ // this._highlightElements(frame, dataAttribute)
2918
+ // .then(async () => {
2919
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2920
+ // this._unhighlightElements(frame, dataAttribute).then(
2921
+ // () => {}
2922
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2923
+ // );
2924
+ // })
2925
+ // .catch(e);
2926
+ // }
2927
+ //await this._highlightElements(frame, cssAnchor);
2928
+ const element = await frame.locator(dataAttribute).first();
2929
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2930
+ // await this._unhighlightElements(frame, dataAttribute);
2931
+ if (element) {
2932
+ await this.scrollIfNeeded(element, state.info);
2933
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2934
+ }
2935
+ await _screenshot(state, this);
2936
+ return state.info;
2937
+ }
2938
+ }
2212
2939
  }
2213
2940
  }
2214
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2215
- return info;
2941
+ catch (error) {
2942
+ console.error(error);
2943
+ }
2216
2944
  }
2217
2945
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2218
2946
  }
2219
2947
  catch (e) {
2220
- //await this.closeUnexpectedPopups();
2221
- this.logger.error("verify text exist in page failed " + info.log);
2222
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2223
- info.screenshotPath = screenshotPath;
2224
- Object.assign(e, { info: info });
2225
- error = e;
2226
- throw e;
2948
+ await _commandError(state, e, this);
2227
2949
  }
2228
2950
  finally {
2229
- const endTime = Date.now();
2230
- this._reportToWorld(world, {
2231
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2232
- text: "Verify text exists in page",
2233
- screenshotId,
2234
- result: error
2235
- ? {
2236
- status: "FAILED",
2237
- startTime,
2238
- endTime,
2239
- message: error === null || error === void 0 ? void 0 : error.message,
2240
- }
2241
- : {
2242
- status: "PASSED",
2243
- startTime,
2244
- endTime,
2245
- },
2246
- info: info,
2247
- });
2951
+ await _commandFinally(state, this);
2248
2952
  }
2249
2953
  }
2250
- _getServerUrl() {
2251
- let serviceUrl = "https://api.blinq.io";
2252
- if (process.env.NODE_ENV_BLINQ === "dev") {
2253
- serviceUrl = "https://dev.api.blinq.io";
2254
- }
2255
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2256
- serviceUrl = "https://stage.api.blinq.io";
2954
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2955
+ const frames = this.page.frames();
2956
+ let results = [];
2957
+ let ignoreCase = false;
2958
+ for (let i = 0; i < frames.length; i++) {
2959
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2960
+ result.frame = frames[i];
2961
+ const climbArray = [];
2962
+ for (let i = 0; i < climb; i++) {
2963
+ climbArray.push("..");
2964
+ }
2965
+ let climbXpath = "xpath=" + climbArray.join("/");
2966
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2967
+ const count = await frames[i].locator(newLocator).count();
2968
+ if (count > 0) {
2969
+ result.elementCount = count;
2970
+ result.locator = newLocator;
2971
+ results.push(result);
2972
+ }
2257
2973
  }
2258
- return serviceUrl;
2974
+ // state.info.results = results;
2975
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2976
+ return resultWithElementsFound;
2259
2977
  }
2260
2978
  async visualVerification(text, options = {}, world = null) {
2261
2979
  const startTime = Date.now();
@@ -2271,14 +2989,17 @@ class StableBrowser {
2271
2989
  throw new Error("TOKEN is not set");
2272
2990
  }
2273
2991
  try {
2274
- let serviceUrl = this._getServerUrl();
2992
+ let serviceUrl = _getServerUrl();
2275
2993
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2276
2994
  info.screenshotPath = screenshotPath;
2277
2995
  const screenshot = await this.takeScreenshot();
2278
- const request = {
2279
- method: "POST",
2996
+ let request = {
2997
+ method: "post",
2998
+ maxBodyLength: Infinity,
2280
2999
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2281
3000
  headers: {
3001
+ "x-bvt-project-id": path.basename(this.project_path),
3002
+ "x-source": "aaa",
2282
3003
  "Content-Type": "application/json",
2283
3004
  Authorization: `Bearer ${process.env.TOKEN}`,
2284
3005
  },
@@ -2287,7 +3008,7 @@ class StableBrowser {
2287
3008
  screenshot: screenshot,
2288
3009
  }),
2289
3010
  };
2290
- let result = await this.context.api.request(request);
3011
+ const result = await axios.request(request);
2291
3012
  if (result.data.status !== true) {
2292
3013
  throw new Error("Visual validation failed");
2293
3014
  }
@@ -2307,20 +3028,22 @@ class StableBrowser {
2307
3028
  info.screenshotPath = screenshotPath;
2308
3029
  Object.assign(e, { info: info });
2309
3030
  error = e;
2310
- throw e;
3031
+ // throw e;
3032
+ await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
2311
3033
  }
2312
3034
  finally {
2313
3035
  const endTime = Date.now();
2314
- this._reportToWorld(world, {
3036
+ _reportToWorld(world, {
2315
3037
  type: Types.VERIFY_VISUAL,
2316
3038
  text: "Visual verification",
3039
+ _text: "Visual verification of " + text,
2317
3040
  screenshotId,
2318
3041
  result: error
2319
3042
  ? {
2320
3043
  status: "FAILED",
2321
3044
  startTime,
2322
3045
  endTime,
2323
- message: error === null || error === void 0 ? void 0 : error.message,
3046
+ message: error?.message,
2324
3047
  }
2325
3048
  : {
2326
3049
  status: "PASSED",
@@ -2352,13 +3075,14 @@ class StableBrowser {
2352
3075
  this.logger.info("Table data verified");
2353
3076
  }
2354
3077
  async getTableData(selectors, _params = null, options = {}, world = null) {
2355
- this._validateSelectors(selectors);
3078
+ _validateSelectors(selectors);
2356
3079
  const startTime = Date.now();
2357
3080
  let error = null;
2358
3081
  let screenshotId = null;
2359
3082
  let screenshotPath = null;
2360
3083
  const info = {};
2361
3084
  info.log = "";
3085
+ info.locatorLog = new LocatorLog(selectors);
2362
3086
  info.operation = "getTableData";
2363
3087
  info.selectors = selectors;
2364
3088
  try {
@@ -2374,11 +3098,12 @@ class StableBrowser {
2374
3098
  info.screenshotPath = screenshotPath;
2375
3099
  Object.assign(e, { info: info });
2376
3100
  error = e;
2377
- throw e;
3101
+ // throw e;
3102
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2378
3103
  }
2379
3104
  finally {
2380
3105
  const endTime = Date.now();
2381
- this._reportToWorld(world, {
3106
+ _reportToWorld(world, {
2382
3107
  element_name: selectors.element_name,
2383
3108
  type: Types.GET_TABLE_DATA,
2384
3109
  text: "Get table data",
@@ -2388,7 +3113,7 @@ class StableBrowser {
2388
3113
  status: "FAILED",
2389
3114
  startTime,
2390
3115
  endTime,
2391
- message: error === null || error === void 0 ? void 0 : error.message,
3116
+ message: error?.message,
2392
3117
  }
2393
3118
  : {
2394
3119
  status: "PASSED",
@@ -2400,7 +3125,7 @@ class StableBrowser {
2400
3125
  }
2401
3126
  }
2402
3127
  async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
2403
- this._validateSelectors(selectors);
3128
+ _validateSelectors(selectors);
2404
3129
  if (!query) {
2405
3130
  throw new Error("query is null");
2406
3131
  }
@@ -2433,7 +3158,7 @@ class StableBrowser {
2433
3158
  info.operation = "analyzeTable";
2434
3159
  info.selectors = selectors;
2435
3160
  info.query = query;
2436
- query = this._fixUsingParams(query, _params);
3161
+ query = _fixUsingParams(query, _params);
2437
3162
  info.query_fixed = query;
2438
3163
  info.operator = operator;
2439
3164
  info.value = value;
@@ -2539,11 +3264,12 @@ class StableBrowser {
2539
3264
  info.screenshotPath = screenshotPath;
2540
3265
  Object.assign(e, { info: info });
2541
3266
  error = e;
2542
- throw e;
3267
+ // throw e;
3268
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2543
3269
  }
2544
3270
  finally {
2545
3271
  const endTime = Date.now();
2546
- this._reportToWorld(world, {
3272
+ _reportToWorld(world, {
2547
3273
  element_name: selectors.element_name,
2548
3274
  type: Types.ANALYZE_TABLE,
2549
3275
  text: "Analyze table",
@@ -2553,7 +3279,7 @@ class StableBrowser {
2553
3279
  status: "FAILED",
2554
3280
  startTime,
2555
3281
  endTime,
2556
- message: error === null || error === void 0 ? void 0 : error.message,
3282
+ message: error?.message,
2557
3283
  }
2558
3284
  : {
2559
3285
  status: "PASSED",
@@ -2565,27 +3291,13 @@ class StableBrowser {
2565
3291
  }
2566
3292
  }
2567
3293
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2568
- if (!value) {
2569
- return value;
2570
- }
2571
- // find all the accurance of {{(.*?)}} and replace with the value
2572
- let regex = /{{(.*?)}}/g;
2573
- let matches = value.match(regex);
2574
- if (matches) {
2575
- const testData = this.getTestData(world);
2576
- for (let i = 0; i < matches.length; i++) {
2577
- let match = matches[i];
2578
- let key = match.substring(2, match.length - 2);
2579
- let newValue = objectPath.get(testData, key, null);
2580
- if (newValue !== null) {
2581
- value = value.replace(match, newValue);
2582
- }
2583
- }
3294
+ try {
3295
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
2584
3296
  }
2585
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2586
- return await decrypt(value, null, totpWait);
3297
+ catch (error) {
3298
+ this.logger.debug(error);
3299
+ throw error;
2587
3300
  }
2588
- return value;
2589
3301
  }
2590
3302
  _getLoadTimeout(options) {
2591
3303
  let timeout = 15000;
@@ -2597,6 +3309,37 @@ class StableBrowser {
2597
3309
  }
2598
3310
  return timeout;
2599
3311
  }
3312
+ _getFindElementTimeout(options) {
3313
+ if (options && options.timeout) {
3314
+ return options.timeout;
3315
+ }
3316
+ if (this.configuration.find_element_timeout) {
3317
+ return this.configuration.find_element_timeout;
3318
+ }
3319
+ return 30000;
3320
+ }
3321
+ async saveStoreState(path = null, world = null) {
3322
+ const storageState = await this.page.context().storageState();
3323
+ path = await this._replaceWithLocalData(path, this.world);
3324
+ //const testDataFile = _getDataFile(world, this.context, this);
3325
+ if (path) {
3326
+ // save { storageState: storageState } into the path
3327
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3328
+ }
3329
+ else {
3330
+ await this.setTestData({ storageState: storageState }, world);
3331
+ }
3332
+ }
3333
+ async restoreSaveState(path = null, world = null) {
3334
+ path = await this._replaceWithLocalData(path, this.world);
3335
+ await refreshBrowser(this, path, world);
3336
+ this.registerEventListeners(this.context);
3337
+ registerNetworkEvents(this.world, this, this.context, this.page);
3338
+ registerDownloadEvent(this.page, this.world, this.context);
3339
+ if (this.onRestoreSaveState) {
3340
+ this.onRestoreSaveState(path);
3341
+ }
3342
+ }
2600
3343
  async waitForPageLoad(options = {}, world = null) {
2601
3344
  let timeout = this._getLoadTimeout(options);
2602
3345
  const promiseArray = [];
@@ -2636,7 +3379,7 @@ class StableBrowser {
2636
3379
  await new Promise((resolve) => setTimeout(resolve, 2000));
2637
3380
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2638
3381
  const endTime = Date.now();
2639
- this._reportToWorld(world, {
3382
+ _reportToWorld(world, {
2640
3383
  type: Types.GET_PAGE_STATUS,
2641
3384
  text: "Wait for page load",
2642
3385
  screenshotId,
@@ -2645,7 +3388,7 @@ class StableBrowser {
2645
3388
  status: "FAILED",
2646
3389
  startTime,
2647
3390
  endTime,
2648
- message: error === null || error === void 0 ? void 0 : error.message,
3391
+ message: error?.message,
2649
3392
  }
2650
3393
  : {
2651
3394
  status: "PASSED",
@@ -2656,41 +3399,123 @@ class StableBrowser {
2656
3399
  }
2657
3400
  }
2658
3401
  async closePage(options = {}, world = null) {
2659
- const startTime = Date.now();
2660
- let error = null;
2661
- let screenshotId = null;
2662
- let screenshotPath = null;
2663
- const info = {};
3402
+ const state = {
3403
+ options,
3404
+ world,
3405
+ locate: false,
3406
+ scroll: false,
3407
+ highlight: false,
3408
+ type: Types.CLOSE_PAGE,
3409
+ text: `Close page`,
3410
+ _text: `Close the page`,
3411
+ operation: "closePage",
3412
+ log: "***** close page *****\n",
3413
+ throwError: false,
3414
+ };
2664
3415
  try {
3416
+ await _preCommand(state, this);
2665
3417
  await this.page.close();
2666
3418
  }
2667
3419
  catch (e) {
2668
3420
  console.log(".");
3421
+ await _commandError(state, e, this);
2669
3422
  }
2670
3423
  finally {
2671
- await new Promise((resolve) => setTimeout(resolve, 2000));
2672
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2673
- const endTime = Date.now();
2674
- this._reportToWorld(world, {
2675
- type: Types.CLOSE_PAGE,
2676
- text: "close page",
2677
- screenshotId,
2678
- result: error
2679
- ? {
2680
- status: "FAILED",
2681
- startTime,
2682
- endTime,
2683
- message: error === null || error === void 0 ? void 0 : error.message,
3424
+ await _commandFinally(state, this);
3425
+ }
3426
+ }
3427
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3428
+ let operation = null;
3429
+ if (!options || !options.operation) {
3430
+ throw new Error("operation is not defined");
3431
+ }
3432
+ operation = options.operation;
3433
+ // validate operation is one of the supported operations
3434
+ if (operation != "click" && operation != "hover+click") {
3435
+ throw new Error("operation is not supported");
3436
+ }
3437
+ const state = {
3438
+ options,
3439
+ world,
3440
+ locate: false,
3441
+ scroll: false,
3442
+ highlight: false,
3443
+ type: Types.TABLE_OPERATION,
3444
+ text: `Table operation`,
3445
+ _text: `Table ${operation} operation`,
3446
+ operation: operation,
3447
+ log: "***** Table operation *****\n",
3448
+ };
3449
+ const timeout = this._getFindElementTimeout(options);
3450
+ try {
3451
+ await _preCommand(state, this);
3452
+ const start = Date.now();
3453
+ let cellArea = null;
3454
+ while (true) {
3455
+ try {
3456
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3457
+ if (cellArea) {
3458
+ break;
2684
3459
  }
2685
- : {
2686
- status: "PASSED",
2687
- startTime,
2688
- endTime,
2689
- },
2690
- info: info,
2691
- });
3460
+ }
3461
+ catch (e) {
3462
+ // ignore
3463
+ }
3464
+ if (Date.now() - start > timeout) {
3465
+ throw new Error(`Cell not found in table`);
3466
+ }
3467
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3468
+ }
3469
+ switch (operation) {
3470
+ case "click":
3471
+ if (!options.css) {
3472
+ // will click in the center of the cell
3473
+ let xOffset = 0;
3474
+ let yOffset = 0;
3475
+ if (options.xOffset) {
3476
+ xOffset = options.xOffset;
3477
+ }
3478
+ if (options.yOffset) {
3479
+ yOffset = options.yOffset;
3480
+ }
3481
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3482
+ }
3483
+ else {
3484
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3485
+ if (results.length === 0) {
3486
+ throw new Error(`Element not found in cell area`);
3487
+ }
3488
+ state.element = results[0];
3489
+ await performAction("click", state.element, options, this, state, _params);
3490
+ }
3491
+ break;
3492
+ case "hover+click":
3493
+ if (!options.css) {
3494
+ throw new Error("css is not defined");
3495
+ }
3496
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3497
+ if (results.length === 0) {
3498
+ throw new Error(`Element not found in cell area`);
3499
+ }
3500
+ state.element = results[0];
3501
+ await performAction("hover+click", state.element, options, this, state, _params);
3502
+ break;
3503
+ default:
3504
+ throw new Error("operation is not supported");
3505
+ }
3506
+ }
3507
+ catch (e) {
3508
+ await _commandError(state, e, this);
3509
+ }
3510
+ finally {
3511
+ await _commandFinally(state, this);
2692
3512
  }
2693
3513
  }
3514
+ saveTestDataAsGlobal(options, world) {
3515
+ const dataFile = _getDataFile(world, this.context, this);
3516
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
3517
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
3518
+ }
2694
3519
  async setViewportSize(width, hight, options = {}, world = null) {
2695
3520
  const startTime = Date.now();
2696
3521
  let error = null;
@@ -2708,21 +3533,23 @@ class StableBrowser {
2708
3533
  }
2709
3534
  catch (e) {
2710
3535
  console.log(".");
3536
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2711
3537
  }
2712
3538
  finally {
2713
3539
  await new Promise((resolve) => setTimeout(resolve, 2000));
2714
3540
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2715
3541
  const endTime = Date.now();
2716
- this._reportToWorld(world, {
3542
+ _reportToWorld(world, {
2717
3543
  type: Types.SET_VIEWPORT,
2718
3544
  text: "set viewport size to " + width + "x" + hight,
3545
+ _text: "Set the viewport size to " + width + "x" + hight,
2719
3546
  screenshotId,
2720
3547
  result: error
2721
3548
  ? {
2722
3549
  status: "FAILED",
2723
3550
  startTime,
2724
3551
  endTime,
2725
- message: error === null || error === void 0 ? void 0 : error.message,
3552
+ message: error?.message,
2726
3553
  }
2727
3554
  : {
2728
3555
  status: "PASSED",
@@ -2744,12 +3571,13 @@ class StableBrowser {
2744
3571
  }
2745
3572
  catch (e) {
2746
3573
  console.log(".");
3574
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2747
3575
  }
2748
3576
  finally {
2749
3577
  await new Promise((resolve) => setTimeout(resolve, 2000));
2750
3578
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2751
3579
  const endTime = Date.now();
2752
- this._reportToWorld(world, {
3580
+ _reportToWorld(world, {
2753
3581
  type: Types.GET_PAGE_STATUS,
2754
3582
  text: "page relaod",
2755
3583
  screenshotId,
@@ -2758,7 +3586,7 @@ class StableBrowser {
2758
3586
  status: "FAILED",
2759
3587
  startTime,
2760
3588
  endTime,
2761
- message: error === null || error === void 0 ? void 0 : error.message,
3589
+ message: error?.message,
2762
3590
  }
2763
3591
  : {
2764
3592
  status: "PASSED",
@@ -2785,11 +3613,195 @@ class StableBrowser {
2785
3613
  console.log("#-#");
2786
3614
  }
2787
3615
  }
2788
- _reportToWorld(world, properties) {
2789
- if (!world || !world.attach) {
2790
- return;
3616
+ async beforeScenario(world, scenario) {
3617
+ this.beforeScenarioCalled = true;
3618
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3619
+ this.scenarioName = scenario.pickle.name;
3620
+ }
3621
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3622
+ this.featureName = scenario.gherkinDocument.feature.name;
3623
+ }
3624
+ if (this.context) {
3625
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3626
+ }
3627
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3628
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3629
+ // check if @global_test_data tag is present
3630
+ if (this.tags.includes("@global_test_data")) {
3631
+ this.saveTestDataAsGlobal({}, world);
3632
+ }
3633
+ }
3634
+ // update test data based on feature/scenario
3635
+ let envName = null;
3636
+ if (this.context && this.context.environment) {
3637
+ envName = this.context.environment.name;
3638
+ }
3639
+ if (!process.env.TEMP_RUN) {
3640
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3641
+ }
3642
+ await loadBrunoParams(this.context, this.context.environment.name);
3643
+ }
3644
+ async afterScenario(world, scenario) { }
3645
+ async beforeStep(world, step) {
3646
+ if (!this.beforeScenarioCalled) {
3647
+ this.beforeScenario(world, step);
3648
+ }
3649
+ if (this.stepIndex === undefined) {
3650
+ this.stepIndex = 0;
3651
+ }
3652
+ else {
3653
+ this.stepIndex++;
3654
+ }
3655
+ if (step && step.pickleStep && step.pickleStep.text) {
3656
+ this.stepName = step.pickleStep.text;
3657
+ this.logger.info("step: " + this.stepName);
3658
+ }
3659
+ else if (step && step.text) {
3660
+ this.stepName = step.text;
3661
+ }
3662
+ else {
3663
+ this.stepName = "step " + this.stepIndex;
3664
+ }
3665
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3666
+ if (this.context.browserObject.context) {
3667
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3668
+ }
3669
+ }
3670
+ if (this.initSnapshotTaken === false) {
3671
+ this.initSnapshotTaken = true;
3672
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3673
+ const snapshot = await this.getAriaSnapshot();
3674
+ if (snapshot) {
3675
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3676
+ }
3677
+ }
3678
+ }
3679
+ }
3680
+ async getAriaSnapshot() {
3681
+ try {
3682
+ // find the page url
3683
+ const url = await this.page.url();
3684
+ // extract the path from the url
3685
+ const path = new URL(url).pathname;
3686
+ // get the page title
3687
+ const title = await this.page.title();
3688
+ // go over other frams
3689
+ const frames = this.page.frames();
3690
+ const snapshots = [];
3691
+ const content = [`- path: ${path}`, `- title: ${title}`];
3692
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3693
+ for (let i = 0; i < frames.length; i++) {
3694
+ const frame = frames[i];
3695
+ try {
3696
+ // Ensure frame is attached and has body
3697
+ const body = frame.locator("body");
3698
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3699
+ const snapshot = await body.ariaSnapshot({ timeout });
3700
+ content.push(`- frame: ${i}`);
3701
+ content.push(snapshot);
3702
+ }
3703
+ catch (innerErr) { }
3704
+ }
3705
+ return content.join("\n");
3706
+ }
3707
+ catch (e) {
3708
+ console.log("Error in getAriaSnapshot");
3709
+ //console.debug(e);
3710
+ }
3711
+ return null;
3712
+ }
3713
+ /**
3714
+ * Sends command with custom payload to report.
3715
+ * @param commandText - Title of the command to be shown in the report.
3716
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3717
+ * @param content - Content of the command to be shown in the report.
3718
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3719
+ * @param world - Optional world context.
3720
+ * @public
3721
+ */
3722
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3723
+ const state = {
3724
+ options,
3725
+ world,
3726
+ locate: false,
3727
+ scroll: false,
3728
+ screenshot: options.screenshot ?? false,
3729
+ highlight: options.highlight ?? false,
3730
+ type: Types.REPORT_COMMAND,
3731
+ text: commandText,
3732
+ _text: commandText,
3733
+ operation: "report_command",
3734
+ log: "***** " + commandText + " *****\n",
3735
+ };
3736
+ try {
3737
+ await _preCommand(state, this);
3738
+ const payload = {
3739
+ type: options.type ?? "text",
3740
+ content: content,
3741
+ screenshotId: null,
3742
+ };
3743
+ state.payload = payload;
3744
+ if (commandStatus === "FAILED") {
3745
+ state.throwError = true;
3746
+ throw new Error("Command failed");
3747
+ }
3748
+ }
3749
+ catch (e) {
3750
+ await _commandError(state, e, this);
3751
+ }
3752
+ finally {
3753
+ await _commandFinally(state, this);
3754
+ }
3755
+ }
3756
+ async afterStep(world, step) {
3757
+ this.stepName = null;
3758
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3759
+ if (this.context.browserObject.context) {
3760
+ await this.context.browserObject.context.tracing.stopChunk({
3761
+ path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3762
+ });
3763
+ if (world && world.attach) {
3764
+ await world.attach(JSON.stringify({
3765
+ type: "trace",
3766
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3767
+ }), "application/json+trace");
3768
+ }
3769
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3770
+ }
3771
+ }
3772
+ if (this.context) {
3773
+ this.context.examplesRow = null;
3774
+ }
3775
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3776
+ const snapshot = await this.getAriaSnapshot();
3777
+ if (snapshot) {
3778
+ const obj = {};
3779
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3780
+ }
3781
+ }
3782
+ if (!process.env.TEMP_RUN) {
3783
+ const state = {
3784
+ world,
3785
+ locate: false,
3786
+ scroll: false,
3787
+ screenshot: true,
3788
+ highlight: true,
3789
+ type: Types.STEP_COMPLETE,
3790
+ text: "end of scenario",
3791
+ _text: "end of scenario",
3792
+ operation: "step_complete",
3793
+ log: "***** " + "end of scenario" + " *****\n",
3794
+ };
3795
+ try {
3796
+ await _preCommand(state, this);
3797
+ }
3798
+ catch (e) {
3799
+ await _commandError(state, e, this);
3800
+ }
3801
+ finally {
3802
+ await _commandFinally(state, this);
3803
+ }
2791
3804
  }
2792
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2793
3805
  }
2794
3806
  }
2795
3807
  function createTimedPromise(promise, label) {
@@ -2797,151 +3809,5 @@ function createTimedPromise(promise, label) {
2797
3809
  .then((result) => ({ status: "fulfilled", label, result }))
2798
3810
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2799
3811
  }
2800
- const KEYBOARD_EVENTS = [
2801
- "ALT",
2802
- "AltGraph",
2803
- "CapsLock",
2804
- "Control",
2805
- "Fn",
2806
- "FnLock",
2807
- "Hyper",
2808
- "Meta",
2809
- "NumLock",
2810
- "ScrollLock",
2811
- "Shift",
2812
- "Super",
2813
- "Symbol",
2814
- "SymbolLock",
2815
- "Enter",
2816
- "Tab",
2817
- "ArrowDown",
2818
- "ArrowLeft",
2819
- "ArrowRight",
2820
- "ArrowUp",
2821
- "End",
2822
- "Home",
2823
- "PageDown",
2824
- "PageUp",
2825
- "Backspace",
2826
- "Clear",
2827
- "Copy",
2828
- "CrSel",
2829
- "Cut",
2830
- "Delete",
2831
- "EraseEof",
2832
- "ExSel",
2833
- "Insert",
2834
- "Paste",
2835
- "Redo",
2836
- "Undo",
2837
- "Accept",
2838
- "Again",
2839
- "Attn",
2840
- "Cancel",
2841
- "ContextMenu",
2842
- "Escape",
2843
- "Execute",
2844
- "Find",
2845
- "Finish",
2846
- "Help",
2847
- "Pause",
2848
- "Play",
2849
- "Props",
2850
- "Select",
2851
- "ZoomIn",
2852
- "ZoomOut",
2853
- "BrightnessDown",
2854
- "BrightnessUp",
2855
- "Eject",
2856
- "LogOff",
2857
- "Power",
2858
- "PowerOff",
2859
- "PrintScreen",
2860
- "Hibernate",
2861
- "Standby",
2862
- "WakeUp",
2863
- "AllCandidates",
2864
- "Alphanumeric",
2865
- "CodeInput",
2866
- "Compose",
2867
- "Convert",
2868
- "Dead",
2869
- "FinalMode",
2870
- "GroupFirst",
2871
- "GroupLast",
2872
- "GroupNext",
2873
- "GroupPrevious",
2874
- "ModeChange",
2875
- "NextCandidate",
2876
- "NonConvert",
2877
- "PreviousCandidate",
2878
- "Process",
2879
- "SingleCandidate",
2880
- "HangulMode",
2881
- "HanjaMode",
2882
- "JunjaMode",
2883
- "Eisu",
2884
- "Hankaku",
2885
- "Hiragana",
2886
- "HiraganaKatakana",
2887
- "KanaMode",
2888
- "KanjiMode",
2889
- "Katakana",
2890
- "Romaji",
2891
- "Zenkaku",
2892
- "ZenkakuHanaku",
2893
- "F1",
2894
- "F2",
2895
- "F3",
2896
- "F4",
2897
- "F5",
2898
- "F6",
2899
- "F7",
2900
- "F8",
2901
- "F9",
2902
- "F10",
2903
- "F11",
2904
- "F12",
2905
- "Soft1",
2906
- "Soft2",
2907
- "Soft3",
2908
- "Soft4",
2909
- "ChannelDown",
2910
- "ChannelUp",
2911
- "Close",
2912
- "MailForward",
2913
- "MailReply",
2914
- "MailSend",
2915
- "MediaFastForward",
2916
- "MediaPause",
2917
- "MediaPlay",
2918
- "MediaPlayPause",
2919
- "MediaRecord",
2920
- "MediaRewind",
2921
- "MediaStop",
2922
- "MediaTrackNext",
2923
- "MediaTrackPrevious",
2924
- "AudioBalanceLeft",
2925
- "AudioBalanceRight",
2926
- "AudioBassBoostDown",
2927
- "AudioBassBoostToggle",
2928
- "AudioBassBoostUp",
2929
- "AudioFaderFront",
2930
- "AudioFaderRear",
2931
- "AudioSurroundModeNext",
2932
- "AudioTrebleDown",
2933
- "AudioTrebleUp",
2934
- "AudioVolumeDown",
2935
- "AudioVolumeMute",
2936
- "AudioVolumeUp",
2937
- "MicrophoneToggle",
2938
- "MicrophoneVolumeDown",
2939
- "MicrophoneVolumeMute",
2940
- "MicrophoneVolumeUp",
2941
- "TV",
2942
- "TV3DMode",
2943
- "TVAntennaCable",
2944
- "TVAudioDescription",
2945
- ];
2946
3812
  export { StableBrowser };
2947
3813
  //# sourceMappingURL=stable_browser.js.map