automation_model 1.0.443-dev → 1.0.443

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 +231 -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 -1305
  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,55 +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();
479
- // wait for the dialog to close
480
- await dialogCloseLocator.waitFor({ state: "hidden" });
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
+ }
481
595
  return { rerun: true };
482
596
  }
483
597
  }
484
598
  }
485
599
  return { rerun: false };
486
600
  }
487
- async _locate(selectors, info, _params, timeout = 30000) {
601
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
602
+ if (!timeout) {
603
+ timeout = 30000;
604
+ }
488
605
  for (let i = 0; i < 3; i++) {
489
606
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
490
607
  for (let j = 0; j < selectors.locators.length; j++) {
491
608
  let selector = selectors.locators[j];
492
609
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
493
610
  }
494
- let element = await this._locate_internal(selectors, info, _params, timeout);
611
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
495
612
  if (!element.rerun) {
496
- 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);
497
635
  }
498
636
  }
499
637
  throw new Error("unable to locate element " + JSON.stringify(selectors));
500
638
  }
501
- async _locate_internal(selectors, info, _params, timeout = 30000) {
502
- let highPriorityTimeout = 5000;
503
- let visibleOnlyTimeout = 6000;
504
- let startTime = performance.now();
505
- let locatorsCount = 0;
506
- //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();
507
646
  let scope = this.page;
647
+ if (selectors.frame) {
648
+ return selectors.frame;
649
+ }
508
650
  if (selectors.iframe_src || selectors.frameLocators) {
509
- const findFrame = (frame, framescope) => {
651
+ const findFrame = async (frame, framescope) => {
510
652
  for (let i = 0; i < frame.selectors.length; i++) {
511
653
  let frameLocator = frame.selectors[i];
512
654
  if (frameLocator.css) {
513
- framescope = framescope.frameLocator(frameLocator.css);
514
- 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
+ }
515
669
  }
516
670
  }
517
671
  if (frame.children) {
518
- return findFrame(frame.children, framescope);
672
+ return await findFrame(frame.children, framescope);
519
673
  }
520
674
  return framescope;
521
675
  };
522
- info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
676
+ let fLocator = null;
523
677
  while (true) {
524
678
  let frameFound = false;
525
679
  if (selectors.nestFrmLoc) {
526
- scope = findFrame(selectors.nestFrmLoc, scope);
680
+ fLocator = selectors.nestFrmLoc;
681
+ scope = await findFrame(selectors.nestFrmLoc, scope);
527
682
  frameFound = true;
528
683
  break;
529
684
  }
@@ -531,6 +686,7 @@ class StableBrowser {
531
686
  for (let i = 0; i < selectors.frameLocators.length; i++) {
532
687
  let frameLocator = selectors.frameLocators[i];
533
688
  if (frameLocator.css) {
689
+ fLocator = frameLocator.css;
534
690
  scope = scope.frameLocator(frameLocator.css);
535
691
  frameFound = true;
536
692
  break;
@@ -538,20 +694,55 @@ class StableBrowser {
538
694
  }
539
695
  }
540
696
  if (!frameFound && selectors.iframe_src) {
697
+ fLocator = selectors.iframe_src;
541
698
  scope = this.page.frame({ url: selectors.iframe_src });
542
699
  }
543
700
  if (!scope) {
544
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
545
- 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}"`;
546
708
  throw new Error("unable to locate iframe " + selectors.iframe_src);
547
709
  }
548
710
  await new Promise((resolve) => setTimeout(resolve, 1000));
549
711
  }
550
712
  else {
713
+ if (info && info.locatorLog) {
714
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
715
+ }
551
716
  break;
552
717
  }
553
718
  }
554
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);
555
746
  let selectorsLocators = null;
556
747
  selectorsLocators = selectors.locators;
557
748
  // group selectors by priority
@@ -587,18 +778,13 @@ class StableBrowser {
587
778
  }
588
779
  // info.log += "scanning locators in priority 1" + "\n";
589
780
  let onlyPriority3 = selectorsLocators[0].priority === 3;
590
- 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);
591
782
  if (result.foundElements.length === 0) {
592
783
  // info.log += "scanning locators in priority 2" + "\n";
593
- 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);
594
785
  }
595
- if (result.foundElements.length === 0 && onlyPriority3) {
596
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
597
- }
598
- else {
599
- if (result.foundElements.length === 0 && !highPriorityOnly) {
600
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
601
- }
786
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
787
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
602
788
  }
603
789
  let foundElements = result.foundElements;
604
790
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -638,24 +824,43 @@ class StableBrowser {
638
824
  return maxCountElement.locator;
639
825
  }
640
826
  }
641
- if (performance.now() - startTime > timeout) {
827
+ if (Date.now() - startTime > timeout) {
642
828
  break;
643
829
  }
644
- if (performance.now() - startTime > highPriorityTimeout) {
645
- 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";
646
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
+ }
647
837
  }
648
- if (performance.now() - startTime > visibleOnlyTimeout) {
649
- 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";
650
840
  visibleOnly = false;
651
841
  }
652
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
+ }
653
848
  }
654
849
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
655
- 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
+ }
656
861
  throw new Error("failed to locate first element no elements found, " + info.log);
657
862
  }
658
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
863
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
659
864
  let foundElements = [];
660
865
  const result = {
661
866
  foundElements: foundElements,
@@ -663,17 +868,20 @@ class StableBrowser {
663
868
  for (let i = 0; i < locatorsGroup.length; i++) {
664
869
  let foundLocators = [];
665
870
  try {
666
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
871
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
667
872
  }
668
873
  catch (e) {
669
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
670
- 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);
671
877
  foundLocators = [];
672
878
  try {
673
- 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);
674
880
  }
675
881
  catch (e) {
676
- 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
+ }
677
885
  }
678
886
  }
679
887
  if (foundLocators.length === 1) {
@@ -684,270 +892,348 @@ class StableBrowser {
684
892
  });
685
893
  result.locatorIndex = i;
686
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
+ }
687
932
  }
688
933
  return result;
689
934
  }
690
- async click(selectors, _params, options = {}, world = null) {
691
- 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);
692
949
  const startTime = Date.now();
693
- if (options && options.context) {
694
- selectors.locators[0].text = options.context;
950
+ let timeout = 30000;
951
+ if (options && options.timeout) {
952
+ timeout = options.timeout;
695
953
  }
696
- const info = {};
697
- info.log = "***** click on " + selectors.element_name + " *****\n";
698
- info.operation = "click";
699
- info.selectors = selectors;
700
- let error = null;
701
- let screenshotId = null;
702
- let screenshotPath = null;
703
- try {
704
- let element = await this._locate(selectors, info, _params);
705
- await this.scrollIfNeeded(element, info);
706
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
954
+ while (true) {
707
955
  try {
708
- await this._highlightElements(element);
709
- await element.click();
710
- 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
+ }
711
969
  }
712
970
  catch (e) {
713
- // await this.closeUnexpectedPopups();
714
- info.log += "click failed, will try again" + "\n";
715
- element = await this._locate(selectors, info, _params);
716
- await element.dispatchEvent("click");
717
- 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
+ }
718
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);
719
1048
  await this.waitForPageLoad();
720
- return info;
1049
+ return state.info;
721
1050
  }
722
1051
  catch (e) {
723
- this.logger.error("click failed " + info.log);
724
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
725
- info.screenshotPath = screenshotPath;
726
- Object.assign(e, { info: info });
727
- error = e;
728
- throw e;
1052
+ await _commandError(state, e, this);
729
1053
  }
730
1054
  finally {
731
- const endTime = Date.now();
732
- this._reportToWorld(world, {
733
- element_name: selectors.element_name,
734
- type: Types.CLICK,
735
- text: `Click element`,
736
- screenshotId,
737
- result: error
738
- ? {
739
- status: "FAILED",
740
- startTime,
741
- endTime,
742
- message: error === null || error === void 0 ? void 0 : error.message,
743
- }
744
- : {
745
- status: "PASSED",
746
- startTime,
747
- endTime,
748
- },
749
- info: info,
750
- });
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);
751
1087
  }
1088
+ return found;
752
1089
  }
753
1090
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
754
- this._validateSelectors(selectors);
755
- const startTime = Date.now();
756
- const info = {};
757
- info.log = "";
758
- info.operation = "setCheck";
759
- info.checked = checked;
760
- info.selectors = selectors;
761
- let error = null;
762
- let screenshotId = null;
763
- 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
+ };
764
1102
  try {
765
- let element = await this._locate(selectors, info, _params);
766
- ({ 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));
767
1107
  try {
768
- await this._highlightElements(element);
769
- 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 });
770
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);
771
1117
  }
772
1118
  catch (e) {
773
1119
  if (e.message && e.message.includes("did not change its state")) {
774
1120
  this.logger.info("element did not change its state, ignoring...");
775
1121
  }
776
1122
  else {
777
- //await this.closeUnexpectedPopups();
778
- info.log += "setCheck failed, will try again" + "\n";
779
- element = await this._locate(selectors, info, _params);
780
- await element.setChecked(checked, { timeout: 5000, force: true });
781
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
+ }
782
1145
  }
783
1146
  }
784
1147
  await this.waitForPageLoad();
785
- return info;
1148
+ return state.info;
786
1149
  }
787
1150
  catch (e) {
788
- this.logger.error("setCheck failed " + info.log);
789
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
790
- info.screenshotPath = screenshotPath;
791
- Object.assign(e, { info: info });
792
- error = e;
793
- throw e;
1151
+ await _commandError(state, e, this);
794
1152
  }
795
1153
  finally {
796
- const endTime = Date.now();
797
- this._reportToWorld(world, {
798
- element_name: selectors.element_name,
799
- type: checked ? Types.CHECK : Types.UNCHECK,
800
- text: checked ? `Check element` : `Uncheck element`,
801
- screenshotId,
802
- result: error
803
- ? {
804
- status: "FAILED",
805
- startTime,
806
- endTime,
807
- message: error === null || error === void 0 ? void 0 : error.message,
808
- }
809
- : {
810
- status: "PASSED",
811
- startTime,
812
- endTime,
813
- },
814
- info: info,
815
- });
1154
+ await _commandFinally(state, this);
816
1155
  }
817
1156
  }
818
1157
  async hover(selectors, _params, options = {}, world = null) {
819
- this._validateSelectors(selectors);
820
- const startTime = Date.now();
821
- const info = {};
822
- info.log = "";
823
- info.operation = "hover";
824
- info.selectors = selectors;
825
- let error = null;
826
- let screenshotId = null;
827
- 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
+ };
828
1169
  try {
829
- let element = await this._locate(selectors, info, _params);
830
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
831
- try {
832
- await this._highlightElements(element);
833
- await element.hover();
834
- await new Promise((resolve) => setTimeout(resolve, 1000));
835
- }
836
- catch (e) {
837
- //await this.closeUnexpectedPopups();
838
- info.log += "hover failed, will try again" + "\n";
839
- element = await this._locate(selectors, info, _params);
840
- await element.hover({ timeout: 10000 });
841
- await new Promise((resolve) => setTimeout(resolve, 1000));
842
- }
1170
+ await _preCommand(state, this);
1171
+ await performAction("hover", state.element, options, this, state, _params);
1172
+ await _screenshot(state, this);
843
1173
  await this.waitForPageLoad();
844
- return info;
1174
+ return state.info;
845
1175
  }
846
1176
  catch (e) {
847
- this.logger.error("hover failed " + info.log);
848
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
849
- info.screenshotPath = screenshotPath;
850
- Object.assign(e, { info: info });
851
- error = e;
852
- throw e;
1177
+ await _commandError(state, e, this);
853
1178
  }
854
1179
  finally {
855
- const endTime = Date.now();
856
- this._reportToWorld(world, {
857
- element_name: selectors.element_name,
858
- type: Types.HOVER,
859
- text: `Hover element`,
860
- screenshotId,
861
- result: error
862
- ? {
863
- status: "FAILED",
864
- startTime,
865
- endTime,
866
- message: error === null || error === void 0 ? void 0 : error.message,
867
- }
868
- : {
869
- status: "PASSED",
870
- startTime,
871
- endTime,
872
- },
873
- info: info,
874
- });
1180
+ await _commandFinally(state, this);
875
1181
  }
876
1182
  }
877
1183
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
878
- this._validateSelectors(selectors);
879
1184
  if (!values) {
880
1185
  throw new Error("values is null");
881
1186
  }
882
- const startTime = Date.now();
883
- let error = null;
884
- let screenshotId = null;
885
- let screenshotPath = null;
886
- const info = {};
887
- info.log = "";
888
- info.operation = "selectOptions";
889
- 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
+ };
890
1199
  try {
891
- let element = await this._locate(selectors, info, _params);
892
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1200
+ await _preCommand(state, this);
893
1201
  try {
894
- await this._highlightElements(element);
895
- await element.selectOption(values);
1202
+ await state.element.selectOption(values);
896
1203
  }
897
1204
  catch (e) {
898
1205
  //await this.closeUnexpectedPopups();
899
- info.log += "selectOption failed, will try force" + "\n";
900
- 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 });
901
1208
  }
902
1209
  await this.waitForPageLoad();
903
- return info;
1210
+ return state.info;
904
1211
  }
905
1212
  catch (e) {
906
- this.logger.error("selectOption failed " + info.log);
907
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
908
- info.screenshotPath = screenshotPath;
909
- Object.assign(e, { info: info });
910
- this.logger.info("click failed, will try next selector");
911
- error = e;
912
- throw e;
1213
+ await _commandError(state, e, this);
913
1214
  }
914
1215
  finally {
915
- const endTime = Date.now();
916
- this._reportToWorld(world, {
917
- element_name: selectors.element_name,
918
- type: Types.SELECT,
919
- text: `Select option: ${values}`,
920
- value: values.toString(),
921
- screenshotId,
922
- result: error
923
- ? {
924
- status: "FAILED",
925
- startTime,
926
- endTime,
927
- message: error === null || error === void 0 ? void 0 : error.message,
928
- }
929
- : {
930
- status: "PASSED",
931
- startTime,
932
- endTime,
933
- },
934
- info: info,
935
- });
1216
+ await _commandFinally(state, this);
936
1217
  }
937
1218
  }
938
1219
  async type(_value, _params = null, options = {}, world = null) {
939
- const startTime = Date.now();
940
- let error = null;
941
- let screenshotId = null;
942
- let screenshotPath = null;
943
- const info = {};
944
- info.log = "";
945
- info.operation = "type";
946
- _value = this._fixUsingParams(_value, _params);
947
- 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
+ };
948
1234
  try {
949
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
950
- const valueSegment = _value.split("&&");
1235
+ await _preCommand(state, this);
1236
+ const valueSegment = state.value.split("&&");
951
1237
  for (let i = 0; i < valueSegment.length; i++) {
952
1238
  if (i > 0) {
953
1239
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -967,134 +1253,77 @@ class StableBrowser {
967
1253
  await this.page.keyboard.type(value);
968
1254
  }
969
1255
  }
970
- return info;
1256
+ return state.info;
971
1257
  }
972
1258
  catch (e) {
973
- //await this.closeUnexpectedPopups();
974
- this.logger.error("type failed " + info.log);
975
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
976
- info.screenshotPath = screenshotPath;
977
- Object.assign(e, { info: info });
978
- error = e;
979
- throw e;
1259
+ await _commandError(state, e, this);
980
1260
  }
981
1261
  finally {
982
- const endTime = Date.now();
983
- this._reportToWorld(world, {
984
- type: Types.TYPE_PRESS,
985
- screenshotId,
986
- value: _value,
987
- text: `type value: ${_value}`,
988
- result: error
989
- ? {
990
- status: "FAILED",
991
- startTime,
992
- endTime,
993
- message: error === null || error === void 0 ? void 0 : error.message,
994
- }
995
- : {
996
- status: "PASSED",
997
- startTime,
998
- endTime,
999
- },
1000
- info: info,
1001
- });
1262
+ await _commandFinally(state, this);
1002
1263
  }
1003
1264
  }
1004
1265
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
1005
- // set input value for non fillable inputs like date, time, range, color, etc.
1006
- this._validateSelectors(selectors);
1007
- const startTime = Date.now();
1008
- const info = {};
1009
- info.log = "***** set input value " + selectors.element_name + " *****\n";
1010
- info.operation = "setInputValue";
1011
- info.selectors = selectors;
1012
- value = this._fixUsingParams(value, _params);
1013
- info.value = value;
1014
- let error = null;
1015
- let screenshotId = null;
1016
- 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
+ };
1017
1277
  try {
1018
- value = await this._replaceWithLocalData(value, this);
1019
- let element = await this._locate(selectors, info, _params);
1020
- await this.scrollIfNeeded(element, info);
1021
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1022
- await this._highlightElements(element);
1278
+ await _preCommand(state, this);
1279
+ let value = await this._replaceWithLocalData(state.value, this);
1023
1280
  try {
1024
- await element.evaluateHandle((el, value) => {
1281
+ await state.element.evaluateHandle((el, value) => {
1025
1282
  el.value = value;
1026
1283
  }, value);
1027
1284
  }
1028
1285
  catch (error) {
1029
1286
  this.logger.error("setInputValue failed, will try again");
1030
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1031
- info.screenshotPath = screenshotPath;
1032
- Object.assign(error, { info: info });
1033
- await element.evaluateHandle((el, value) => {
1287
+ await _screenshot(state, this);
1288
+ Object.assign(error, { info: state.info });
1289
+ await state.element.evaluateHandle((el, value) => {
1034
1290
  el.value = value;
1035
1291
  });
1036
1292
  }
1037
1293
  }
1038
1294
  catch (e) {
1039
- this.logger.error("setInputValue failed " + info.log);
1040
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1041
- info.screenshotPath = screenshotPath;
1042
- Object.assign(e, { info: info });
1043
- error = e;
1044
- throw e;
1295
+ await _commandError(state, e, this);
1045
1296
  }
1046
1297
  finally {
1047
- const endTime = Date.now();
1048
- this._reportToWorld(world, {
1049
- element_name: selectors.element_name,
1050
- type: Types.SET_INPUT,
1051
- text: `Set input value`,
1052
- value: value,
1053
- screenshotId,
1054
- result: error
1055
- ? {
1056
- status: "FAILED",
1057
- startTime,
1058
- endTime,
1059
- message: error === null || error === void 0 ? void 0 : error.message,
1060
- }
1061
- : {
1062
- status: "PASSED",
1063
- startTime,
1064
- endTime,
1065
- },
1066
- info: info,
1067
- });
1298
+ await _commandFinally(state, this);
1068
1299
  }
1069
1300
  }
1070
1301
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1071
- this._validateSelectors(selectors);
1072
- const startTime = Date.now();
1073
- let error = null;
1074
- let screenshotId = null;
1075
- let screenshotPath = null;
1076
- const info = {};
1077
- info.log = "";
1078
- info.operation = Types.SET_DATE_TIME;
1079
- info.selectors = selectors;
1080
- 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
+ };
1081
1315
  try {
1082
- value = await this._replaceWithLocalData(value, this);
1083
- let element = await this._locate(selectors, info, _params);
1084
- //insert red border around the element
1085
- await this.scrollIfNeeded(element, info);
1086
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1087
- await this._highlightElements(element);
1316
+ await _preCommand(state, this);
1088
1317
  try {
1089
- await element.click();
1318
+ await performAction("click", state.element, options, this, state, _params);
1090
1319
  await new Promise((resolve) => setTimeout(resolve, 500));
1091
1320
  if (format) {
1092
- value = dayjs(value).format(format);
1093
- await element.fill(value);
1321
+ state.value = dayjs(state.value).format(format);
1322
+ await state.element.fill(state.value);
1094
1323
  }
1095
1324
  else {
1096
- const dateTimeValue = await getDateTimeValue({ value, element });
1097
- await element.evaluateHandle((el, dateTimeValue) => {
1325
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1326
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1098
1327
  el.value = ""; // clear input
1099
1328
  el.value = dateTimeValue;
1100
1329
  }, dateTimeValue);
@@ -1107,20 +1336,19 @@ class StableBrowser {
1107
1336
  }
1108
1337
  catch (err) {
1109
1338
  //await this.closeUnexpectedPopups();
1110
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1339
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1111
1340
  this.logger.info("Trying again");
1112
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1113
- info.screenshotPath = screenshotPath;
1114
- Object.assign(err, { info: info });
1341
+ await _screenshot(state, this);
1342
+ Object.assign(err, { info: state.info });
1115
1343
  await element.click();
1116
1344
  await new Promise((resolve) => setTimeout(resolve, 500));
1117
1345
  if (format) {
1118
- value = dayjs(value).format(format);
1119
- await element.fill(value);
1346
+ state.value = dayjs(state.value).format(format);
1347
+ await state.element.fill(state.value);
1120
1348
  }
1121
1349
  else {
1122
- const dateTimeValue = await getDateTimeValue({ value, element });
1123
- await element.evaluateHandle((el, dateTimeValue) => {
1350
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1351
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1124
1352
  el.value = ""; // clear input
1125
1353
  el.value = dateTimeValue;
1126
1354
  }, dateTimeValue);
@@ -1133,84 +1361,63 @@ class StableBrowser {
1133
1361
  }
1134
1362
  }
1135
1363
  catch (e) {
1136
- error = e;
1137
- throw e;
1364
+ await _commandError(state, e, this);
1138
1365
  }
1139
1366
  finally {
1140
- const endTime = Date.now();
1141
- this._reportToWorld(world, {
1142
- element_name: selectors.element_name,
1143
- type: Types.SET_DATE_TIME,
1144
- screenshotId,
1145
- value: value,
1146
- text: `setDateTime input with value: ${value}`,
1147
- result: error
1148
- ? {
1149
- status: "FAILED",
1150
- startTime,
1151
- endTime,
1152
- message: error === null || error === void 0 ? void 0 : error.message,
1153
- }
1154
- : {
1155
- status: "PASSED",
1156
- startTime,
1157
- endTime,
1158
- },
1159
- info: info,
1160
- });
1367
+ await _commandFinally(state, this);
1161
1368
  }
1162
1369
  }
1163
1370
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
1164
- this._validateSelectors(selectors);
1165
- const startTime = Date.now();
1166
- let error = null;
1167
- let screenshotId = null;
1168
- let screenshotPath = null;
1169
- const info = {};
1170
- info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1171
- info.operation = "clickType";
1172
- info.selectors = selectors;
1371
+ _value = unEscapeString(_value);
1173
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
+ }
1174
1389
  if (newValue !== _value) {
1175
1390
  //this.logger.info(_value + "=" + newValue);
1176
1391
  _value = newValue;
1177
1392
  }
1178
- info.value = _value;
1179
1393
  try {
1180
- let element = await this._locate(selectors, info, _params);
1181
- //insert red border around the element
1182
- await this.scrollIfNeeded(element, info);
1183
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1184
- await this._highlightElements(element);
1185
- if (options === null || options === undefined || !options.press) {
1394
+ await _preCommand(state, this);
1395
+ state.info.value = _value;
1396
+ if (!options.press) {
1186
1397
  try {
1187
- let currentValue = await element.inputValue();
1398
+ let currentValue = await state.element.inputValue();
1188
1399
  if (currentValue) {
1189
- await element.fill("");
1400
+ await state.element.fill("");
1190
1401
  }
1191
1402
  }
1192
1403
  catch (e) {
1193
1404
  this.logger.info("unable to clear input value");
1194
1405
  }
1195
1406
  }
1196
- if (options === null || options === undefined || options.press) {
1197
- try {
1198
- await element.click({ timeout: 5000 });
1199
- }
1200
- catch (e) {
1201
- await element.dispatchEvent("click");
1202
- }
1407
+ if (options.press) {
1408
+ options.timeout = 5000;
1409
+ await performAction("click", state.element, options, this, state, _params);
1203
1410
  }
1204
1411
  else {
1205
1412
  try {
1206
- await element.focus();
1413
+ await state.element.focus();
1207
1414
  }
1208
1415
  catch (e) {
1209
- await element.dispatchEvent("focus");
1416
+ await state.element.dispatchEvent("focus");
1210
1417
  }
1211
1418
  }
1212
1419
  await new Promise((resolve) => setTimeout(resolve, 500));
1213
- const valueSegment = _value.split("&&");
1420
+ const valueSegment = state.value.split("&&");
1214
1421
  for (let i = 0; i < valueSegment.length; i++) {
1215
1422
  if (i > 0) {
1216
1423
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1230,13 +1437,19 @@ class StableBrowser {
1230
1437
  await new Promise((resolve) => setTimeout(resolve, 500));
1231
1438
  }
1232
1439
  }
1440
+ await _screenshot(state, this);
1233
1441
  if (enter === true) {
1234
1442
  await new Promise((resolve) => setTimeout(resolve, 2000));
1235
1443
  await this.page.keyboard.press("Enter");
1236
1444
  await this.waitForPageLoad();
1237
1445
  }
1238
1446
  else if (enter === false) {
1239
- await element.dispatchEvent("change");
1447
+ try {
1448
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1449
+ }
1450
+ catch (e) {
1451
+ // ignore
1452
+ }
1240
1453
  //await this.page.keyboard.press("Tab");
1241
1454
  }
1242
1455
  else {
@@ -1245,111 +1458,95 @@ class StableBrowser {
1245
1458
  await this.waitForPageLoad();
1246
1459
  }
1247
1460
  }
1248
- return info;
1461
+ return state.info;
1249
1462
  }
1250
1463
  catch (e) {
1251
- //await this.closeUnexpectedPopups();
1252
- this.logger.error("fill failed " + JSON.stringify(info));
1253
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1254
- info.screenshotPath = screenshotPath;
1255
- Object.assign(e, { info: info });
1256
- error = e;
1257
- throw e;
1464
+ await _commandError(state, e, this);
1258
1465
  }
1259
1466
  finally {
1260
- const endTime = Date.now();
1261
- this._reportToWorld(world, {
1262
- element_name: selectors.element_name,
1263
- type: Types.FILL,
1264
- screenshotId,
1265
- value: _value,
1266
- text: `clickType input with value: ${_value}`,
1267
- result: error
1268
- ? {
1269
- status: "FAILED",
1270
- startTime,
1271
- endTime,
1272
- message: error === null || error === void 0 ? void 0 : error.message,
1273
- }
1274
- : {
1275
- status: "PASSED",
1276
- startTime,
1277
- endTime,
1278
- },
1279
- info: info,
1280
- });
1467
+ await _commandFinally(state, this);
1281
1468
  }
1282
1469
  }
1283
1470
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
1284
- this._validateSelectors(selectors);
1285
- const startTime = Date.now();
1286
- let error = null;
1287
- let screenshotId = null;
1288
- let screenshotPath = null;
1289
- const info = {};
1290
- info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1291
- info.operation = "fill";
1292
- info.selectors = selectors;
1293
- 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
+ };
1294
1482
  try {
1295
- let element = await this._locate(selectors, info, _params);
1296
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1297
- await this._highlightElements(element);
1298
- await element.fill(value);
1299
- await element.dispatchEvent("change");
1483
+ await _preCommand(state, this);
1484
+ await state.element.fill(value);
1485
+ await state.element.dispatchEvent("change");
1300
1486
  if (enter) {
1301
1487
  await new Promise((resolve) => setTimeout(resolve, 2000));
1302
1488
  await this.page.keyboard.press("Enter");
1303
1489
  }
1304
1490
  await this.waitForPageLoad();
1305
- return info;
1491
+ return state.info;
1306
1492
  }
1307
1493
  catch (e) {
1308
- //await this.closeUnexpectedPopups();
1309
- this.logger.error("fill failed " + info.log);
1310
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1311
- info.screenshotPath = screenshotPath;
1312
- Object.assign(e, { info: info });
1313
- error = e;
1314
- throw e;
1494
+ await _commandError(state, e, this);
1315
1495
  }
1316
1496
  finally {
1317
- const endTime = Date.now();
1318
- this._reportToWorld(world, {
1319
- element_name: selectors.element_name,
1320
- type: Types.FILL,
1321
- screenshotId,
1322
- value,
1323
- text: `Fill input with value: ${value}`,
1324
- result: error
1325
- ? {
1326
- status: "FAILED",
1327
- startTime,
1328
- endTime,
1329
- message: error === null || error === void 0 ? void 0 : error.message,
1330
- }
1331
- : {
1332
- status: "PASSED",
1333
- startTime,
1334
- endTime,
1335
- },
1336
- info: info,
1337
- });
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);
1338
1533
  }
1339
1534
  }
1340
1535
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1341
1536
  return await this._getText(selectors, 0, _params, options, info, world);
1342
1537
  }
1343
1538
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1344
- this._validateSelectors(selectors);
1539
+ const timeout = this._getFindElementTimeout(options);
1540
+ _validateSelectors(selectors);
1345
1541
  let screenshotId = null;
1346
1542
  let screenshotPath = null;
1347
1543
  if (!info.log) {
1348
1544
  info.log = "";
1545
+ info.locatorLog = new LocatorLog(selectors);
1349
1546
  }
1350
1547
  info.operation = "getText";
1351
1548
  info.selectors = selectors;
1352
- let element = await this._locate(selectors, info, _params);
1549
+ let element = await this._locate(selectors, info, _params, timeout);
1353
1550
  if (climb > 0) {
1354
1551
  const climbArray = [];
1355
1552
  for (let i = 0; i < climb; i++) {
@@ -1368,6 +1565,18 @@ class StableBrowser {
1368
1565
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1369
1566
  try {
1370
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
+ // }
1371
1580
  const elementText = await element.innerText();
1372
1581
  return {
1373
1582
  text: elementText,
@@ -1379,188 +1588,214 @@ class StableBrowser {
1379
1588
  }
1380
1589
  catch (e) {
1381
1590
  //await this.closeUnexpectedPopups();
1382
- this.logger.info("no innerText will use textContent");
1591
+ this.logger.info("no innerText, will use textContent");
1383
1592
  const elementText = await element.textContent();
1384
1593
  return { text: elementText, screenshotId, screenshotPath, value: value };
1385
1594
  }
1386
1595
  }
1387
1596
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1388
- var _a;
1389
- this._validateSelectors(selectors);
1390
1597
  if (!pattern) {
1391
1598
  throw new Error("pattern is null");
1392
1599
  }
1393
1600
  if (!text) {
1394
1601
  throw new Error("text is null");
1395
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
+ };
1396
1620
  const newValue = await this._replaceWithLocalData(text, world);
1397
1621
  if (newValue !== text) {
1398
1622
  this.logger.info(text + "=" + newValue);
1399
1623
  text = newValue;
1400
1624
  }
1401
- const startTime = Date.now();
1402
- let error = null;
1403
- let screenshotId = null;
1404
- let screenshotPath = null;
1405
- const info = {};
1406
- info.log =
1407
- "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1408
- info.operation = "containsPattern";
1409
- info.selectors = selectors;
1410
- info.value = text;
1411
- info.pattern = pattern;
1412
1625
  let foundObj = null;
1413
1626
  try {
1414
- 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);
1415
1630
  if (foundObj && foundObj.element) {
1416
- await this.scrollIfNeeded(foundObj.element, info);
1631
+ await this.scrollIfNeeded(foundObj.element, state.info);
1417
1632
  }
1418
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1633
+ await _screenshot(state, this);
1419
1634
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1420
1635
  pattern = pattern.replace("{text}", escapedText);
1421
1636
  let regex = new RegExp(pattern, "im");
1422
- 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))) {
1423
- 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;
1424
1639
  throw new Error("element doesn't contain text " + text);
1425
1640
  }
1426
- return info;
1641
+ return state.info;
1427
1642
  }
1428
1643
  catch (e) {
1429
- //await this.closeUnexpectedPopups();
1430
- this.logger.error("verify element contains text failed " + info.log);
1431
- this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
1432
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1433
- info.screenshotPath = screenshotPath;
1434
- Object.assign(e, { info: info });
1435
- error = e;
1436
- throw e;
1644
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1645
+ await _commandError(state, e, this);
1437
1646
  }
1438
1647
  finally {
1439
- const endTime = Date.now();
1440
- this._reportToWorld(world, {
1441
- element_name: selectors.element_name,
1442
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1443
- value: pattern,
1444
- text: `Verify element contains pattern: ${pattern}`,
1445
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1446
- result: error
1447
- ? {
1448
- status: "FAILED",
1449
- startTime,
1450
- endTime,
1451
- message: error === null || error === void 0 ? void 0 : error.message,
1452
- }
1453
- : {
1454
- status: "PASSED",
1455
- startTime,
1456
- endTime,
1457
- },
1458
- info: info,
1459
- });
1648
+ await _commandFinally(state, this);
1460
1649
  }
1461
1650
  }
1462
1651
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1463
- var _a, _b, _c;
1464
- 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
+ };
1465
1669
  if (!text) {
1466
1670
  throw new Error("text is null");
1467
1671
  }
1468
- const startTime = Date.now();
1469
- let error = null;
1470
- let screenshotId = null;
1471
- let screenshotPath = null;
1472
- const info = {};
1473
- info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1474
- info.operation = "containsText";
1475
- info.selectors = selectors;
1672
+ text = unEscapeString(text);
1476
1673
  const newValue = await this._replaceWithLocalData(text, world);
1477
1674
  if (newValue !== text) {
1478
1675
  this.logger.info(text + "=" + newValue);
1479
1676
  text = newValue;
1480
1677
  }
1481
- info.value = text;
1482
1678
  let foundObj = null;
1483
1679
  try {
1484
- foundObj = await this._getText(selectors, climb, _params, options, info, world);
1485
- if (foundObj && foundObj.element) {
1486
- await this.scrollIfNeeded(foundObj.element, info);
1487
- }
1488
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1489
- const dateAlternatives = findDateAlternatives(text);
1490
- const numberAlternatives = findNumberAlternatives(text);
1491
- if (dateAlternatives.date) {
1492
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1493
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
1494
- ((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
1495
- 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
+ }
1496
1697
  }
1497
- }
1498
- throw new Error("element doesn't contain text " + text);
1499
- }
1500
- else if (numberAlternatives.number) {
1501
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1502
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(numberAlternatives.numbers[i])) ||
1503
- ((_b = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _b === void 0 ? void 0 : _b.includes(numberAlternatives.numbers[i]))) {
1504
- 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;
1505
1708
  }
1506
1709
  }
1507
- throw new Error("element doesn't contain text " + text);
1508
- }
1509
- 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))) {
1510
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1511
- info.value = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value;
1512
- 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
1513
1715
  }
1514
- return info;
1716
+ state.info.foundText = foundObj?.text;
1717
+ state.info.value = foundObj?.value;
1718
+ throw new Error("element doesn't contain text " + text);
1515
1719
  }
1516
1720
  catch (e) {
1517
- //await this.closeUnexpectedPopups();
1518
- this.logger.error("verify element contains text failed " + info.log);
1519
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1520
- info.screenshotPath = screenshotPath;
1521
- Object.assign(e, { info: info });
1522
- error = e;
1721
+ await _commandError(state, e, this);
1523
1722
  throw e;
1524
1723
  }
1525
1724
  finally {
1526
- const endTime = Date.now();
1527
- this._reportToWorld(world, {
1528
- element_name: selectors.element_name,
1529
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1530
- text: `Verify element contains text: ${text}`,
1531
- value: text,
1532
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1533
- result: error
1534
- ? {
1535
- status: "FAILED",
1536
- startTime,
1537
- endTime,
1538
- message: error === null || error === void 0 ? void 0 : error.message,
1539
- }
1540
- : {
1541
- status: "PASSED",
1542
- startTime,
1543
- endTime,
1544
- },
1545
- info: info,
1546
- });
1725
+ await _commandFinally(state, this);
1547
1726
  }
1548
1727
  }
1549
- _getDataFile(world = null) {
1550
- let dataFile = null;
1551
- if (world && world.reportFolder) {
1552
- 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");
1553
1751
  }
1554
- else if (this.reportFolder) {
1555
- 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");
1556
1754
  }
1557
- else if (this.context && this.context.reportFolder) {
1558
- dataFile = path.join(this.context.reportFolder, "data.json");
1755
+ else if (referanceSnapshot.startsWith("yaml:")) {
1756
+ text = referanceSnapshot.substring(5);
1559
1757
  }
1560
1758
  else {
1561
- 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);
1562
1798
  }
1563
- return dataFile;
1564
1799
  }
1565
1800
  async waitForUserInput(message, world = null) {
1566
1801
  if (!message) {
@@ -1590,13 +1825,22 @@ class StableBrowser {
1590
1825
  return;
1591
1826
  }
1592
1827
  // if data file exists, load it
1593
- const dataFile = this._getDataFile(world);
1828
+ const dataFile = _getDataFile(world, this.context, this);
1594
1829
  let data = this.getTestData(world);
1595
1830
  // merge the testData with the existing data
1596
1831
  Object.assign(data, testData);
1597
1832
  // save the data to the file
1598
1833
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1599
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
+ }
1600
1844
  _getDataFilePath(fileName) {
1601
1845
  let dataFile = path.join(this.project_path, "data", fileName);
1602
1846
  if (fs.existsSync(dataFile)) {
@@ -1693,7 +1937,7 @@ class StableBrowser {
1693
1937
  }
1694
1938
  }
1695
1939
  getTestData(world = null) {
1696
- const dataFile = this._getDataFile(world);
1940
+ const dataFile = _getDataFile(world, this.context, this);
1697
1941
  let data = {};
1698
1942
  if (fs.existsSync(dataFile)) {
1699
1943
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1725,11 +1969,9 @@ class StableBrowser {
1725
1969
  if (!fs.existsSync(world.screenshotPath)) {
1726
1970
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1727
1971
  }
1728
- let nextIndex = 1;
1729
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1730
- nextIndex++;
1731
- }
1732
- 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");
1733
1975
  try {
1734
1976
  await this.takeScreenshot(screenshotPath);
1735
1977
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1739,15 +1981,15 @@ class StableBrowser {
1739
1981
  // this.logger.info("unable to save screenshot " + screenshotPath);
1740
1982
  // }
1741
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
+ }
1742
1989
  }
1743
1990
  catch (e) {
1744
1991
  this.logger.info("unable to take screenshot, ignored");
1745
1992
  }
1746
- result.screenshotId = nextIndex;
1747
- result.screenshotPath = screenshotPath;
1748
- if (info && info.box) {
1749
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1750
- }
1751
1993
  }
1752
1994
  else if (options && options.screenshot) {
1753
1995
  result.screenshotPath = options.screenshotPath;
@@ -1772,7 +2014,6 @@ class StableBrowser {
1772
2014
  }
1773
2015
  async takeScreenshot(screenshotPath) {
1774
2016
  const playContext = this.context.playContext;
1775
- const client = await playContext.newCDPSession(this.page);
1776
2017
  // Using CDP to capture the screenshot
1777
2018
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
1778
2019
  document.body.scrollWidth,
@@ -1782,164 +2023,241 @@ class StableBrowser {
1782
2023
  document.body.clientWidth,
1783
2024
  document.documentElement.clientWidth,
1784
2025
  ])));
1785
- const viewportHeight = Math.max(...(await this.page.evaluate(() => [
1786
- document.body.scrollHeight,
1787
- document.documentElement.scrollHeight,
1788
- document.body.offsetHeight,
1789
- document.documentElement.offsetHeight,
1790
- document.body.clientHeight,
1791
- document.documentElement.clientHeight,
1792
- ])));
1793
- const { data } = await client.send("Page.captureScreenshot", {
1794
- format: "png",
1795
- // clip: {
1796
- // x: 0,
1797
- // y: 0,
1798
- // width: viewportWidth,
1799
- // height: viewportHeight,
1800
- // scale: 1,
1801
- // },
1802
- });
1803
- if (!screenshotPath) {
1804
- return data;
1805
- }
1806
- let screenshotBuffer = Buffer.from(data, "base64");
1807
- const sharpBuffer = sharp(screenshotBuffer);
1808
- const metadata = await sharpBuffer.metadata();
1809
- //check if you are on retina display and reduce the quality of the image
1810
- if (metadata.width > viewportWidth || metadata.height > viewportHeight) {
1811
- screenshotBuffer = await sharpBuffer
1812
- .resize(viewportWidth, viewportHeight, {
1813
- fit: sharp.fit.inside,
1814
- withoutEnlargement: true,
1815
- })
1816
- .toBuffer();
1817
- }
1818
- fs.writeFileSync(screenshotPath, screenshotBuffer);
1819
- 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;
1820
2074
  }
1821
2075
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1822
- this._validateSelectors(selectors);
1823
- const startTime = Date.now();
1824
- let error = null;
1825
- let screenshotId = null;
1826
- 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
+ };
1827
2086
  await new Promise((resolve) => setTimeout(resolve, 2000));
1828
- const info = {};
1829
- info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1830
- info.operation = "verify";
1831
- info.selectors = selectors;
1832
2087
  try {
1833
- const element = await this._locate(selectors, info, _params);
1834
- if (element) {
1835
- await this.scrollIfNeeded(element, info);
1836
- }
1837
- await this._highlightElements(element);
1838
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1839
- await expect(element).toHaveCount(1, { timeout: 10000 });
1840
- return info;
2088
+ await _preCommand(state, this);
2089
+ await expect(state.element).toHaveCount(1, { timeout: 10000 });
2090
+ return state.info;
1841
2091
  }
1842
2092
  catch (e) {
1843
- //await this.closeUnexpectedPopups();
1844
- this.logger.error("verify failed " + info.log);
1845
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1846
- info.screenshotPath = screenshotPath;
1847
- Object.assign(e, { info: info });
1848
- error = e;
1849
- throw e;
2093
+ await _commandError(state, e, this);
1850
2094
  }
1851
2095
  finally {
1852
- const endTime = Date.now();
1853
- this._reportToWorld(world, {
1854
- element_name: selectors.element_name,
1855
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1856
- text: "Verify element exists in page",
1857
- screenshotId,
1858
- result: error
1859
- ? {
1860
- status: "FAILED",
1861
- startTime,
1862
- endTime,
1863
- message: error === null || error === void 0 ? void 0 : error.message,
1864
- }
1865
- : {
1866
- status: "PASSED",
1867
- startTime,
1868
- endTime,
1869
- },
1870
- info: info,
1871
- });
2096
+ await _commandFinally(state, this);
1872
2097
  }
1873
2098
  }
1874
2099
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1875
- this._validateSelectors(selectors);
1876
- const startTime = Date.now();
1877
- let error = null;
1878
- let screenshotId = null;
1879
- 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
+ };
1880
2114
  await new Promise((resolve) => setTimeout(resolve, 2000));
1881
- const info = {};
1882
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1883
- info.operation = "extract";
1884
- info.selectors = selectors;
1885
2115
  try {
1886
- const element = await this._locate(selectors, info, _params);
1887
- await this._highlightElements(element);
1888
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2116
+ await _preCommand(state, this);
1889
2117
  switch (attribute) {
1890
2118
  case "inner_text":
1891
- info.value = await element.innerText();
2119
+ state.value = await state.element.innerText();
1892
2120
  break;
1893
2121
  case "href":
1894
- info.value = await element.getAttribute("href");
2122
+ state.value = await state.element.getAttribute("href");
1895
2123
  break;
1896
2124
  case "value":
1897
- info.value = await element.inputValue();
2125
+ state.value = await state.element.inputValue();
2126
+ break;
2127
+ case "text":
2128
+ state.value = await state.element.textContent();
1898
2129
  break;
1899
2130
  default:
1900
- info.value = await element.getAttribute(attribute);
2131
+ state.value = await state.element.getAttribute(attribute);
1901
2132
  break;
1902
2133
  }
1903
- this[variable] = info.value;
1904
- if (world) {
1905
- 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
+ }
1906
2151
  }
1907
- this.setTestData({ [variable]: info.value }, world);
1908
- this.logger.info("set test data: " + variable + "=" + info.value);
1909
- 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;
1910
2157
  }
1911
2158
  catch (e) {
1912
- //await this.closeUnexpectedPopups();
1913
- this.logger.error("extract failed " + info.log);
1914
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1915
- info.screenshotPath = screenshotPath;
1916
- Object.assign(e, { info: info });
1917
- error = e;
1918
- throw e;
2159
+ await _commandError(state, e, this);
1919
2160
  }
1920
2161
  finally {
1921
- const endTime = Date.now();
1922
- this._reportToWorld(world, {
1923
- element_name: selectors.element_name,
1924
- type: Types.EXTRACT_ATTRIBUTE,
1925
- variable: variable,
1926
- value: info.value,
1927
- text: "Extract attribute from element",
1928
- screenshotId,
1929
- result: error
1930
- ? {
1931
- status: "FAILED",
1932
- startTime,
1933
- endTime,
1934
- 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);
1935
2232
  }
1936
- : {
1937
- status: "PASSED",
1938
- startTime,
1939
- endTime,
1940
- },
1941
- info: info,
1942
- });
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);
1943
2261
  }
1944
2262
  }
1945
2263
  async extractEmailData(emailAddress, options, world) {
@@ -1960,7 +2278,7 @@ class StableBrowser {
1960
2278
  if (options && options.timeout) {
1961
2279
  timeout = options.timeout;
1962
2280
  }
1963
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2281
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1964
2282
  const request = {
1965
2283
  method: "POST",
1966
2284
  url: serviceUrl,
@@ -2016,7 +2334,8 @@ class StableBrowser {
2016
2334
  catch (e) {
2017
2335
  errorCount++;
2018
2336
  if (errorCount > 3) {
2019
- throw e;
2337
+ // throw e;
2338
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
2020
2339
  }
2021
2340
  // ignore
2022
2341
  }
@@ -2030,27 +2349,32 @@ class StableBrowser {
2030
2349
  async _highlightElements(scope, css) {
2031
2350
  try {
2032
2351
  if (!scope) {
2352
+ // console.log(`Scope is not defined`);
2033
2353
  return;
2034
2354
  }
2035
2355
  if (!css) {
2036
2356
  scope
2037
2357
  .evaluate((node) => {
2038
2358
  if (node && node.style) {
2039
- let originalBorder = node.style.border;
2040
- 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}`);
2041
2364
  if (window) {
2042
2365
  window.addEventListener("beforeunload", function (e) {
2043
- node.style.border = originalBorder;
2366
+ node.style.outline = originalOutline;
2044
2367
  });
2045
2368
  }
2046
2369
  setTimeout(function () {
2047
- node.style.border = originalBorder;
2370
+ node.style.outline = originalOutline;
2048
2371
  }, 2000);
2049
2372
  }
2050
2373
  })
2051
2374
  .then(() => { })
2052
2375
  .catch((e) => {
2053
2376
  // ignore
2377
+ // console.error(`Could not highlight node : ${e}`);
2054
2378
  });
2055
2379
  }
2056
2380
  else {
@@ -2066,17 +2390,18 @@ class StableBrowser {
2066
2390
  if (!element.style) {
2067
2391
  return;
2068
2392
  }
2069
- var originalBorder = element.style.border;
2393
+ let originalOutline = element.style.outline;
2394
+ element.__previousOutline = originalOutline;
2070
2395
  // Set the new border to be red and 2px solid
2071
- element.style.border = "2px solid red";
2396
+ element.style.outline = "2px solid red";
2072
2397
  if (window) {
2073
2398
  window.addEventListener("beforeunload", function (e) {
2074
- element.style.border = originalBorder;
2399
+ element.style.outline = originalOutline;
2075
2400
  });
2076
2401
  }
2077
2402
  // Set a timeout to revert to the original border after 2 seconds
2078
2403
  setTimeout(function () {
2079
- element.style.border = originalBorder;
2404
+ element.style.outline = originalOutline;
2080
2405
  }, 2000);
2081
2406
  }
2082
2407
  return;
@@ -2084,6 +2409,7 @@ class StableBrowser {
2084
2409
  .then(() => { })
2085
2410
  .catch((e) => {
2086
2411
  // ignore
2412
+ // console.error(`Could not highlight css: ${e}`);
2087
2413
  });
2088
2414
  }
2089
2415
  }
@@ -2091,173 +2417,563 @@ class StableBrowser {
2091
2417
  console.debug(error);
2092
2418
  }
2093
2419
  }
2094
- async verifyPagePath(pathPart, options = {}, world = null) {
2095
- const startTime = Date.now();
2096
- let error = null;
2097
- let screenshotId = null;
2098
- 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);
2099
2732
  await new Promise((resolve) => setTimeout(resolve, 2000));
2100
- const info = {};
2101
- info.log = "***** verify page path " + pathPart + " *****\n";
2102
- info.operation = "verifyPagePath";
2103
- const newValue = await this._replaceWithLocalData(pathPart, world);
2104
- if (newValue !== pathPart) {
2105
- this.logger.info(pathPart + "=" + newValue);
2106
- pathPart = newValue;
2733
+ const newValue = await this._replaceWithLocalData(text, world);
2734
+ if (newValue !== text) {
2735
+ this.logger.info(text + "=" + newValue);
2736
+ text = newValue;
2107
2737
  }
2108
- info.pathPart = pathPart;
2738
+ let dateAlternatives = findDateAlternatives(text);
2739
+ let numberAlternatives = findNumberAlternatives(text);
2109
2740
  try {
2110
- for (let i = 0; i < 30; i++) {
2111
- const url = await this.page.url();
2112
- if (!url.includes(pathPart)) {
2113
- if (i === 29) {
2114
- 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`);
2115
2756
  }
2116
2757
  await new Promise((resolve) => setTimeout(resolve, 1000));
2117
2758
  continue;
2118
2759
  }
2119
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2120
- 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
+ }
2121
2777
  }
2122
2778
  }
2123
2779
  catch (e) {
2124
- //await this.closeUnexpectedPopups();
2125
- this.logger.error("verify page path failed " + info.log);
2126
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2127
- info.screenshotPath = screenshotPath;
2128
- Object.assign(e, { info: info });
2129
- error = e;
2130
- throw e;
2780
+ await _commandError(state, e, this);
2131
2781
  }
2132
2782
  finally {
2133
- const endTime = Date.now();
2134
- this._reportToWorld(world, {
2135
- type: Types.VERIFY_PAGE_PATH,
2136
- text: "Verify page path",
2137
- screenshotId,
2138
- result: error
2139
- ? {
2140
- status: "FAILED",
2141
- startTime,
2142
- endTime,
2143
- message: error === null || error === void 0 ? void 0 : error.message,
2144
- }
2145
- : {
2146
- status: "PASSED",
2147
- startTime,
2148
- endTime,
2149
- },
2150
- info: info,
2151
- });
2783
+ await _commandFinally(state, this);
2152
2784
  }
2153
2785
  }
2154
- async verifyTextExistInPage(text, options = {}, world = null) {
2155
- const startTime = Date.now();
2156
- const timeout = this._getLoadTimeout(options);
2157
- let error = null;
2158
- let screenshotId = null;
2159
- 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);
2160
2805
  await new Promise((resolve) => setTimeout(resolve, 2000));
2161
- const info = {};
2162
- info.log = "***** verify text " + text + " exists in page *****\n";
2163
- info.operation = "verifyTextExistInPage";
2164
2806
  const newValue = await this._replaceWithLocalData(text, world);
2165
2807
  if (newValue !== text) {
2166
2808
  this.logger.info(text + "=" + newValue);
2167
2809
  text = newValue;
2168
2810
  }
2169
- info.text = text;
2170
2811
  let dateAlternatives = findDateAlternatives(text);
2171
2812
  let numberAlternatives = findNumberAlternatives(text);
2172
2813
  try {
2814
+ await _preCommand(state, this);
2815
+ state.info.text = text;
2816
+ let resultWithElementsFound = {
2817
+ length: null, // initial cannot be 0
2818
+ };
2173
2819
  while (true) {
2174
- const frames = this.page.frames();
2175
- let results = [];
2176
- for (let i = 0; i < frames.length; i++) {
2177
- if (dateAlternatives.date) {
2178
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2179
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
2180
- result.frame = frames[i];
2181
- results.push(result);
2182
- }
2183
- }
2184
- else if (numberAlternatives.number) {
2185
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2186
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
2187
- result.frame = frames[i];
2188
- results.push(result);
2189
- }
2190
- }
2191
- else {
2192
- const result = await this._locateElementByText(frames[i], text, "*", true, {});
2193
- result.frame = frames[i];
2194
- results.push(result);
2195
- }
2820
+ try {
2821
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2822
+ }
2823
+ catch (error) {
2824
+ // ignore
2196
2825
  }
2197
- info.results = results;
2198
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2199
2826
  if (resultWithElementsFound.length === 0) {
2200
- if (Date.now() - startTime > timeout) {
2201
- 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`);
2202
2890
  }
2203
2891
  await new Promise((resolve) => setTimeout(resolve, 1000));
2204
2892
  continue;
2205
2893
  }
2206
- if (resultWithElementsFound[0].randomToken) {
2207
- const frame = resultWithElementsFound[0].frame;
2208
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2209
- await this._highlightElements(frame, dataAttribute);
2210
- const element = await frame.$(dataAttribute);
2211
- if (element) {
2212
- await this.scrollIfNeeded(element, info);
2213
- 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
+ }
2214
2939
  }
2215
2940
  }
2216
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2217
- return info;
2941
+ catch (error) {
2942
+ console.error(error);
2943
+ }
2218
2944
  }
2219
2945
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2220
2946
  }
2221
2947
  catch (e) {
2222
- //await this.closeUnexpectedPopups();
2223
- this.logger.error("verify text exist in page failed " + info.log);
2224
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2225
- info.screenshotPath = screenshotPath;
2226
- Object.assign(e, { info: info });
2227
- error = e;
2228
- throw e;
2948
+ await _commandError(state, e, this);
2229
2949
  }
2230
2950
  finally {
2231
- const endTime = Date.now();
2232
- this._reportToWorld(world, {
2233
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2234
- text: "Verify text exists in page",
2235
- screenshotId,
2236
- result: error
2237
- ? {
2238
- status: "FAILED",
2239
- startTime,
2240
- endTime,
2241
- message: error === null || error === void 0 ? void 0 : error.message,
2242
- }
2243
- : {
2244
- status: "PASSED",
2245
- startTime,
2246
- endTime,
2247
- },
2248
- info: info,
2249
- });
2951
+ await _commandFinally(state, this);
2250
2952
  }
2251
2953
  }
2252
- _getServerUrl() {
2253
- let serviceUrl = "https://api.blinq.io";
2254
- if (process.env.NODE_ENV_BLINQ === "dev") {
2255
- serviceUrl = "https://dev.api.blinq.io";
2256
- }
2257
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2258
- 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
+ }
2259
2973
  }
2260
- return serviceUrl;
2974
+ // state.info.results = results;
2975
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2976
+ return resultWithElementsFound;
2261
2977
  }
2262
2978
  async visualVerification(text, options = {}, world = null) {
2263
2979
  const startTime = Date.now();
@@ -2273,14 +2989,17 @@ class StableBrowser {
2273
2989
  throw new Error("TOKEN is not set");
2274
2990
  }
2275
2991
  try {
2276
- let serviceUrl = this._getServerUrl();
2992
+ let serviceUrl = _getServerUrl();
2277
2993
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2278
2994
  info.screenshotPath = screenshotPath;
2279
2995
  const screenshot = await this.takeScreenshot();
2280
- const request = {
2281
- method: "POST",
2996
+ let request = {
2997
+ method: "post",
2998
+ maxBodyLength: Infinity,
2282
2999
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2283
3000
  headers: {
3001
+ "x-bvt-project-id": path.basename(this.project_path),
3002
+ "x-source": "aaa",
2284
3003
  "Content-Type": "application/json",
2285
3004
  Authorization: `Bearer ${process.env.TOKEN}`,
2286
3005
  },
@@ -2289,7 +3008,7 @@ class StableBrowser {
2289
3008
  screenshot: screenshot,
2290
3009
  }),
2291
3010
  };
2292
- let result = await this.context.api.request(request);
3011
+ const result = await axios.request(request);
2293
3012
  if (result.data.status !== true) {
2294
3013
  throw new Error("Visual validation failed");
2295
3014
  }
@@ -2309,20 +3028,22 @@ class StableBrowser {
2309
3028
  info.screenshotPath = screenshotPath;
2310
3029
  Object.assign(e, { info: info });
2311
3030
  error = e;
2312
- throw e;
3031
+ // throw e;
3032
+ await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
2313
3033
  }
2314
3034
  finally {
2315
3035
  const endTime = Date.now();
2316
- this._reportToWorld(world, {
3036
+ _reportToWorld(world, {
2317
3037
  type: Types.VERIFY_VISUAL,
2318
3038
  text: "Visual verification",
3039
+ _text: "Visual verification of " + text,
2319
3040
  screenshotId,
2320
3041
  result: error
2321
3042
  ? {
2322
3043
  status: "FAILED",
2323
3044
  startTime,
2324
3045
  endTime,
2325
- message: error === null || error === void 0 ? void 0 : error.message,
3046
+ message: error?.message,
2326
3047
  }
2327
3048
  : {
2328
3049
  status: "PASSED",
@@ -2354,13 +3075,14 @@ class StableBrowser {
2354
3075
  this.logger.info("Table data verified");
2355
3076
  }
2356
3077
  async getTableData(selectors, _params = null, options = {}, world = null) {
2357
- this._validateSelectors(selectors);
3078
+ _validateSelectors(selectors);
2358
3079
  const startTime = Date.now();
2359
3080
  let error = null;
2360
3081
  let screenshotId = null;
2361
3082
  let screenshotPath = null;
2362
3083
  const info = {};
2363
3084
  info.log = "";
3085
+ info.locatorLog = new LocatorLog(selectors);
2364
3086
  info.operation = "getTableData";
2365
3087
  info.selectors = selectors;
2366
3088
  try {
@@ -2376,11 +3098,12 @@ class StableBrowser {
2376
3098
  info.screenshotPath = screenshotPath;
2377
3099
  Object.assign(e, { info: info });
2378
3100
  error = e;
2379
- throw e;
3101
+ // throw e;
3102
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2380
3103
  }
2381
3104
  finally {
2382
3105
  const endTime = Date.now();
2383
- this._reportToWorld(world, {
3106
+ _reportToWorld(world, {
2384
3107
  element_name: selectors.element_name,
2385
3108
  type: Types.GET_TABLE_DATA,
2386
3109
  text: "Get table data",
@@ -2390,7 +3113,7 @@ class StableBrowser {
2390
3113
  status: "FAILED",
2391
3114
  startTime,
2392
3115
  endTime,
2393
- message: error === null || error === void 0 ? void 0 : error.message,
3116
+ message: error?.message,
2394
3117
  }
2395
3118
  : {
2396
3119
  status: "PASSED",
@@ -2402,7 +3125,7 @@ class StableBrowser {
2402
3125
  }
2403
3126
  }
2404
3127
  async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
2405
- this._validateSelectors(selectors);
3128
+ _validateSelectors(selectors);
2406
3129
  if (!query) {
2407
3130
  throw new Error("query is null");
2408
3131
  }
@@ -2435,7 +3158,7 @@ class StableBrowser {
2435
3158
  info.operation = "analyzeTable";
2436
3159
  info.selectors = selectors;
2437
3160
  info.query = query;
2438
- query = this._fixUsingParams(query, _params);
3161
+ query = _fixUsingParams(query, _params);
2439
3162
  info.query_fixed = query;
2440
3163
  info.operator = operator;
2441
3164
  info.value = value;
@@ -2541,11 +3264,12 @@ class StableBrowser {
2541
3264
  info.screenshotPath = screenshotPath;
2542
3265
  Object.assign(e, { info: info });
2543
3266
  error = e;
2544
- throw e;
3267
+ // throw e;
3268
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2545
3269
  }
2546
3270
  finally {
2547
3271
  const endTime = Date.now();
2548
- this._reportToWorld(world, {
3272
+ _reportToWorld(world, {
2549
3273
  element_name: selectors.element_name,
2550
3274
  type: Types.ANALYZE_TABLE,
2551
3275
  text: "Analyze table",
@@ -2555,7 +3279,7 @@ class StableBrowser {
2555
3279
  status: "FAILED",
2556
3280
  startTime,
2557
3281
  endTime,
2558
- message: error === null || error === void 0 ? void 0 : error.message,
3282
+ message: error?.message,
2559
3283
  }
2560
3284
  : {
2561
3285
  status: "PASSED",
@@ -2567,27 +3291,13 @@ class StableBrowser {
2567
3291
  }
2568
3292
  }
2569
3293
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2570
- if (!value) {
2571
- return value;
2572
- }
2573
- // find all the accurance of {{(.*?)}} and replace with the value
2574
- let regex = /{{(.*?)}}/g;
2575
- let matches = value.match(regex);
2576
- if (matches) {
2577
- const testData = this.getTestData(world);
2578
- for (let i = 0; i < matches.length; i++) {
2579
- let match = matches[i];
2580
- let key = match.substring(2, match.length - 2);
2581
- let newValue = objectPath.get(testData, key, null);
2582
- if (newValue !== null) {
2583
- value = value.replace(match, newValue);
2584
- }
2585
- }
3294
+ try {
3295
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
2586
3296
  }
2587
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2588
- return await decrypt(value, null, totpWait);
3297
+ catch (error) {
3298
+ this.logger.debug(error);
3299
+ throw error;
2589
3300
  }
2590
- return value;
2591
3301
  }
2592
3302
  _getLoadTimeout(options) {
2593
3303
  let timeout = 15000;
@@ -2599,6 +3309,37 @@ class StableBrowser {
2599
3309
  }
2600
3310
  return timeout;
2601
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
+ }
2602
3343
  async waitForPageLoad(options = {}, world = null) {
2603
3344
  let timeout = this._getLoadTimeout(options);
2604
3345
  const promiseArray = [];
@@ -2638,7 +3379,7 @@ class StableBrowser {
2638
3379
  await new Promise((resolve) => setTimeout(resolve, 2000));
2639
3380
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2640
3381
  const endTime = Date.now();
2641
- this._reportToWorld(world, {
3382
+ _reportToWorld(world, {
2642
3383
  type: Types.GET_PAGE_STATUS,
2643
3384
  text: "Wait for page load",
2644
3385
  screenshotId,
@@ -2647,7 +3388,7 @@ class StableBrowser {
2647
3388
  status: "FAILED",
2648
3389
  startTime,
2649
3390
  endTime,
2650
- message: error === null || error === void 0 ? void 0 : error.message,
3391
+ message: error?.message,
2651
3392
  }
2652
3393
  : {
2653
3394
  status: "PASSED",
@@ -2658,41 +3399,123 @@ class StableBrowser {
2658
3399
  }
2659
3400
  }
2660
3401
  async closePage(options = {}, world = null) {
2661
- const startTime = Date.now();
2662
- let error = null;
2663
- let screenshotId = null;
2664
- let screenshotPath = null;
2665
- 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
+ };
2666
3415
  try {
3416
+ await _preCommand(state, this);
2667
3417
  await this.page.close();
2668
3418
  }
2669
3419
  catch (e) {
2670
3420
  console.log(".");
3421
+ await _commandError(state, e, this);
2671
3422
  }
2672
3423
  finally {
2673
- await new Promise((resolve) => setTimeout(resolve, 2000));
2674
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2675
- const endTime = Date.now();
2676
- this._reportToWorld(world, {
2677
- type: Types.CLOSE_PAGE,
2678
- text: "close page",
2679
- screenshotId,
2680
- result: error
2681
- ? {
2682
- status: "FAILED",
2683
- startTime,
2684
- endTime,
2685
- 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;
2686
3459
  }
2687
- : {
2688
- status: "PASSED",
2689
- startTime,
2690
- endTime,
2691
- },
2692
- info: info,
2693
- });
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);
2694
3512
  }
2695
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
+ }
2696
3519
  async setViewportSize(width, hight, options = {}, world = null) {
2697
3520
  const startTime = Date.now();
2698
3521
  let error = null;
@@ -2710,21 +3533,23 @@ class StableBrowser {
2710
3533
  }
2711
3534
  catch (e) {
2712
3535
  console.log(".");
3536
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2713
3537
  }
2714
3538
  finally {
2715
3539
  await new Promise((resolve) => setTimeout(resolve, 2000));
2716
3540
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2717
3541
  const endTime = Date.now();
2718
- this._reportToWorld(world, {
3542
+ _reportToWorld(world, {
2719
3543
  type: Types.SET_VIEWPORT,
2720
3544
  text: "set viewport size to " + width + "x" + hight,
3545
+ _text: "Set the viewport size to " + width + "x" + hight,
2721
3546
  screenshotId,
2722
3547
  result: error
2723
3548
  ? {
2724
3549
  status: "FAILED",
2725
3550
  startTime,
2726
3551
  endTime,
2727
- message: error === null || error === void 0 ? void 0 : error.message,
3552
+ message: error?.message,
2728
3553
  }
2729
3554
  : {
2730
3555
  status: "PASSED",
@@ -2746,12 +3571,13 @@ class StableBrowser {
2746
3571
  }
2747
3572
  catch (e) {
2748
3573
  console.log(".");
3574
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2749
3575
  }
2750
3576
  finally {
2751
3577
  await new Promise((resolve) => setTimeout(resolve, 2000));
2752
3578
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2753
3579
  const endTime = Date.now();
2754
- this._reportToWorld(world, {
3580
+ _reportToWorld(world, {
2755
3581
  type: Types.GET_PAGE_STATUS,
2756
3582
  text: "page relaod",
2757
3583
  screenshotId,
@@ -2760,7 +3586,7 @@ class StableBrowser {
2760
3586
  status: "FAILED",
2761
3587
  startTime,
2762
3588
  endTime,
2763
- message: error === null || error === void 0 ? void 0 : error.message,
3589
+ message: error?.message,
2764
3590
  }
2765
3591
  : {
2766
3592
  status: "PASSED",
@@ -2787,11 +3613,195 @@ class StableBrowser {
2787
3613
  console.log("#-#");
2788
3614
  }
2789
3615
  }
2790
- _reportToWorld(world, properties) {
2791
- if (!world || !world.attach) {
2792
- 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, this.context);
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
+ }
2793
3804
  }
2794
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2795
3805
  }
2796
3806
  }
2797
3807
  function createTimedPromise(promise, label) {
@@ -2799,151 +3809,5 @@ function createTimedPromise(promise, label) {
2799
3809
  .then((result) => ({ status: "fulfilled", label, result }))
2800
3810
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2801
3811
  }
2802
- const KEYBOARD_EVENTS = [
2803
- "ALT",
2804
- "AltGraph",
2805
- "CapsLock",
2806
- "Control",
2807
- "Fn",
2808
- "FnLock",
2809
- "Hyper",
2810
- "Meta",
2811
- "NumLock",
2812
- "ScrollLock",
2813
- "Shift",
2814
- "Super",
2815
- "Symbol",
2816
- "SymbolLock",
2817
- "Enter",
2818
- "Tab",
2819
- "ArrowDown",
2820
- "ArrowLeft",
2821
- "ArrowRight",
2822
- "ArrowUp",
2823
- "End",
2824
- "Home",
2825
- "PageDown",
2826
- "PageUp",
2827
- "Backspace",
2828
- "Clear",
2829
- "Copy",
2830
- "CrSel",
2831
- "Cut",
2832
- "Delete",
2833
- "EraseEof",
2834
- "ExSel",
2835
- "Insert",
2836
- "Paste",
2837
- "Redo",
2838
- "Undo",
2839
- "Accept",
2840
- "Again",
2841
- "Attn",
2842
- "Cancel",
2843
- "ContextMenu",
2844
- "Escape",
2845
- "Execute",
2846
- "Find",
2847
- "Finish",
2848
- "Help",
2849
- "Pause",
2850
- "Play",
2851
- "Props",
2852
- "Select",
2853
- "ZoomIn",
2854
- "ZoomOut",
2855
- "BrightnessDown",
2856
- "BrightnessUp",
2857
- "Eject",
2858
- "LogOff",
2859
- "Power",
2860
- "PowerOff",
2861
- "PrintScreen",
2862
- "Hibernate",
2863
- "Standby",
2864
- "WakeUp",
2865
- "AllCandidates",
2866
- "Alphanumeric",
2867
- "CodeInput",
2868
- "Compose",
2869
- "Convert",
2870
- "Dead",
2871
- "FinalMode",
2872
- "GroupFirst",
2873
- "GroupLast",
2874
- "GroupNext",
2875
- "GroupPrevious",
2876
- "ModeChange",
2877
- "NextCandidate",
2878
- "NonConvert",
2879
- "PreviousCandidate",
2880
- "Process",
2881
- "SingleCandidate",
2882
- "HangulMode",
2883
- "HanjaMode",
2884
- "JunjaMode",
2885
- "Eisu",
2886
- "Hankaku",
2887
- "Hiragana",
2888
- "HiraganaKatakana",
2889
- "KanaMode",
2890
- "KanjiMode",
2891
- "Katakana",
2892
- "Romaji",
2893
- "Zenkaku",
2894
- "ZenkakuHanaku",
2895
- "F1",
2896
- "F2",
2897
- "F3",
2898
- "F4",
2899
- "F5",
2900
- "F6",
2901
- "F7",
2902
- "F8",
2903
- "F9",
2904
- "F10",
2905
- "F11",
2906
- "F12",
2907
- "Soft1",
2908
- "Soft2",
2909
- "Soft3",
2910
- "Soft4",
2911
- "ChannelDown",
2912
- "ChannelUp",
2913
- "Close",
2914
- "MailForward",
2915
- "MailReply",
2916
- "MailSend",
2917
- "MediaFastForward",
2918
- "MediaPause",
2919
- "MediaPlay",
2920
- "MediaPlayPause",
2921
- "MediaRecord",
2922
- "MediaRewind",
2923
- "MediaStop",
2924
- "MediaTrackNext",
2925
- "MediaTrackPrevious",
2926
- "AudioBalanceLeft",
2927
- "AudioBalanceRight",
2928
- "AudioBassBoostDown",
2929
- "AudioBassBoostToggle",
2930
- "AudioBassBoostUp",
2931
- "AudioFaderFront",
2932
- "AudioFaderRear",
2933
- "AudioSurroundModeNext",
2934
- "AudioTrebleDown",
2935
- "AudioTrebleUp",
2936
- "AudioVolumeDown",
2937
- "AudioVolumeMute",
2938
- "AudioVolumeUp",
2939
- "MicrophoneToggle",
2940
- "MicrophoneVolumeDown",
2941
- "MicrophoneVolumeMute",
2942
- "MicrophoneVolumeUp",
2943
- "TV",
2944
- "TV3DMode",
2945
- "TVAntennaCable",
2946
- "TVAudioDescription",
2947
- ];
2948
3812
  export { StableBrowser };
2949
3813
  //# sourceMappingURL=stable_browser.js.map