automation_model 1.0.445-dev → 1.0.445

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 +239 -49
  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 +37 -0
  43. package/lib/locator.js +172 -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 +152 -56
  56. package/lib/stable_browser.js +2416 -1303
  57. package/lib/stable_browser.js.map +1 -1
  58. package/lib/table.d.ts +15 -0
  59. package/lib/table.js +257 -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 +678 -11
  70. package/lib/utils.js.map +1 -1
  71. package/package.json +18 -10
@@ -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 { highlightSnapshot, 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,49 @@ 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
+ SLEEP: "sleep",
69
+ };
70
+ export const apps = {};
71
+ const formatElementName = (elementName) => {
72
+ return elementName ? JSON.stringify(elementName) : "element";
44
73
  };
45
74
  class StableBrowser {
46
- constructor(browser, page, logger = null, context = null) {
75
+ browser;
76
+ page;
77
+ logger;
78
+ context;
79
+ world;
80
+ fastMode;
81
+ project_path = null;
82
+ webLogFile = null;
83
+ networkLogger = null;
84
+ configuration = null;
85
+ appName = "main";
86
+ tags = null;
87
+ isRecording = false;
88
+ initSnapshotTaken = false;
89
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
47
90
  this.browser = browser;
48
91
  this.page = page;
49
92
  this.logger = logger;
50
93
  this.context = context;
51
- this.project_path = null;
52
- this.webLogFile = null;
53
- this.configuration = null;
94
+ this.world = world;
95
+ this.fastMode = fastMode;
54
96
  if (!this.logger) {
55
97
  this.logger = console;
56
98
  }
@@ -75,17 +117,49 @@ class StableBrowser {
75
117
  catch (e) {
76
118
  this.logger.error("unable to read ai_config.json");
77
119
  }
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
120
  context.pageLoading = { status: false };
121
+ context.pages = [this.page];
122
+ const logFolder = path.join(this.project_path, "logs", "web");
123
+ this.world = world;
124
+ if (process.env.FAST_MODE === "true") {
125
+ this.fastMode = true;
126
+ }
127
+ if (this.context) {
128
+ this.context.fastMode = this.fastMode;
129
+ }
130
+ this.registerEventListeners(this.context);
131
+ registerNetworkEvents(this.world, this, this.context, this.page);
132
+ registerDownloadEvent(this.page, this.world, this.context);
133
+ }
134
+ registerEventListeners(context) {
135
+ this.registerConsoleLogListener(this.page, context);
136
+ // this.registerRequestListener(this.page, context, this.webLogFile);
137
+ if (!context.pageLoading) {
138
+ context.pageLoading = { status: false };
139
+ }
140
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
141
+ this.page.on("dialog", (dialog) => dialog.accept());
142
+ }
84
143
  context.playContext.on("page", async function (page) {
144
+ if (this.configuration && this.configuration.closePopups === true) {
145
+ console.log("close unexpected popups");
146
+ await page.close();
147
+ return;
148
+ }
85
149
  context.pageLoading.status = true;
86
150
  this.page = page;
151
+ try {
152
+ if (this.configuration && this.configuration.acceptDialog) {
153
+ await page.on("dialog", (dialog) => dialog.accept());
154
+ }
155
+ }
156
+ catch (error) {
157
+ console.error("Error on dialog accept registration", error);
158
+ }
87
159
  context.page = page;
88
160
  context.pages.push(page);
161
+ registerNetworkEvents(this.world, this, context, this.page);
162
+ registerDownloadEvent(this.page, this.world, context);
89
163
  page.on("close", async () => {
90
164
  if (this.context && this.context.pages && this.context.pages.length > 1) {
91
165
  this.context.pages.pop();
@@ -110,117 +184,167 @@ class StableBrowser {
110
184
  context.pageLoading.status = false;
111
185
  }.bind(this));
112
186
  }
113
- getWebLogFile(logFolder) {
114
- if (!fs.existsSync(logFolder)) {
115
- fs.mkdirSync(logFolder, { recursive: true });
187
+ async switchApp(appName) {
188
+ // check if the current app (this.appName) is the same as the new app
189
+ if (this.appName === appName) {
190
+ return;
191
+ }
192
+ let newContextCreated = false;
193
+ if (!apps[appName]) {
194
+ let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
195
+ newContextCreated = true;
196
+ apps[appName] = {
197
+ context: newContext,
198
+ browser: newContext.browser,
199
+ page: newContext.page,
200
+ };
201
+ }
202
+ const tempContext = {};
203
+ _copyContext(this, tempContext);
204
+ _copyContext(apps[appName], this);
205
+ apps[this.appName] = tempContext;
206
+ this.appName = appName;
207
+ if (newContextCreated) {
208
+ this.registerEventListeners(this.context);
209
+ await this.goto(this.context.environment.baseUrl);
210
+ if (!this.fastMode) {
211
+ await this.waitForPageLoad();
212
+ }
213
+ }
214
+ }
215
+ async switchTab(tabTitleOrIndex) {
216
+ // first check if the tabNameOrIndex is a number
217
+ let index = parseInt(tabTitleOrIndex);
218
+ if (!isNaN(index)) {
219
+ if (index >= 0 && index < this.context.pages.length) {
220
+ this.page = this.context.pages[index];
221
+ this.context.page = this.page;
222
+ await this.page.bringToFront();
223
+ return;
224
+ }
116
225
  }
117
- let nextIndex = 1;
118
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
119
- nextIndex++;
226
+ // if the tabNameOrIndex is a string, find the tab by name
227
+ for (let i = 0; i < this.context.pages.length; i++) {
228
+ let page = this.context.pages[i];
229
+ let title = await page.title();
230
+ if (title.includes(tabTitleOrIndex)) {
231
+ this.page = page;
232
+ this.context.page = this.page;
233
+ await this.page.bringToFront();
234
+ return;
235
+ }
120
236
  }
121
- const fileName = nextIndex + ".json";
122
- return path.join(logFolder, fileName);
237
+ throw new Error("Tab not found: " + tabTitleOrIndex);
123
238
  }
124
- registerConsoleLogListener(page, context, logFile) {
239
+ registerConsoleLogListener(page, context) {
125
240
  if (!this.context.webLogger) {
126
241
  this.context.webLogger = [];
127
242
  }
128
243
  page.on("console", async (msg) => {
129
- this.context.webLogger.push({
244
+ const obj = {
130
245
  type: msg.type(),
131
246
  text: msg.text(),
132
247
  location: msg.location(),
133
248
  time: new Date().toISOString(),
134
- });
135
- await fs.promises.writeFile(logFile, JSON.stringify(this.context.webLogger, null, 2));
249
+ };
250
+ this.context.webLogger.push(obj);
251
+ if (msg.type() === "error") {
252
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
253
+ }
136
254
  });
137
255
  }
138
- registerRequestListener() {
139
- this.page.on("request", async (data) => {
256
+ registerRequestListener(page, context, logFile) {
257
+ if (!this.context.networkLogger) {
258
+ this.context.networkLogger = [];
259
+ }
260
+ page.on("request", async (data) => {
261
+ const startTime = new Date().getTime();
140
262
  try {
141
- const pageUrl = new URL(this.page.url());
263
+ const pageUrl = new URL(page.url());
142
264
  const requestUrl = new URL(data.url());
143
265
  if (pageUrl.hostname === requestUrl.hostname) {
144
266
  const method = data.method();
145
- if (method === "POST" || method === "GET" || method === "PUT" || method === "DELETE" || method === "PATCH") {
267
+ if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
146
268
  const token = await data.headerValue("Authorization");
147
269
  if (token) {
148
- this.context.authtoken = token;
270
+ context.authtoken = token;
149
271
  }
150
272
  }
151
273
  }
274
+ const response = await data.response();
275
+ const endTime = new Date().getTime();
276
+ const obj = {
277
+ url: data.url(),
278
+ method: data.method(),
279
+ postData: data.postData(),
280
+ error: data.failure() ? data.failure().errorText : null,
281
+ duration: endTime - startTime,
282
+ startTime,
283
+ };
284
+ context.networkLogger.push(obj);
285
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
152
286
  }
153
287
  catch (error) {
154
- console.error("Error in request listener", error);
288
+ // console.error("Error in request listener", error);
289
+ context.networkLogger.push({
290
+ error: "not able to listen",
291
+ message: error.message,
292
+ stack: error.stack,
293
+ time: new Date().toISOString(),
294
+ });
295
+ // await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
155
296
  }
156
297
  });
157
298
  }
158
299
  // async closeUnexpectedPopups() {
159
300
  // await closeUnexpectedPopups(this.page);
160
301
  // }
161
- async goto(url) {
302
+ async goto(url, world = null) {
303
+ if (!url) {
304
+ throw new Error("url is null, verify that the environment file is correct");
305
+ }
306
+ url = await this._replaceWithLocalData(url, this.world);
162
307
  if (!url.startsWith("http")) {
163
308
  url = "https://" + url;
164
309
  }
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;
310
+ const state = {
311
+ value: url,
312
+ world: world,
313
+ type: Types.NAVIGATE,
314
+ text: `Navigate Page to: ${url}`,
315
+ operation: "goto",
316
+ log: "***** navigate page to " + url + " *****\n",
317
+ info: {},
318
+ locate: false,
319
+ scroll: false,
320
+ screenshot: false,
321
+ highlight: false,
322
+ };
323
+ try {
324
+ await _preCommand(state, this);
325
+ await this.page.goto(url, {
326
+ timeout: 60000,
327
+ });
328
+ await _screenshot(state, this);
186
329
  }
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]);
330
+ catch (error) {
331
+ console.error("Error on goto", error);
332
+ _commandError(state, error, this);
194
333
  }
195
- return text;
196
- }
197
- _fixLocatorUsingParams(locator, _params) {
198
- // check if not null
199
- if (!locator) {
200
- return locator;
334
+ finally {
335
+ await _commandFinally(state, this);
201
336
  }
202
- // clone the locator
203
- locator = JSON.parse(JSON.stringify(locator));
204
- this.scanAndManipulate(locator, _params);
205
- return locator;
206
- }
207
- _isObject(value) {
208
- return value && typeof value === "object" && value.constructor === Object;
209
337
  }
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);
338
+ async _getLocator(locator, scope, _params) {
339
+ locator = _fixLocatorUsingParams(locator, _params);
340
+ // locator = await this._replaceWithLocalData(locator);
341
+ for (let key in locator) {
342
+ if (typeof locator[key] !== "string")
343
+ continue;
344
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
345
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
219
346
  }
220
347
  }
221
- }
222
- _getLocator(locator, scope, _params) {
223
- locator = this._fixLocatorUsingParams(locator, _params);
224
348
  let locatorReturn;
225
349
  if (locator.role) {
226
350
  if (locator.role[1].nameReg) {
@@ -228,7 +352,7 @@ class StableBrowser {
228
352
  delete locator.role[1].nameReg;
229
353
  }
230
354
  // if (locator.role[1].name) {
231
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
355
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
232
356
  // }
233
357
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
234
358
  }
@@ -247,7 +371,7 @@ class StableBrowser {
247
371
  locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
248
372
  }
249
373
  }
250
- if (locator === null || locator === void 0 ? void 0 : locator.engine) {
374
+ if (locator?.engine) {
251
375
  if (locator.engine === "css") {
252
376
  locatorReturn = scope.locator(locator.selector);
253
377
  }
@@ -268,192 +392,181 @@ class StableBrowser {
268
392
  return locatorReturn;
269
393
  }
270
394
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
271
- let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
395
+ if (css && css.locator) {
396
+ css = css.locator;
397
+ }
398
+ let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
272
399
  if (result.elementCount === 0) {
273
400
  return;
274
401
  }
275
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
402
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
276
403
  // css climb to parent element
277
404
  const climbArray = [];
278
405
  for (let i = 0; i < climb; i++) {
279
406
  climbArray.push("..");
280
407
  }
281
408
  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
- }
409
+ let resultCss = textElementCss + " >> " + climbXpath;
410
+ if (css) {
411
+ resultCss = resultCss + " >> " + css;
412
+ }
413
+ return resultCss;
414
+ }
415
+ async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
416
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
417
+ const locator = scope.locator(query);
418
+ const count = await locator.count();
419
+ if (!tag1) {
420
+ tag1 = "*";
421
+ }
422
+ const randomToken = Math.random().toString(36).substring(7);
423
+ let tagCount = 0;
424
+ for (let i = 0; i < count; i++) {
425
+ const element = locator.nth(i);
426
+ // check if the tag matches
427
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
428
+ if (!tag.startsWith("*")) {
429
+ if (el.tagName.toLowerCase() !== tag) {
430
+ return false;
431
+ }
432
+ }
433
+ if (!el.setAttribute) {
434
+ el = el.parentElement;
435
+ }
436
+ // remove any attributes start with data-blinq-id
437
+ // for (let i = 0; i < el.attributes.length; i++) {
438
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
439
+ // el.removeAttribute(el.attributes[i].name);
440
+ // }
441
+ // }
442
+ el.setAttribute("data-blinq-id-" + randomToken, "");
443
+ return true;
444
+ }, [tag1, randomToken]))) {
445
+ continue;
385
446
  }
386
- return { elementCount: elementCount, randomToken: randomToken };
387
- }, [text1, tag1, regex1, partial1]);
447
+ tagCount++;
448
+ }
449
+ return { elementCount: tagCount, randomToken };
388
450
  }
389
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
451
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
452
+ if (!info) {
453
+ info = {};
454
+ }
455
+ if (!info.failCause) {
456
+ info.failCause = {};
457
+ }
458
+ if (!info.log) {
459
+ info.log = "";
460
+ info.locatorLog = new LocatorLog(selectorHierarchy);
461
+ }
390
462
  let locatorSearch = selectorHierarchy[index];
463
+ let originalLocatorSearch = "";
464
+ try {
465
+ originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
466
+ locatorSearch = JSON.parse(originalLocatorSearch);
467
+ }
468
+ catch (e) {
469
+ console.error(e);
470
+ }
391
471
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
392
472
  let locator = null;
393
473
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
394
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
474
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
475
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
395
476
  if (!locatorString) {
477
+ info.failCause.textNotFound = true;
478
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
396
479
  return;
397
480
  }
398
- locator = this._getLocator({ css: locatorString }, scope, _params);
481
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
399
482
  }
400
483
  else if (locatorSearch.text) {
401
- let result = await this._locateElementByText(scope, this._fixUsingParams(locatorSearch.text, _params), locatorSearch.tag, false, locatorSearch.partial === true, _params);
484
+ let text = _fixUsingParams(locatorSearch.text, _params);
485
+ let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
402
486
  if (result.elementCount === 0) {
487
+ info.failCause.textNotFound = true;
488
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
403
489
  return;
404
490
  }
405
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
491
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
406
492
  if (locatorSearch.childCss) {
407
493
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
408
494
  }
409
- locator = this._getLocator(locatorSearch, scope, _params);
495
+ locator = await this._getLocator(locatorSearch, scope, _params);
410
496
  }
411
497
  else {
412
- locator = this._getLocator(locatorSearch, scope, _params);
498
+ locator = await this._getLocator(locatorSearch, scope, _params);
413
499
  }
414
500
  // let cssHref = false;
415
501
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
416
502
  // cssHref = true;
417
503
  // }
418
504
  let count = await locator.count();
505
+ if (count > 0 && !info.failCause.count) {
506
+ info.failCause.count = count;
507
+ }
419
508
  //info.log += "total elements found " + count + "\n";
420
509
  //let visibleCount = 0;
421
510
  let visibleLocator = null;
422
- if (locatorSearch.index && locatorSearch.index < count) {
511
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
423
512
  foundLocators.push(locator.nth(locatorSearch.index));
513
+ if (info.locatorLog) {
514
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
515
+ }
424
516
  return;
425
517
  }
518
+ if (info.locatorLog && count === 0 && logErrors) {
519
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
520
+ }
426
521
  for (let j = 0; j < count; j++) {
427
522
  let visible = await locator.nth(j).isVisible();
428
523
  const enabled = await locator.nth(j).isEnabled();
429
524
  if (!visibleOnly) {
430
525
  visible = true;
431
526
  }
432
- if (visible && enabled) {
527
+ if (visible && (allowDisabled || enabled)) {
433
528
  foundLocators.push(locator.nth(j));
529
+ if (info.locatorLog) {
530
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
531
+ }
434
532
  }
435
- else {
533
+ else if (logErrors) {
534
+ info.failCause.visible = visible;
535
+ info.failCause.enabled = enabled;
436
536
  if (!info.printMessages) {
437
537
  info.printMessages = {};
438
538
  }
539
+ if (info.locatorLog && !visible) {
540
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
541
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
542
+ }
543
+ if (info.locatorLog && !enabled) {
544
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
545
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
546
+ }
439
547
  if (!info.printMessages[j.toString()]) {
440
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
548
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
441
549
  info.printMessages[j.toString()] = true;
442
550
  }
443
551
  }
444
552
  }
445
553
  }
446
554
  async closeUnexpectedPopups(info, _params) {
555
+ if (!info) {
556
+ info = {};
557
+ info.failCause = {};
558
+ info.log = "";
559
+ }
447
560
  if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
448
561
  if (!info) {
449
562
  info = {};
450
563
  }
451
- info.log += "scan for popup handlers" + "\n";
564
+ //info.log += "scan for popup handlers" + "\n";
452
565
  const handlerGroup = [];
453
566
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
454
567
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
455
568
  }
456
- const scopes = [this.page, ...this.page.frames()];
569
+ const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
457
570
  let result = null;
458
571
  let scope = null;
459
572
  for (let i = 0; i < scopes.length; i++) {
@@ -475,55 +588,108 @@ class StableBrowser {
475
588
  }
476
589
  if (result.foundElements.length > 0) {
477
590
  let dialogCloseLocator = result.foundElements[0].locator;
478
- await dialogCloseLocator.click();
479
- // wait for the dialog to close
480
- await dialogCloseLocator.waitFor({ state: "hidden" });
591
+ try {
592
+ await scope?.evaluate(() => {
593
+ window.__isClosingPopups = true;
594
+ });
595
+ await dialogCloseLocator.click();
596
+ // wait for the dialog to close
597
+ await dialogCloseLocator.waitFor({ state: "hidden" });
598
+ }
599
+ catch (e) {
600
+ }
601
+ finally {
602
+ await scope?.evaluate(() => {
603
+ window.__isClosingPopups = false;
604
+ });
605
+ }
481
606
  return { rerun: true };
482
607
  }
483
608
  }
484
609
  }
485
610
  return { rerun: false };
486
611
  }
487
- async _locate(selectors, info, _params, timeout = 30000) {
612
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
613
+ if (!timeout) {
614
+ timeout = 30000;
615
+ }
488
616
  for (let i = 0; i < 3; i++) {
489
617
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
490
618
  for (let j = 0; j < selectors.locators.length; j++) {
491
619
  let selector = selectors.locators[j];
492
620
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
493
621
  }
494
- let element = await this._locate_internal(selectors, info, _params, timeout);
622
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
495
623
  if (!element.rerun) {
496
- return element;
624
+ const randomToken = Math.random().toString(36).substring(7);
625
+ await element.evaluate((el, randomToken) => {
626
+ el.setAttribute("data-blinq-id-" + randomToken, "");
627
+ }, randomToken);
628
+ // if (element._frame) {
629
+ // return element;
630
+ // }
631
+ const scope = element._frame ?? element.page();
632
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
633
+ let prefixSelector = "";
634
+ const frameControlSelector = " >> internal:control=enter-frame";
635
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
636
+ if (frameSelectorIndex !== -1) {
637
+ // remove everything after the >> internal:control=enter-frame
638
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
639
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
640
+ }
641
+ // if (element?._frame?._selector) {
642
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
643
+ // }
644
+ const newSelector = prefixSelector + newElementSelector;
645
+ return scope.locator(newSelector);
497
646
  }
498
647
  }
499
648
  throw new Error("unable to locate element " + JSON.stringify(selectors));
500
649
  }
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);
650
+ async _findFrameScope(selectors, timeout = 30000, info) {
651
+ if (!info) {
652
+ info = {};
653
+ info.failCause = {};
654
+ info.log = "";
655
+ }
656
+ let startTime = Date.now();
507
657
  let scope = this.page;
658
+ if (selectors.frame) {
659
+ return selectors.frame;
660
+ }
508
661
  if (selectors.iframe_src || selectors.frameLocators) {
509
- const findFrame = (frame, framescope) => {
662
+ const findFrame = async (frame, framescope) => {
510
663
  for (let i = 0; i < frame.selectors.length; i++) {
511
664
  let frameLocator = frame.selectors[i];
512
665
  if (frameLocator.css) {
513
- framescope = framescope.frameLocator(frameLocator.css);
514
- break;
666
+ let testframescope = framescope.frameLocator(frameLocator.css);
667
+ if (frameLocator.index) {
668
+ testframescope = framescope.nth(frameLocator.index);
669
+ }
670
+ try {
671
+ await testframescope.owner().evaluateHandle(() => true, null, {
672
+ timeout: 5000,
673
+ });
674
+ framescope = testframescope;
675
+ break;
676
+ }
677
+ catch (error) {
678
+ console.error("frame not found " + frameLocator.css);
679
+ }
515
680
  }
516
681
  }
517
682
  if (frame.children) {
518
- return findFrame(frame.children, framescope);
683
+ return await findFrame(frame.children, framescope);
519
684
  }
520
685
  return framescope;
521
686
  };
522
- info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
687
+ let fLocator = null;
523
688
  while (true) {
524
689
  let frameFound = false;
525
690
  if (selectors.nestFrmLoc) {
526
- scope = findFrame(selectors.nestFrmLoc, scope);
691
+ fLocator = selectors.nestFrmLoc;
692
+ scope = await findFrame(selectors.nestFrmLoc, scope);
527
693
  frameFound = true;
528
694
  break;
529
695
  }
@@ -531,6 +697,7 @@ class StableBrowser {
531
697
  for (let i = 0; i < selectors.frameLocators.length; i++) {
532
698
  let frameLocator = selectors.frameLocators[i];
533
699
  if (frameLocator.css) {
700
+ fLocator = frameLocator.css;
534
701
  scope = scope.frameLocator(frameLocator.css);
535
702
  frameFound = true;
536
703
  break;
@@ -538,20 +705,55 @@ class StableBrowser {
538
705
  }
539
706
  }
540
707
  if (!frameFound && selectors.iframe_src) {
708
+ fLocator = selectors.iframe_src;
541
709
  scope = this.page.frame({ url: selectors.iframe_src });
542
710
  }
543
711
  if (!scope) {
544
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
545
- if (performance.now() - startTime > timeout) {
712
+ if (info && info.locatorLog) {
713
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
714
+ }
715
+ //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
716
+ if (Date.now() - startTime > timeout) {
717
+ info.failCause.iframeNotFound = true;
718
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
546
719
  throw new Error("unable to locate iframe " + selectors.iframe_src);
547
720
  }
548
721
  await new Promise((resolve) => setTimeout(resolve, 1000));
549
722
  }
550
723
  else {
724
+ if (info && info.locatorLog) {
725
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
726
+ }
551
727
  break;
552
728
  }
553
729
  }
554
730
  }
731
+ if (!scope) {
732
+ scope = this.page;
733
+ }
734
+ return scope;
735
+ }
736
+ async _getDocumentBody(selectors, timeout = 30000, info) {
737
+ let scope = await this._findFrameScope(selectors, timeout, info);
738
+ return scope.evaluate(() => {
739
+ var bodyContent = document.body.innerHTML;
740
+ return bodyContent;
741
+ });
742
+ }
743
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
744
+ if (!info) {
745
+ info = {};
746
+ info.failCause = {};
747
+ info.log = "";
748
+ info.locatorLog = new LocatorLog(selectors);
749
+ }
750
+ let highPriorityTimeout = 5000;
751
+ let visibleOnlyTimeout = 6000;
752
+ let startTime = Date.now();
753
+ let locatorsCount = 0;
754
+ let lazy_scroll = false;
755
+ //let arrayMode = Array.isArray(selectors);
756
+ let scope = await this._findFrameScope(selectors, timeout, info);
555
757
  let selectorsLocators = null;
556
758
  selectorsLocators = selectors.locators;
557
759
  // group selectors by priority
@@ -587,18 +789,13 @@ class StableBrowser {
587
789
  }
588
790
  // info.log += "scanning locators in priority 1" + "\n";
589
791
  let onlyPriority3 = selectorsLocators[0].priority === 3;
590
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
792
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
591
793
  if (result.foundElements.length === 0) {
592
794
  // info.log += "scanning locators in priority 2" + "\n";
593
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
594
- }
595
- if (result.foundElements.length === 0 && onlyPriority3) {
596
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
795
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
597
796
  }
598
- else {
599
- if (result.foundElements.length === 0 && !highPriorityOnly) {
600
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
601
- }
797
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
798
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
602
799
  }
603
800
  let foundElements = result.foundElements;
604
801
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -638,24 +835,43 @@ class StableBrowser {
638
835
  return maxCountElement.locator;
639
836
  }
640
837
  }
641
- if (performance.now() - startTime > timeout) {
838
+ if (Date.now() - startTime > timeout) {
642
839
  break;
643
840
  }
644
- if (performance.now() - startTime > highPriorityTimeout) {
645
- info.log += "high priority timeout, will try all elements" + "\n";
841
+ if (Date.now() - startTime > highPriorityTimeout) {
842
+ //info.log += "high priority timeout, will try all elements" + "\n";
646
843
  highPriorityOnly = false;
844
+ if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
845
+ lazy_scroll = true;
846
+ await scrollPageToLoadLazyElements(this.page);
847
+ }
647
848
  }
648
- if (performance.now() - startTime > visibleOnlyTimeout) {
649
- info.log += "visible only timeout, will try all elements" + "\n";
849
+ if (Date.now() - startTime > visibleOnlyTimeout) {
850
+ //info.log += "visible only timeout, will try all elements" + "\n";
650
851
  visibleOnly = false;
651
852
  }
652
853
  await new Promise((resolve) => setTimeout(resolve, 1000));
854
+ // sheck of more of half of the timeout has passed
855
+ if (Date.now() - startTime > timeout / 2) {
856
+ highPriorityOnly = false;
857
+ visibleOnly = false;
858
+ }
653
859
  }
654
860
  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";
861
+ // if (info.locatorLog) {
862
+ // const lines = info.locatorLog.toString().split("\n");
863
+ // for (let line of lines) {
864
+ // this.logger.debug(line);
865
+ // }
866
+ // }
867
+ //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
868
+ info.failCause.locatorNotFound = true;
869
+ if (!info?.failCause?.lastError) {
870
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
871
+ }
656
872
  throw new Error("failed to locate first element no elements found, " + info.log);
657
873
  }
658
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
874
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
659
875
  let foundElements = [];
660
876
  const result = {
661
877
  foundElements: foundElements,
@@ -663,17 +879,20 @@ class StableBrowser {
663
879
  for (let i = 0; i < locatorsGroup.length; i++) {
664
880
  let foundLocators = [];
665
881
  try {
666
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
882
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
667
883
  }
668
884
  catch (e) {
669
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
670
- this.logger.debug(e);
885
+ // this call can fail it the browser is navigating
886
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
887
+ // this.logger.debug(e);
671
888
  foundLocators = [];
672
889
  try {
673
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
890
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
674
891
  }
675
892
  catch (e) {
676
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
893
+ if (logErrors) {
894
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
895
+ }
677
896
  }
678
897
  }
679
898
  if (foundLocators.length === 1) {
@@ -684,270 +903,350 @@ class StableBrowser {
684
903
  });
685
904
  result.locatorIndex = i;
686
905
  }
906
+ if (foundLocators.length > 1) {
907
+ // remove elements that consume the same space with 10 pixels tolerance
908
+ const boxes = [];
909
+ for (let j = 0; j < foundLocators.length; j++) {
910
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
911
+ }
912
+ for (let j = 0; j < boxes.length; j++) {
913
+ for (let k = 0; k < boxes.length; k++) {
914
+ if (j === k) {
915
+ continue;
916
+ }
917
+ // check if x, y, width, height are the same with 10 pixels tolerance
918
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
919
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
920
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
921
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
922
+ // as the element is not unique, will remove it
923
+ boxes.splice(k, 1);
924
+ k--;
925
+ }
926
+ }
927
+ }
928
+ if (boxes.length === 1) {
929
+ result.foundElements.push({
930
+ locator: boxes[0].locator.first(),
931
+ box: boxes[0].box,
932
+ unique: true,
933
+ });
934
+ result.locatorIndex = i;
935
+ }
936
+ else if (logErrors) {
937
+ info.failCause.foundMultiple = true;
938
+ if (info.locatorLog) {
939
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
940
+ }
941
+ }
942
+ }
687
943
  }
688
944
  return result;
689
945
  }
690
- async click(selectors, _params, options = {}, world = null) {
691
- this._validateSelectors(selectors);
946
+ async simpleClick(elementDescription, _params, options = {}, world = null) {
947
+ const state = {
948
+ locate: false,
949
+ scroll: false,
950
+ highlight: false,
951
+ _params,
952
+ options,
953
+ world,
954
+ type: Types.CLICK,
955
+ text: "Click element",
956
+ operation: "simpleClick",
957
+ log: "***** click on " + elementDescription + " *****\n",
958
+ };
959
+ _preCommand(state, this);
692
960
  const startTime = Date.now();
693
- if (options && options.context) {
694
- selectors.locators[0].text = options.context;
961
+ let timeout = 30000;
962
+ if (options && options.timeout) {
963
+ timeout = options.timeout;
695
964
  }
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));
965
+ while (true) {
707
966
  try {
708
- await this._highlightElements(element);
709
- await element.click();
710
- await new Promise((resolve) => setTimeout(resolve, 1000));
967
+ const result = await locate_element(this.context, elementDescription, "click");
968
+ if (result?.elementNumber >= 0) {
969
+ const selectors = {
970
+ frame: result?.frame,
971
+ locators: [
972
+ {
973
+ css: result?.css,
974
+ },
975
+ ],
976
+ };
977
+ await this.click(selectors, _params, options, world);
978
+ return;
979
+ }
711
980
  }
712
981
  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));
982
+ if (performance.now() - startTime > timeout) {
983
+ // throw e;
984
+ try {
985
+ await _commandError(state, "timeout looking for " + elementDescription, this);
986
+ }
987
+ finally {
988
+ await _commandFinally(state, this);
989
+ }
990
+ }
718
991
  }
719
- await this.waitForPageLoad();
720
- return info;
992
+ await new Promise((resolve) => setTimeout(resolve, 3000));
993
+ }
994
+ }
995
+ async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
996
+ const state = {
997
+ locate: false,
998
+ scroll: false,
999
+ highlight: false,
1000
+ _params,
1001
+ options,
1002
+ world,
1003
+ type: Types.FILL,
1004
+ text: "Fill element",
1005
+ operation: "simpleClickType",
1006
+ log: "***** click type on " + elementDescription + " *****\n",
1007
+ };
1008
+ _preCommand(state, this);
1009
+ const startTime = Date.now();
1010
+ let timeout = 30000;
1011
+ if (options && options.timeout) {
1012
+ timeout = options.timeout;
1013
+ }
1014
+ while (true) {
1015
+ try {
1016
+ const result = await locate_element(this.context, elementDescription, "fill", value);
1017
+ if (result?.elementNumber >= 0) {
1018
+ const selectors = {
1019
+ frame: result?.frame,
1020
+ locators: [
1021
+ {
1022
+ css: result?.css,
1023
+ },
1024
+ ],
1025
+ };
1026
+ await this.clickType(selectors, value, false, _params, options, world);
1027
+ return;
1028
+ }
1029
+ }
1030
+ catch (e) {
1031
+ if (performance.now() - startTime > timeout) {
1032
+ // throw e;
1033
+ try {
1034
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1035
+ }
1036
+ finally {
1037
+ await _commandFinally(state, this);
1038
+ }
1039
+ }
1040
+ }
1041
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1042
+ }
1043
+ }
1044
+ async click(selectors, _params, options = {}, world = null) {
1045
+ const state = {
1046
+ selectors,
1047
+ _params,
1048
+ options,
1049
+ world,
1050
+ text: "Click element",
1051
+ _text: "Click on " + selectors.element_name,
1052
+ type: Types.CLICK,
1053
+ operation: "click",
1054
+ log: "***** click on " + selectors.element_name + " *****\n",
1055
+ };
1056
+ try {
1057
+ await _preCommand(state, this);
1058
+ await performAction("click", state.element, options, this, state, _params);
1059
+ if (!this.fastMode) {
1060
+ await this.waitForPageLoad();
1061
+ }
1062
+ return state.info;
721
1063
  }
722
1064
  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;
1065
+ await _commandError(state, e, this);
729
1066
  }
730
1067
  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
- });
1068
+ await _commandFinally(state, this);
1069
+ }
1070
+ }
1071
+ async waitForElement(selectors, _params, options = {}, world = null) {
1072
+ const timeout = this._getFindElementTimeout(options);
1073
+ const state = {
1074
+ selectors,
1075
+ _params,
1076
+ options,
1077
+ world,
1078
+ text: "Wait for element",
1079
+ _text: "Wait for " + selectors.element_name,
1080
+ type: Types.WAIT_ELEMENT,
1081
+ operation: "waitForElement",
1082
+ log: "***** wait for " + selectors.element_name + " *****\n",
1083
+ };
1084
+ let found = false;
1085
+ try {
1086
+ await _preCommand(state, this);
1087
+ // if (state.options && state.options.context) {
1088
+ // state.selectors.locators[0].text = state.options.context;
1089
+ // }
1090
+ await state.element.waitFor({ timeout: timeout });
1091
+ found = true;
1092
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1093
+ }
1094
+ catch (e) {
1095
+ console.error("Error on waitForElement", e);
1096
+ // await _commandError(state, e, this);
1097
+ }
1098
+ finally {
1099
+ await _commandFinally(state, this);
751
1100
  }
1101
+ return found;
752
1102
  }
753
1103
  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;
1104
+ const state = {
1105
+ selectors,
1106
+ _params,
1107
+ options,
1108
+ world,
1109
+ type: checked ? Types.CHECK : Types.UNCHECK,
1110
+ text: checked ? `Check element` : `Uncheck element`,
1111
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
1112
+ operation: "setCheck",
1113
+ log: "***** check " + selectors.element_name + " *****\n",
1114
+ };
764
1115
  try {
765
- let element = await this._locate(selectors, info, _params);
766
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1116
+ await _preCommand(state, this);
1117
+ state.info.checked = checked;
1118
+ // let element = await this._locate(selectors, info, _params);
1119
+ // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
767
1120
  try {
768
- await this._highlightElements(element);
769
- await element.setChecked(checked);
1121
+ // if (world && world.screenshot && !world.screenshotPath) {
1122
+ // console.log(`Highlighting while running from recorder`);
1123
+ await this._highlightElements(state.element);
1124
+ await state.element.setChecked(checked, { timeout: 2000 });
770
1125
  await new Promise((resolve) => setTimeout(resolve, 1000));
1126
+ // await this._unHighlightElements(element);
1127
+ // }
1128
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1129
+ // await this._unHighlightElements(element);
771
1130
  }
772
1131
  catch (e) {
773
1132
  if (e.message && e.message.includes("did not change its state")) {
774
1133
  this.logger.info("element did not change its state, ignoring...");
775
1134
  }
776
1135
  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
1136
  await new Promise((resolve) => setTimeout(resolve, 1000));
1137
+ //await this.closeUnexpectedPopups();
1138
+ state.info.log += "setCheck failed, will try again" + "\n";
1139
+ state.element_found = false;
1140
+ try {
1141
+ state.element = await this._locate(selectors, state.info, _params, 100);
1142
+ state.element_found = true;
1143
+ // check the check state
1144
+ }
1145
+ catch (error) {
1146
+ // element dismissed
1147
+ }
1148
+ if (state.element_found) {
1149
+ const isChecked = await state.element.isChecked();
1150
+ if (isChecked !== checked) {
1151
+ // perform click
1152
+ await state.element.click({ timeout: 2000, force: true });
1153
+ }
1154
+ else {
1155
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1156
+ }
1157
+ }
782
1158
  }
783
1159
  }
784
1160
  await this.waitForPageLoad();
785
- return info;
1161
+ return state.info;
786
1162
  }
787
1163
  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;
1164
+ await _commandError(state, e, this);
794
1165
  }
795
1166
  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
- });
1167
+ await _commandFinally(state, this);
816
1168
  }
817
1169
  }
818
1170
  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;
1171
+ const state = {
1172
+ selectors,
1173
+ _params,
1174
+ options,
1175
+ world,
1176
+ type: Types.HOVER,
1177
+ text: `Hover element`,
1178
+ _text: `Hover on ${selectors.element_name}`,
1179
+ operation: "hover",
1180
+ log: "***** hover " + selectors.element_name + " *****\n",
1181
+ };
828
1182
  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
- }
1183
+ await _preCommand(state, this);
1184
+ await performAction("hover", state.element, options, this, state, _params);
1185
+ await _screenshot(state, this);
843
1186
  await this.waitForPageLoad();
844
- return info;
1187
+ return state.info;
845
1188
  }
846
1189
  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;
1190
+ await _commandError(state, e, this);
853
1191
  }
854
1192
  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
- });
1193
+ await _commandFinally(state, this);
875
1194
  }
876
1195
  }
877
1196
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
878
- this._validateSelectors(selectors);
879
1197
  if (!values) {
880
1198
  throw new Error("values is null");
881
1199
  }
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;
1200
+ const state = {
1201
+ selectors,
1202
+ _params,
1203
+ options,
1204
+ world,
1205
+ value: values.toString(),
1206
+ type: Types.SELECT,
1207
+ text: `Select option: ${values}`,
1208
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1209
+ operation: "selectOption",
1210
+ log: "***** select option " + selectors.element_name + " *****\n",
1211
+ };
890
1212
  try {
891
- let element = await this._locate(selectors, info, _params);
892
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1213
+ await _preCommand(state, this);
893
1214
  try {
894
- await this._highlightElements(element);
895
- await element.selectOption(values);
1215
+ await state.element.selectOption(values);
896
1216
  }
897
1217
  catch (e) {
898
1218
  //await this.closeUnexpectedPopups();
899
- info.log += "selectOption failed, will try force" + "\n";
900
- await element.selectOption(values, { timeout: 10000, force: true });
1219
+ state.info.log += "selectOption failed, will try force" + "\n";
1220
+ await state.element.selectOption(values, { timeout: 10000, force: true });
901
1221
  }
902
1222
  await this.waitForPageLoad();
903
- return info;
1223
+ return state.info;
904
1224
  }
905
1225
  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;
1226
+ await _commandError(state, e, this);
913
1227
  }
914
1228
  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
- });
1229
+ await _commandFinally(state, this);
936
1230
  }
937
1231
  }
938
1232
  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;
1233
+ const state = {
1234
+ value: _value,
1235
+ _params,
1236
+ options,
1237
+ world,
1238
+ locate: false,
1239
+ scroll: false,
1240
+ highlight: false,
1241
+ type: Types.TYPE_PRESS,
1242
+ text: `Type value: ${_value}`,
1243
+ _text: `Type value: ${_value}`,
1244
+ operation: "type",
1245
+ log: "",
1246
+ };
948
1247
  try {
949
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
950
- const valueSegment = _value.split("&&");
1248
+ await _preCommand(state, this);
1249
+ const valueSegment = state.value.split("&&");
951
1250
  for (let i = 0; i < valueSegment.length; i++) {
952
1251
  if (i > 0) {
953
1252
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -967,134 +1266,77 @@ class StableBrowser {
967
1266
  await this.page.keyboard.type(value);
968
1267
  }
969
1268
  }
970
- return info;
1269
+ return state.info;
971
1270
  }
972
1271
  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;
1272
+ await _commandError(state, e, this);
980
1273
  }
981
1274
  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
- });
1275
+ await _commandFinally(state, this);
1002
1276
  }
1003
1277
  }
1004
1278
  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;
1279
+ const state = {
1280
+ selectors,
1281
+ _params,
1282
+ value,
1283
+ options,
1284
+ world,
1285
+ type: Types.SET_INPUT,
1286
+ text: `Set input value`,
1287
+ operation: "setInputValue",
1288
+ log: "***** set input value " + selectors.element_name + " *****\n",
1289
+ };
1017
1290
  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);
1291
+ await _preCommand(state, this);
1292
+ let value = await this._replaceWithLocalData(state.value, this);
1023
1293
  try {
1024
- await element.evaluateHandle((el, value) => {
1294
+ await state.element.evaluateHandle((el, value) => {
1025
1295
  el.value = value;
1026
1296
  }, value);
1027
1297
  }
1028
1298
  catch (error) {
1029
1299
  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) => {
1300
+ await _screenshot(state, this);
1301
+ Object.assign(error, { info: state.info });
1302
+ await state.element.evaluateHandle((el, value) => {
1034
1303
  el.value = value;
1035
1304
  });
1036
1305
  }
1037
1306
  }
1038
1307
  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;
1308
+ await _commandError(state, e, this);
1045
1309
  }
1046
1310
  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
- });
1311
+ await _commandFinally(state, this);
1068
1312
  }
1069
1313
  }
1070
1314
  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;
1315
+ const state = {
1316
+ selectors,
1317
+ _params,
1318
+ value: await this._replaceWithLocalData(value, this),
1319
+ options,
1320
+ world,
1321
+ type: Types.SET_DATE_TIME,
1322
+ text: `Set date time value: ${value}`,
1323
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1324
+ operation: "setDateTime",
1325
+ log: "***** set date time value " + selectors.element_name + " *****\n",
1326
+ throwError: false,
1327
+ };
1081
1328
  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);
1329
+ await _preCommand(state, this);
1088
1330
  try {
1089
- await element.click();
1331
+ await performAction("click", state.element, options, this, state, _params);
1090
1332
  await new Promise((resolve) => setTimeout(resolve, 500));
1091
1333
  if (format) {
1092
- value = dayjs(value).format(format);
1093
- await element.fill(value);
1334
+ state.value = dayjs(state.value).format(format);
1335
+ await state.element.fill(state.value);
1094
1336
  }
1095
1337
  else {
1096
- const dateTimeValue = await getDateTimeValue({ value, element });
1097
- await element.evaluateHandle((el, dateTimeValue) => {
1338
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1339
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1098
1340
  el.value = ""; // clear input
1099
1341
  el.value = dateTimeValue;
1100
1342
  }, dateTimeValue);
@@ -1107,20 +1349,19 @@ class StableBrowser {
1107
1349
  }
1108
1350
  catch (err) {
1109
1351
  //await this.closeUnexpectedPopups();
1110
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1352
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1111
1353
  this.logger.info("Trying again");
1112
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1113
- info.screenshotPath = screenshotPath;
1114
- Object.assign(err, { info: info });
1354
+ await _screenshot(state, this);
1355
+ Object.assign(err, { info: state.info });
1115
1356
  await element.click();
1116
1357
  await new Promise((resolve) => setTimeout(resolve, 500));
1117
1358
  if (format) {
1118
- value = dayjs(value).format(format);
1119
- await element.fill(value);
1359
+ state.value = dayjs(state.value).format(format);
1360
+ await state.element.fill(state.value);
1120
1361
  }
1121
1362
  else {
1122
- const dateTimeValue = await getDateTimeValue({ value, element });
1123
- await element.evaluateHandle((el, dateTimeValue) => {
1363
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1364
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1124
1365
  el.value = ""; // clear input
1125
1366
  el.value = dateTimeValue;
1126
1367
  }, dateTimeValue);
@@ -1133,84 +1374,63 @@ class StableBrowser {
1133
1374
  }
1134
1375
  }
1135
1376
  catch (e) {
1136
- error = e;
1137
- throw e;
1377
+ await _commandError(state, e, this);
1138
1378
  }
1139
1379
  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
- });
1380
+ await _commandFinally(state, this);
1161
1381
  }
1162
1382
  }
1163
1383
  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;
1384
+ _value = unEscapeString(_value);
1173
1385
  const newValue = await this._replaceWithLocalData(_value, world);
1386
+ const state = {
1387
+ selectors,
1388
+ _params,
1389
+ value: newValue,
1390
+ originalValue: _value,
1391
+ options,
1392
+ world,
1393
+ type: Types.FILL,
1394
+ text: `Click type input with value: ${_value}`,
1395
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1396
+ operation: "clickType",
1397
+ log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1398
+ };
1399
+ if (!options) {
1400
+ options = {};
1401
+ }
1174
1402
  if (newValue !== _value) {
1175
1403
  //this.logger.info(_value + "=" + newValue);
1176
1404
  _value = newValue;
1177
1405
  }
1178
- info.value = _value;
1179
1406
  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) {
1407
+ await _preCommand(state, this);
1408
+ state.info.value = _value;
1409
+ if (!options.press) {
1186
1410
  try {
1187
- let currentValue = await element.inputValue();
1411
+ let currentValue = await state.element.inputValue();
1188
1412
  if (currentValue) {
1189
- await element.fill("");
1413
+ await state.element.fill("");
1190
1414
  }
1191
1415
  }
1192
1416
  catch (e) {
1193
1417
  this.logger.info("unable to clear input value");
1194
1418
  }
1195
1419
  }
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
- }
1420
+ if (options.press) {
1421
+ options.timeout = 5000;
1422
+ await performAction("click", state.element, options, this, state, _params);
1203
1423
  }
1204
1424
  else {
1205
1425
  try {
1206
- await element.focus();
1426
+ await state.element.focus();
1207
1427
  }
1208
1428
  catch (e) {
1209
- await element.dispatchEvent("focus");
1429
+ await state.element.dispatchEvent("focus");
1210
1430
  }
1211
1431
  }
1212
1432
  await new Promise((resolve) => setTimeout(resolve, 500));
1213
- const valueSegment = _value.split("&&");
1433
+ const valueSegment = state.value.split("&&");
1214
1434
  for (let i = 0; i < valueSegment.length; i++) {
1215
1435
  if (i > 0) {
1216
1436
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1230,13 +1450,21 @@ class StableBrowser {
1230
1450
  await new Promise((resolve) => setTimeout(resolve, 500));
1231
1451
  }
1232
1452
  }
1453
+ //if (!this.fastMode) {
1454
+ await _screenshot(state, this);
1455
+ //}
1233
1456
  if (enter === true) {
1234
1457
  await new Promise((resolve) => setTimeout(resolve, 2000));
1235
1458
  await this.page.keyboard.press("Enter");
1236
1459
  await this.waitForPageLoad();
1237
1460
  }
1238
1461
  else if (enter === false) {
1239
- await element.dispatchEvent("change");
1462
+ try {
1463
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1464
+ }
1465
+ catch (e) {
1466
+ // ignore
1467
+ }
1240
1468
  //await this.page.keyboard.press("Tab");
1241
1469
  }
1242
1470
  else {
@@ -1245,111 +1473,95 @@ class StableBrowser {
1245
1473
  await this.waitForPageLoad();
1246
1474
  }
1247
1475
  }
1248
- return info;
1476
+ return state.info;
1249
1477
  }
1250
1478
  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;
1479
+ await _commandError(state, e, this);
1258
1480
  }
1259
1481
  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
- });
1482
+ await _commandFinally(state, this);
1281
1483
  }
1282
1484
  }
1283
1485
  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;
1486
+ const state = {
1487
+ selectors,
1488
+ _params,
1489
+ value: unEscapeString(value),
1490
+ options,
1491
+ world,
1492
+ type: Types.FILL,
1493
+ text: `Fill input with value: ${value}`,
1494
+ operation: "fill",
1495
+ log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
1496
+ };
1294
1497
  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");
1498
+ await _preCommand(state, this);
1499
+ await state.element.fill(value);
1500
+ await state.element.dispatchEvent("change");
1300
1501
  if (enter) {
1301
1502
  await new Promise((resolve) => setTimeout(resolve, 2000));
1302
1503
  await this.page.keyboard.press("Enter");
1303
1504
  }
1304
1505
  await this.waitForPageLoad();
1305
- return info;
1506
+ return state.info;
1306
1507
  }
1307
1508
  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;
1509
+ await _commandError(state, e, this);
1315
1510
  }
1316
1511
  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
- });
1512
+ await _commandFinally(state, this);
1513
+ }
1514
+ }
1515
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1516
+ const state = {
1517
+ selectors,
1518
+ _params,
1519
+ files,
1520
+ value: '"' + files.join('", "') + '"',
1521
+ options,
1522
+ world,
1523
+ type: Types.SET_INPUT_FILES,
1524
+ text: `Set input files`,
1525
+ _text: `Set input files on ${selectors.element_name}`,
1526
+ operation: "setInputFiles",
1527
+ log: "***** set input files " + selectors.element_name + " *****\n",
1528
+ };
1529
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1530
+ try {
1531
+ await _preCommand(state, this);
1532
+ for (let i = 0; i < files.length; i++) {
1533
+ const file = files[i];
1534
+ const filePath = path.join(uploadsFolder, file);
1535
+ if (!fs.existsSync(filePath)) {
1536
+ throw new Error(`File not found: ${filePath}`);
1537
+ }
1538
+ state.files[i] = filePath;
1539
+ }
1540
+ await state.element.setInputFiles(files);
1541
+ return state.info;
1542
+ }
1543
+ catch (e) {
1544
+ await _commandError(state, e, this);
1545
+ }
1546
+ finally {
1547
+ await _commandFinally(state, this);
1338
1548
  }
1339
1549
  }
1340
1550
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1341
1551
  return await this._getText(selectors, 0, _params, options, info, world);
1342
1552
  }
1343
1553
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1344
- this._validateSelectors(selectors);
1554
+ const timeout = this._getFindElementTimeout(options);
1555
+ _validateSelectors(selectors);
1345
1556
  let screenshotId = null;
1346
1557
  let screenshotPath = null;
1347
1558
  if (!info.log) {
1348
1559
  info.log = "";
1560
+ info.locatorLog = new LocatorLog(selectors);
1349
1561
  }
1350
1562
  info.operation = "getText";
1351
1563
  info.selectors = selectors;
1352
- let element = await this._locate(selectors, info, _params);
1564
+ let element = await this._locate(selectors, info, _params, timeout);
1353
1565
  if (climb > 0) {
1354
1566
  const climbArray = [];
1355
1567
  for (let i = 0; i < climb; i++) {
@@ -1368,6 +1580,18 @@ class StableBrowser {
1368
1580
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1369
1581
  try {
1370
1582
  await this._highlightElements(element);
1583
+ // if (world && world.screenshot && !world.screenshotPath) {
1584
+ // // console.log(`Highlighting for get text while running from recorder`);
1585
+ // this._highlightElements(element)
1586
+ // .then(async () => {
1587
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1588
+ // this._unhighlightElements(element).then(
1589
+ // () => {}
1590
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1591
+ // );
1592
+ // })
1593
+ // .catch(e);
1594
+ // }
1371
1595
  const elementText = await element.innerText();
1372
1596
  return {
1373
1597
  text: elementText,
@@ -1379,188 +1603,219 @@ class StableBrowser {
1379
1603
  }
1380
1604
  catch (e) {
1381
1605
  //await this.closeUnexpectedPopups();
1382
- this.logger.info("no innerText will use textContent");
1606
+ this.logger.info("no innerText, will use textContent");
1383
1607
  const elementText = await element.textContent();
1384
1608
  return { text: elementText, screenshotId, screenshotPath, value: value };
1385
1609
  }
1386
1610
  }
1387
1611
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1388
- var _a;
1389
- this._validateSelectors(selectors);
1390
1612
  if (!pattern) {
1391
1613
  throw new Error("pattern is null");
1392
1614
  }
1393
1615
  if (!text) {
1394
1616
  throw new Error("text is null");
1395
1617
  }
1618
+ const state = {
1619
+ selectors,
1620
+ _params,
1621
+ pattern,
1622
+ value: pattern,
1623
+ options,
1624
+ world,
1625
+ locate: false,
1626
+ scroll: false,
1627
+ screenshot: false,
1628
+ highlight: false,
1629
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1630
+ text: `Verify element contains pattern: ${pattern}`,
1631
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1632
+ operation: "containsPattern",
1633
+ log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1634
+ };
1396
1635
  const newValue = await this._replaceWithLocalData(text, world);
1397
1636
  if (newValue !== text) {
1398
1637
  this.logger.info(text + "=" + newValue);
1399
1638
  text = newValue;
1400
1639
  }
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
1640
  let foundObj = null;
1413
1641
  try {
1414
- foundObj = await this._getText(selectors, 0, _params, options, info, world);
1642
+ await _preCommand(state, this);
1643
+ state.info.pattern = pattern;
1644
+ foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
1415
1645
  if (foundObj && foundObj.element) {
1416
- await this.scrollIfNeeded(foundObj.element, info);
1646
+ await this.scrollIfNeeded(foundObj.element, state.info);
1417
1647
  }
1418
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1648
+ await _screenshot(state, this);
1419
1649
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1420
1650
  pattern = pattern.replace("{text}", escapedText);
1421
1651
  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;
1652
+ if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
1653
+ state.info.foundText = foundObj?.text;
1424
1654
  throw new Error("element doesn't contain text " + text);
1425
1655
  }
1426
- return info;
1656
+ return state.info;
1427
1657
  }
1428
1658
  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;
1659
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1660
+ await _commandError(state, e, this);
1437
1661
  }
1438
1662
  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
- });
1663
+ await _commandFinally(state, this);
1460
1664
  }
1461
1665
  }
1462
1666
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1463
- var _a, _b, _c;
1464
- this._validateSelectors(selectors);
1667
+ const timeout = this._getFindElementTimeout(options);
1668
+ const startTime = Date.now();
1669
+ const state = {
1670
+ selectors,
1671
+ _params,
1672
+ value: text,
1673
+ options,
1674
+ world,
1675
+ locate: false,
1676
+ scroll: false,
1677
+ screenshot: false,
1678
+ highlight: false,
1679
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1680
+ text: `Verify element contains text: ${text}`,
1681
+ operation: "containsText",
1682
+ log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
1683
+ };
1465
1684
  if (!text) {
1466
1685
  throw new Error("text is null");
1467
1686
  }
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;
1687
+ text = unEscapeString(text);
1476
1688
  const newValue = await this._replaceWithLocalData(text, world);
1477
1689
  if (newValue !== text) {
1478
1690
  this.logger.info(text + "=" + newValue);
1479
1691
  text = newValue;
1480
1692
  }
1481
- info.value = text;
1482
1693
  let foundObj = null;
1483
1694
  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;
1695
+ while (Date.now() - startTime < timeout) {
1696
+ try {
1697
+ await _preCommand(state, this);
1698
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1699
+ if (foundObj && foundObj.element) {
1700
+ await this.scrollIfNeeded(foundObj.element, state.info);
1701
+ }
1702
+ await _screenshot(state, this);
1703
+ const dateAlternatives = findDateAlternatives(text);
1704
+ const numberAlternatives = findNumberAlternatives(text);
1705
+ if (dateAlternatives.date) {
1706
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1707
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1708
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1709
+ return state.info;
1710
+ }
1711
+ }
1496
1712
  }
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;
1713
+ else if (numberAlternatives.number) {
1714
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1715
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1716
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1717
+ return state.info;
1718
+ }
1719
+ }
1720
+ }
1721
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1722
+ return state.info;
1505
1723
  }
1506
1724
  }
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);
1725
+ catch (e) {
1726
+ // Log error but continue retrying until timeout is reached
1727
+ this.logger.warn("Retrying containsText due to: " + e.message);
1728
+ }
1729
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1513
1730
  }
1514
- return info;
1731
+ state.info.foundText = foundObj?.text;
1732
+ state.info.value = foundObj?.value;
1733
+ throw new Error("element doesn't contain text " + text);
1515
1734
  }
1516
1735
  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;
1736
+ await _commandError(state, e, this);
1523
1737
  throw e;
1524
1738
  }
1525
1739
  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
- });
1740
+ await _commandFinally(state, this);
1547
1741
  }
1548
1742
  }
1549
- _getDataFile(world = null) {
1550
- let dataFile = null;
1551
- if (world && world.reportFolder) {
1552
- dataFile = path.join(world.reportFolder, "data.json");
1743
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1744
+ const timeout = this._getFindElementTimeout(options);
1745
+ const startTime = Date.now();
1746
+ const state = {
1747
+ _params,
1748
+ value: referanceSnapshot,
1749
+ options,
1750
+ world,
1751
+ locate: false,
1752
+ scroll: false,
1753
+ screenshot: true,
1754
+ highlight: false,
1755
+ type: Types.SNAPSHOT_VALIDATION,
1756
+ text: `verify snapshot: ${referanceSnapshot}`,
1757
+ operation: "snapshotValidation",
1758
+ log: "***** verify snapshot *****\n",
1759
+ };
1760
+ if (!referanceSnapshot) {
1761
+ throw new Error("referanceSnapshot is null");
1553
1762
  }
1554
- else if (this.reportFolder) {
1555
- dataFile = path.join(this.reportFolder, "data.json");
1763
+ let text = null;
1764
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1765
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1556
1766
  }
1557
- else if (this.context && this.context.reportFolder) {
1558
- dataFile = path.join(this.context.reportFolder, "data.json");
1767
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1768
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1769
+ }
1770
+ else if (referanceSnapshot.startsWith("yaml:")) {
1771
+ text = referanceSnapshot.substring(5);
1559
1772
  }
1560
1773
  else {
1561
- dataFile = "data.json";
1774
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1775
+ }
1776
+ state.text = text;
1777
+ const newValue = await this._replaceWithLocalData(text, world);
1778
+ await _preCommand(state, this);
1779
+ let foundObj = null;
1780
+ try {
1781
+ let matchResult = null;
1782
+ while (Date.now() - startTime < timeout) {
1783
+ try {
1784
+ let scope = null;
1785
+ if (!frameSelectors) {
1786
+ scope = this.page;
1787
+ }
1788
+ else {
1789
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1790
+ }
1791
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1792
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1793
+ if (matchResult.errorLine !== -1) {
1794
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1795
+ }
1796
+ // highlight and screenshot
1797
+ try {
1798
+ await await highlightSnapshot(newValue, scope);
1799
+ await _screenshot(state, this);
1800
+ }
1801
+ catch (e) { }
1802
+ return state.info;
1803
+ }
1804
+ catch (e) {
1805
+ // Log error but continue retrying until timeout is reached
1806
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1807
+ }
1808
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1809
+ }
1810
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1811
+ }
1812
+ catch (e) {
1813
+ await _commandError(state, e, this);
1814
+ throw e;
1815
+ }
1816
+ finally {
1817
+ await _commandFinally(state, this);
1562
1818
  }
1563
- return dataFile;
1564
1819
  }
1565
1820
  async waitForUserInput(message, world = null) {
1566
1821
  if (!message) {
@@ -1590,12 +1845,21 @@ class StableBrowser {
1590
1845
  return;
1591
1846
  }
1592
1847
  // if data file exists, load it
1593
- const dataFile = this._getDataFile(world);
1848
+ const dataFile = _getDataFile(world, this.context, this);
1594
1849
  let data = this.getTestData(world);
1595
1850
  // merge the testData with the existing data
1596
1851
  Object.assign(data, testData);
1597
1852
  // save the data to the file
1598
- fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1853
+ fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1854
+ }
1855
+ overwriteTestData(testData, world = null) {
1856
+ if (!testData) {
1857
+ return;
1858
+ }
1859
+ // if data file exists, load it
1860
+ const dataFile = _getDataFile(world, this.context, this);
1861
+ // save the data to the file
1862
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1599
1863
  }
1600
1864
  _getDataFilePath(fileName) {
1601
1865
  let dataFile = path.join(this.project_path, "data", fileName);
@@ -1693,7 +1957,7 @@ class StableBrowser {
1693
1957
  }
1694
1958
  }
1695
1959
  getTestData(world = null) {
1696
- const dataFile = this._getDataFile(world);
1960
+ const dataFile = _getDataFile(world, this.context, this);
1697
1961
  let data = {};
1698
1962
  if (fs.existsSync(dataFile)) {
1699
1963
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1725,11 +1989,9 @@ class StableBrowser {
1725
1989
  if (!fs.existsSync(world.screenshotPath)) {
1726
1990
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1727
1991
  }
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");
1992
+ // to make sure the path doesn't start with -
1993
+ const uuidStr = "id_" + randomUUID();
1994
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1733
1995
  try {
1734
1996
  await this.takeScreenshot(screenshotPath);
1735
1997
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1739,15 +2001,15 @@ class StableBrowser {
1739
2001
  // this.logger.info("unable to save screenshot " + screenshotPath);
1740
2002
  // }
1741
2003
  // });
2004
+ result.screenshotId = uuidStr;
2005
+ result.screenshotPath = screenshotPath;
2006
+ if (info && info.box) {
2007
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
2008
+ }
1742
2009
  }
1743
2010
  catch (e) {
1744
2011
  this.logger.info("unable to take screenshot, ignored");
1745
2012
  }
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
2013
  }
1752
2014
  else if (options && options.screenshot) {
1753
2015
  result.screenshotPath = options.screenshotPath;
@@ -1772,7 +2034,6 @@ class StableBrowser {
1772
2034
  }
1773
2035
  async takeScreenshot(screenshotPath) {
1774
2036
  const playContext = this.context.playContext;
1775
- const client = await playContext.newCDPSession(this.page);
1776
2037
  // Using CDP to capture the screenshot
1777
2038
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
1778
2039
  document.body.scrollWidth,
@@ -1782,164 +2043,433 @@ class StableBrowser {
1782
2043
  document.body.clientWidth,
1783
2044
  document.documentElement.clientWidth,
1784
2045
  ])));
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();
2046
+ let screenshotBuffer = null;
2047
+ // if (focusedElement) {
2048
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2049
+ // await this._unhighlightElements(focusedElement);
2050
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2051
+ // console.log(`Unhighlighted previous element`);
2052
+ // }
2053
+ // if (focusedElement) {
2054
+ // await this._highlightElements(focusedElement);
2055
+ // }
2056
+ if (this.context.browserName === "chromium") {
2057
+ const client = await playContext.newCDPSession(this.page);
2058
+ const { data } = await client.send("Page.captureScreenshot", {
2059
+ format: "png",
2060
+ // clip: {
2061
+ // x: 0,
2062
+ // y: 0,
2063
+ // width: viewportWidth,
2064
+ // height: viewportHeight,
2065
+ // scale: 1,
2066
+ // },
2067
+ });
2068
+ await client.detach();
2069
+ if (!screenshotPath) {
2070
+ return data;
2071
+ }
2072
+ screenshotBuffer = Buffer.from(data, "base64");
2073
+ }
2074
+ else {
2075
+ screenshotBuffer = await this.page.screenshot();
2076
+ }
2077
+ // if (focusedElement) {
2078
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2079
+ // await this._unhighlightElements(focusedElement);
2080
+ // }
2081
+ let image = await Jimp.read(screenshotBuffer);
2082
+ // Get the image dimensions
2083
+ const { width, height } = image.bitmap;
2084
+ const resizeRatio = viewportWidth / width;
2085
+ // Resize the image to fit within the viewport dimensions without enlarging
2086
+ if (width > viewportWidth) {
2087
+ image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
2088
+ await image.write(screenshotPath);
2089
+ }
2090
+ else {
2091
+ fs.writeFileSync(screenshotPath, screenshotBuffer);
2092
+ }
2093
+ return screenshotBuffer;
1820
2094
  }
1821
2095
  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;
2096
+ const state = {
2097
+ selectors,
2098
+ _params,
2099
+ options,
2100
+ world,
2101
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2102
+ text: `Verify element exists in page`,
2103
+ operation: "verifyElementExistInPage",
2104
+ log: "***** verify element " + selectors.element_name + " exists in page *****\n",
2105
+ };
1827
2106
  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
2107
  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;
2108
+ await _preCommand(state, this);
2109
+ await expect(state.element).toHaveCount(1, { timeout: 10000 });
2110
+ return state.info;
1841
2111
  }
1842
2112
  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;
2113
+ await _commandError(state, e, this);
1850
2114
  }
1851
2115
  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
- });
2116
+ await _commandFinally(state, this);
1872
2117
  }
1873
2118
  }
1874
2119
  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;
2120
+ const state = {
2121
+ selectors,
2122
+ _params,
2123
+ attribute,
2124
+ variable,
2125
+ options,
2126
+ world,
2127
+ type: Types.EXTRACT,
2128
+ text: `Extract attribute from element`,
2129
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
2130
+ operation: "extractAttribute",
2131
+ log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
2132
+ allowDisabled: true,
2133
+ };
1880
2134
  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
2135
  try {
1886
- const element = await this._locate(selectors, info, _params);
1887
- await this._highlightElements(element);
1888
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2136
+ await _preCommand(state, this);
1889
2137
  switch (attribute) {
1890
2138
  case "inner_text":
1891
- info.value = await element.innerText();
2139
+ state.value = await state.element.innerText();
1892
2140
  break;
1893
2141
  case "href":
1894
- info.value = await element.getAttribute("href");
2142
+ state.value = await state.element.getAttribute("href");
1895
2143
  break;
1896
2144
  case "value":
1897
- info.value = await element.inputValue();
2145
+ state.value = await state.element.inputValue();
2146
+ break;
2147
+ case "text":
2148
+ state.value = await state.element.textContent();
1898
2149
  break;
1899
2150
  default:
1900
- info.value = await element.getAttribute(attribute);
2151
+ state.value = await state.element.getAttribute(attribute);
1901
2152
  break;
1902
2153
  }
1903
- this[variable] = info.value;
1904
- if (world) {
1905
- world[variable] = info.value;
2154
+ if (options !== null) {
2155
+ if (options.regex && options.regex !== "") {
2156
+ // Construct a regex pattern from the provided string
2157
+ const regex = options.regex.slice(1, -1);
2158
+ const regexPattern = new RegExp(regex, "g");
2159
+ const matches = state.value.match(regexPattern);
2160
+ if (matches) {
2161
+ let newValue = "";
2162
+ for (const match of matches) {
2163
+ newValue += match;
2164
+ }
2165
+ state.value = newValue;
2166
+ }
2167
+ }
2168
+ if (options.trimSpaces && options.trimSpaces === true) {
2169
+ state.value = state.value.trim();
2170
+ }
1906
2171
  }
1907
- this.setTestData({ [variable]: info.value }, world);
1908
- this.logger.info("set test data: " + variable + "=" + info.value);
1909
- return info;
2172
+ state.info.value = state.value;
2173
+ this.setTestData({ [variable]: state.value }, world);
2174
+ this.logger.info("set test data: " + variable + "=" + state.value);
2175
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2176
+ return state.info;
1910
2177
  }
1911
2178
  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;
2179
+ await _commandError(state, e, this);
1919
2180
  }
1920
2181
  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,
2182
+ await _commandFinally(state, this);
2183
+ }
2184
+ }
2185
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2186
+ const state = {
2187
+ selectors,
2188
+ _params,
2189
+ property,
2190
+ variable,
2191
+ options,
2192
+ world,
2193
+ type: Types.EXTRACT_PROPERTY,
2194
+ text: `Extract property from element`,
2195
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2196
+ operation: "extractProperty",
2197
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2198
+ allowDisabled: true,
2199
+ };
2200
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2201
+ try {
2202
+ await _preCommand(state, this);
2203
+ switch (property) {
2204
+ case "inner_text":
2205
+ state.value = await state.element.innerText();
2206
+ break;
2207
+ case "href":
2208
+ state.value = await state.element.getAttribute("href");
2209
+ break;
2210
+ case "value":
2211
+ state.value = await state.element.inputValue();
2212
+ break;
2213
+ case "text":
2214
+ state.value = await state.element.textContent();
2215
+ break;
2216
+ default:
2217
+ if (property.startsWith("dataset.")) {
2218
+ const dataAttribute = property.substring(8);
2219
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
1935
2220
  }
1936
- : {
1937
- status: "PASSED",
1938
- startTime,
1939
- endTime,
1940
- },
1941
- info: info,
1942
- });
2221
+ else {
2222
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2223
+ }
2224
+ }
2225
+ if (options !== null) {
2226
+ if (options.regex && options.regex !== "") {
2227
+ // Construct a regex pattern from the provided string
2228
+ const regex = options.regex.slice(1, -1);
2229
+ const regexPattern = new RegExp(regex, "g");
2230
+ const matches = state.value.match(regexPattern);
2231
+ if (matches) {
2232
+ let newValue = "";
2233
+ for (const match of matches) {
2234
+ newValue += match;
2235
+ }
2236
+ state.value = newValue;
2237
+ }
2238
+ }
2239
+ if (options.trimSpaces && options.trimSpaces === true) {
2240
+ state.value = state.value.trim();
2241
+ }
2242
+ }
2243
+ state.info.value = state.value;
2244
+ this.setTestData({ [variable]: state.value }, world);
2245
+ this.logger.info("set test data: " + variable + "=" + state.value);
2246
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2247
+ return state.info;
2248
+ }
2249
+ catch (e) {
2250
+ await _commandError(state, e, this);
2251
+ }
2252
+ finally {
2253
+ await _commandFinally(state, this);
2254
+ }
2255
+ }
2256
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2257
+ const state = {
2258
+ selectors,
2259
+ _params,
2260
+ attribute,
2261
+ value,
2262
+ options,
2263
+ world,
2264
+ type: Types.VERIFY_ATTRIBUTE,
2265
+ highlight: true,
2266
+ screenshot: true,
2267
+ text: `Verify element attribute`,
2268
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2269
+ operation: "verifyAttribute",
2270
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2271
+ allowDisabled: true,
2272
+ };
2273
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2274
+ let val;
2275
+ let expectedValue;
2276
+ try {
2277
+ await _preCommand(state, this);
2278
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2279
+ state.info.expectedValue = expectedValue;
2280
+ switch (attribute) {
2281
+ case "innerText":
2282
+ val = String(await state.element.innerText());
2283
+ break;
2284
+ case "text":
2285
+ val = String(await state.element.textContent());
2286
+ break;
2287
+ case "value":
2288
+ val = String(await state.element.inputValue());
2289
+ break;
2290
+ case "checked":
2291
+ val = String(await state.element.isChecked());
2292
+ break;
2293
+ case "disabled":
2294
+ val = String(await state.element.isDisabled());
2295
+ break;
2296
+ case "readOnly":
2297
+ const isEditable = await state.element.isEditable();
2298
+ val = String(!isEditable);
2299
+ break;
2300
+ default:
2301
+ val = String(await state.element.getAttribute(attribute));
2302
+ break;
2303
+ }
2304
+ state.info.value = val;
2305
+ let regex;
2306
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2307
+ const patternBody = expectedValue.slice(1, -1);
2308
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2309
+ regex = new RegExp(processedPattern, "gs");
2310
+ state.info.regex = true;
2311
+ }
2312
+ else {
2313
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2314
+ regex = new RegExp(escapedPattern, "g");
2315
+ }
2316
+ if (attribute === "innerText") {
2317
+ if (state.info.regex) {
2318
+ if (!regex.test(val)) {
2319
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2320
+ state.info.failCause.assertionFailed = true;
2321
+ state.info.failCause.lastError = errorMessage;
2322
+ throw new Error(errorMessage);
2323
+ }
2324
+ }
2325
+ else {
2326
+ const valLines = val.split("\n");
2327
+ const expectedLines = expectedValue.split("\n");
2328
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2329
+ if (!isPart) {
2330
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2331
+ state.info.failCause.assertionFailed = true;
2332
+ state.info.failCause.lastError = errorMessage;
2333
+ throw new Error(errorMessage);
2334
+ }
2335
+ }
2336
+ }
2337
+ else {
2338
+ if (!val.match(regex)) {
2339
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2340
+ state.info.failCause.assertionFailed = true;
2341
+ state.info.failCause.lastError = errorMessage;
2342
+ throw new Error(errorMessage);
2343
+ }
2344
+ }
2345
+ return state.info;
2346
+ }
2347
+ catch (e) {
2348
+ await _commandError(state, e, this);
2349
+ }
2350
+ finally {
2351
+ await _commandFinally(state, this);
2352
+ }
2353
+ }
2354
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2355
+ const state = {
2356
+ selectors,
2357
+ _params,
2358
+ property,
2359
+ value,
2360
+ options,
2361
+ world,
2362
+ type: Types.VERIFY_PROPERTY,
2363
+ highlight: true,
2364
+ screenshot: true,
2365
+ text: `Verify element property`,
2366
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2367
+ operation: "verifyProperty",
2368
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2369
+ allowDisabled: true,
2370
+ };
2371
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2372
+ let val;
2373
+ let expectedValue;
2374
+ try {
2375
+ await _preCommand(state, this);
2376
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2377
+ state.info.expectedValue = expectedValue;
2378
+ switch (property) {
2379
+ case "innerText":
2380
+ val = String(await state.element.innerText());
2381
+ break;
2382
+ case "text":
2383
+ val = String(await state.element.textContent());
2384
+ break;
2385
+ case "value":
2386
+ val = String(await state.element.inputValue());
2387
+ break;
2388
+ case "checked":
2389
+ val = String(await state.element.isChecked());
2390
+ break;
2391
+ case "disabled":
2392
+ val = String(await state.element.isDisabled());
2393
+ break;
2394
+ case "readOnly":
2395
+ const isEditable = await state.element.isEditable();
2396
+ val = String(!isEditable);
2397
+ break;
2398
+ case "innerHTML":
2399
+ val = String(await state.element.innerHTML());
2400
+ break;
2401
+ case "outerHTML":
2402
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2403
+ break;
2404
+ default:
2405
+ if (property.startsWith("dataset.")) {
2406
+ const dataAttribute = property.substring(8);
2407
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2408
+ }
2409
+ else {
2410
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2411
+ }
2412
+ }
2413
+ // Helper function to remove all style="" attributes
2414
+ const removeStyleAttributes = (htmlString) => {
2415
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, '');
2416
+ };
2417
+ // Remove style attributes for innerHTML and outerHTML properties
2418
+ if (property === "innerHTML" || property === "outerHTML") {
2419
+ val = removeStyleAttributes(val);
2420
+ expectedValue = removeStyleAttributes(expectedValue);
2421
+ }
2422
+ state.info.value = val;
2423
+ let regex;
2424
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2425
+ const patternBody = expectedValue.slice(1, -1);
2426
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2427
+ regex = new RegExp(processedPattern, "gs");
2428
+ state.info.regex = true;
2429
+ }
2430
+ else {
2431
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2432
+ regex = new RegExp(escapedPattern, "g");
2433
+ }
2434
+ if (property === "innerText") {
2435
+ if (state.info.regex) {
2436
+ if (!regex.test(val)) {
2437
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2438
+ state.info.failCause.assertionFailed = true;
2439
+ state.info.failCause.lastError = errorMessage;
2440
+ throw new Error(errorMessage);
2441
+ }
2442
+ }
2443
+ else {
2444
+ // Fix: Replace escaped newlines with actual newlines before splitting
2445
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, '\n');
2446
+ const valLines = val.split("\n");
2447
+ const expectedLines = normalizedExpectedValue.split("\n");
2448
+ // Check if all expected lines are present in the actual lines
2449
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2450
+ if (!isPart) {
2451
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2452
+ state.info.failCause.assertionFailed = true;
2453
+ state.info.failCause.lastError = errorMessage;
2454
+ throw new Error(errorMessage);
2455
+ }
2456
+ }
2457
+ }
2458
+ else {
2459
+ if (!val.match(regex)) {
2460
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2461
+ state.info.failCause.assertionFailed = true;
2462
+ state.info.failCause.lastError = errorMessage;
2463
+ throw new Error(errorMessage);
2464
+ }
2465
+ }
2466
+ return state.info;
2467
+ }
2468
+ catch (e) {
2469
+ await _commandError(state, e, this);
2470
+ }
2471
+ finally {
2472
+ await _commandFinally(state, this);
1943
2473
  }
1944
2474
  }
1945
2475
  async extractEmailData(emailAddress, options, world) {
@@ -1960,7 +2490,7 @@ class StableBrowser {
1960
2490
  if (options && options.timeout) {
1961
2491
  timeout = options.timeout;
1962
2492
  }
1963
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2493
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1964
2494
  const request = {
1965
2495
  method: "POST",
1966
2496
  url: serviceUrl,
@@ -2016,7 +2546,8 @@ class StableBrowser {
2016
2546
  catch (e) {
2017
2547
  errorCount++;
2018
2548
  if (errorCount > 3) {
2019
- throw e;
2549
+ // throw e;
2550
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
2020
2551
  }
2021
2552
  // ignore
2022
2553
  }
@@ -2030,27 +2561,32 @@ class StableBrowser {
2030
2561
  async _highlightElements(scope, css) {
2031
2562
  try {
2032
2563
  if (!scope) {
2564
+ // console.log(`Scope is not defined`);
2033
2565
  return;
2034
2566
  }
2035
2567
  if (!css) {
2036
2568
  scope
2037
2569
  .evaluate((node) => {
2038
2570
  if (node && node.style) {
2039
- let originalBorder = node.style.border;
2040
- node.style.border = "2px solid red";
2571
+ let originalOutline = node.style.outline;
2572
+ // console.log(`Original outline was: ${originalOutline}`);
2573
+ // node.__previousOutline = originalOutline;
2574
+ node.style.outline = "2px solid red";
2575
+ // console.log(`New outline is: ${node.style.outline}`);
2041
2576
  if (window) {
2042
2577
  window.addEventListener("beforeunload", function (e) {
2043
- node.style.border = originalBorder;
2578
+ node.style.outline = originalOutline;
2044
2579
  });
2045
2580
  }
2046
2581
  setTimeout(function () {
2047
- node.style.border = originalBorder;
2582
+ node.style.outline = originalOutline;
2048
2583
  }, 2000);
2049
2584
  }
2050
2585
  })
2051
2586
  .then(() => { })
2052
2587
  .catch((e) => {
2053
2588
  // ignore
2589
+ // console.error(`Could not highlight node : ${e}`);
2054
2590
  });
2055
2591
  }
2056
2592
  else {
@@ -2066,17 +2602,18 @@ class StableBrowser {
2066
2602
  if (!element.style) {
2067
2603
  return;
2068
2604
  }
2069
- var originalBorder = element.style.border;
2605
+ let originalOutline = element.style.outline;
2606
+ element.__previousOutline = originalOutline;
2070
2607
  // Set the new border to be red and 2px solid
2071
- element.style.border = "2px solid red";
2608
+ element.style.outline = "2px solid red";
2072
2609
  if (window) {
2073
2610
  window.addEventListener("beforeunload", function (e) {
2074
- element.style.border = originalBorder;
2611
+ element.style.outline = originalOutline;
2075
2612
  });
2076
2613
  }
2077
2614
  // Set a timeout to revert to the original border after 2 seconds
2078
2615
  setTimeout(function () {
2079
- element.style.border = originalBorder;
2616
+ element.style.outline = originalOutline;
2080
2617
  }, 2000);
2081
2618
  }
2082
2619
  return;
@@ -2084,6 +2621,7 @@ class StableBrowser {
2084
2621
  .then(() => { })
2085
2622
  .catch((e) => {
2086
2623
  // ignore
2624
+ // console.error(`Could not highlight css: ${e}`);
2087
2625
  });
2088
2626
  }
2089
2627
  }
@@ -2091,173 +2629,563 @@ class StableBrowser {
2091
2629
  console.debug(error);
2092
2630
  }
2093
2631
  }
2632
+ _matcher(text) {
2633
+ if (!text) {
2634
+ return { matcher: "contains", queryText: "" };
2635
+ }
2636
+ if (text.length < 2) {
2637
+ return { matcher: "contains", queryText: text };
2638
+ }
2639
+ const split = text.split(":");
2640
+ const matcher = split[0].toLowerCase();
2641
+ const queryText = split.slice(1).join(":").trim();
2642
+ return { matcher, queryText };
2643
+ }
2644
+ _getDomain(url) {
2645
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2646
+ return "";
2647
+ }
2648
+ let hostnameFragments = url.split("/")[2].split(".");
2649
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2650
+ return hostnameFragments.join("-").split(":").join("-");
2651
+ }
2652
+ let n = hostnameFragments.length;
2653
+ let fragments = [...hostnameFragments];
2654
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2655
+ hostnameFragments.pop();
2656
+ n = hostnameFragments.length;
2657
+ }
2658
+ if (n == 0) {
2659
+ if (fragments[0] === "www")
2660
+ fragments = fragments.slice(1);
2661
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2662
+ }
2663
+ if (hostnameFragments[0] === "www")
2664
+ hostnameFragments = hostnameFragments.slice(1);
2665
+ return hostnameFragments.join(".");
2666
+ }
2667
+ /**
2668
+ * Verify the page path matches the given path.
2669
+ * @param {string} pathPart - The path to verify.
2670
+ * @param {object} options - Options for verification.
2671
+ * @param {object} world - The world context.
2672
+ * @returns {Promise<object>} - The state info after verification.
2673
+ */
2094
2674
  async verifyPagePath(pathPart, options = {}, world = null) {
2095
- const startTime = Date.now();
2096
2675
  let error = null;
2097
2676
  let screenshotId = null;
2098
2677
  let screenshotPath = null;
2099
2678
  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;
2679
+ const info = {};
2680
+ info.log = "***** verify page path " + pathPart + " *****\n";
2681
+ info.operation = "verifyPagePath";
2682
+ const newValue = await this._replaceWithLocalData(pathPart, world);
2683
+ if (newValue !== pathPart) {
2684
+ this.logger.info(pathPart + "=" + newValue);
2685
+ pathPart = newValue;
2686
+ }
2687
+ info.pathPart = pathPart;
2688
+ const { matcher, queryText } = this._matcher(pathPart);
2689
+ const state = {
2690
+ text_search: queryText,
2691
+ options,
2692
+ world,
2693
+ locate: false,
2694
+ scroll: false,
2695
+ highlight: false,
2696
+ type: Types.VERIFY_PAGE_PATH,
2697
+ text: `Verify the page url is ${queryText}`,
2698
+ _text: `Verify the page url is ${queryText}`,
2699
+ operation: "verifyPagePath",
2700
+ log: "***** verify page url is " + queryText + " *****\n",
2701
+ };
2702
+ try {
2703
+ await _preCommand(state, this);
2704
+ state.info.text = queryText;
2705
+ for (let i = 0; i < 30; i++) {
2706
+ const url = await this.page.url();
2707
+ switch (matcher) {
2708
+ case "exact":
2709
+ if (url !== queryText) {
2710
+ if (i === 29) {
2711
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2712
+ }
2713
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2714
+ continue;
2715
+ }
2716
+ break;
2717
+ case "contains":
2718
+ if (!url.includes(queryText)) {
2719
+ if (i === 29) {
2720
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2721
+ }
2722
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2723
+ continue;
2724
+ }
2725
+ break;
2726
+ case "starts-with":
2727
+ {
2728
+ const domain = this._getDomain(url);
2729
+ if (domain.length > 0 && domain !== queryText) {
2730
+ if (i === 29) {
2731
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2732
+ }
2733
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2734
+ continue;
2735
+ }
2736
+ }
2737
+ break;
2738
+ case "ends-with":
2739
+ {
2740
+ const urlObj = new URL(url);
2741
+ let route = "/";
2742
+ if (urlObj.pathname !== "/") {
2743
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2744
+ }
2745
+ else {
2746
+ route = "/";
2747
+ }
2748
+ if (route !== queryText) {
2749
+ if (i === 29) {
2750
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2751
+ }
2752
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2753
+ continue;
2754
+ }
2755
+ }
2756
+ break;
2757
+ case "regex":
2758
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2759
+ if (!regex.test(url)) {
2760
+ if (i === 29) {
2761
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2762
+ }
2763
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2764
+ continue;
2765
+ }
2766
+ break;
2767
+ default:
2768
+ console.log("Unknown matching type, defaulting to contains matching");
2769
+ if (!url.includes(pathPart)) {
2770
+ if (i === 29) {
2771
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2772
+ }
2773
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2774
+ continue;
2775
+ }
2776
+ }
2777
+ await _screenshot(state, this);
2778
+ return state.info;
2779
+ }
2780
+ }
2781
+ catch (e) {
2782
+ state.info.failCause.lastError = e.message;
2783
+ state.info.failCause.assertionFailed = true;
2784
+ await _commandError(state, e, this);
2785
+ }
2786
+ finally {
2787
+ await _commandFinally(state, this);
2788
+ }
2789
+ }
2790
+ /**
2791
+ * Verify the page title matches the given title.
2792
+ * @param {string} title - The title to verify.
2793
+ * @param {object} options - Options for verification.
2794
+ * @param {object} world - The world context.
2795
+ * @returns {Promise<object>} - The state info after verification.
2796
+ */
2797
+ async verifyPageTitle(title, options = {}, world = null) {
2798
+ let error = null;
2799
+ let screenshotId = null;
2800
+ let screenshotPath = null;
2801
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2802
+ const newValue = await this._replaceWithLocalData(title, world);
2803
+ if (newValue !== title) {
2804
+ this.logger.info(title + "=" + newValue);
2805
+ title = newValue;
2806
+ }
2807
+ const { matcher, queryText } = this._matcher(title);
2808
+ const state = {
2809
+ text_search: queryText,
2810
+ options,
2811
+ world,
2812
+ locate: false,
2813
+ scroll: false,
2814
+ highlight: false,
2815
+ type: Types.VERIFY_PAGE_TITLE,
2816
+ text: `Verify the page title is ${queryText}`,
2817
+ _text: `Verify the page title is ${queryText}`,
2818
+ operation: "verifyPageTitle",
2819
+ log: "***** verify page title is " + queryText + " *****\n",
2820
+ };
2821
+ try {
2822
+ await _preCommand(state, this);
2823
+ state.info.text = queryText;
2824
+ for (let i = 0; i < 30; i++) {
2825
+ const foundTitle = await this.page.title();
2826
+ switch (matcher) {
2827
+ case "exact":
2828
+ if (foundTitle !== queryText) {
2829
+ if (i === 29) {
2830
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2831
+ }
2832
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2833
+ continue;
2834
+ }
2835
+ break;
2836
+ case "contains":
2837
+ if (!foundTitle.includes(queryText)) {
2838
+ if (i === 29) {
2839
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2840
+ }
2841
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2842
+ continue;
2843
+ }
2844
+ break;
2845
+ case "starts-with":
2846
+ if (!foundTitle.startsWith(queryText)) {
2847
+ if (i === 29) {
2848
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2849
+ }
2850
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2851
+ continue;
2852
+ }
2853
+ break;
2854
+ case "ends-with":
2855
+ if (!foundTitle.endsWith(queryText)) {
2856
+ if (i === 29) {
2857
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2858
+ }
2859
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2860
+ continue;
2861
+ }
2862
+ break;
2863
+ case "regex":
2864
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2865
+ if (!regex.test(foundTitle)) {
2866
+ if (i === 29) {
2867
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2868
+ }
2869
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2870
+ continue;
2871
+ }
2872
+ break;
2873
+ default:
2874
+ console.log("Unknown matching type, defaulting to contains matching");
2875
+ if (!foundTitle.includes(title)) {
2876
+ if (i === 29) {
2877
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2878
+ }
2879
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2880
+ continue;
2881
+ }
2882
+ }
2883
+ await _screenshot(state, this);
2884
+ return state.info;
2885
+ }
2886
+ }
2887
+ catch (e) {
2888
+ state.info.failCause.lastError = e.message;
2889
+ state.info.failCause.assertionFailed = true;
2890
+ await _commandError(state, e, this);
2891
+ }
2892
+ finally {
2893
+ await _commandFinally(state, this);
2894
+ }
2895
+ }
2896
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2897
+ const frames = this.page.frames();
2898
+ let results = [];
2899
+ // let ignoreCase = false;
2900
+ for (let i = 0; i < frames.length; i++) {
2901
+ if (dateAlternatives.date) {
2902
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
2903
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2904
+ result.frame = frames[i];
2905
+ results.push(result);
2906
+ }
2907
+ }
2908
+ else if (numberAlternatives.number) {
2909
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2910
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2911
+ result.frame = frames[i];
2912
+ results.push(result);
2913
+ }
2914
+ }
2915
+ else {
2916
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2917
+ result.frame = frames[i];
2918
+ results.push(result);
2919
+ }
2920
+ }
2921
+ state.info.results = results;
2922
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2923
+ return resultWithElementsFound;
2924
+ }
2925
+ async verifyTextExistInPage(text, options = {}, world = null) {
2926
+ text = unEscapeString(text);
2927
+ const state = {
2928
+ text_search: text,
2929
+ options,
2930
+ world,
2931
+ locate: false,
2932
+ scroll: false,
2933
+ highlight: false,
2934
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2935
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2936
+ _text: `Verify the text '${text}' exists in page`,
2937
+ operation: "verifyTextExistInPage",
2938
+ log: "***** verify text " + text + " exists in page *****\n",
2939
+ };
2940
+ if (testForRegex(text)) {
2941
+ text = text.replace(/\\"/g, '"');
2942
+ }
2943
+ const timeout = this._getFindElementTimeout(options);
2944
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2945
+ const newValue = await this._replaceWithLocalData(text, world);
2946
+ if (newValue !== text) {
2947
+ this.logger.info(text + "=" + newValue);
2948
+ text = newValue;
2107
2949
  }
2108
- info.pathPart = pathPart;
2950
+ let dateAlternatives = findDateAlternatives(text);
2951
+ let numberAlternatives = findNumberAlternatives(text);
2109
2952
  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}`);
2953
+ await _preCommand(state, this);
2954
+ state.info.text = text;
2955
+ while (true) {
2956
+ let resultWithElementsFound = {
2957
+ length: 0,
2958
+ };
2959
+ try {
2960
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2961
+ }
2962
+ catch (error) {
2963
+ // ignore
2964
+ }
2965
+ if (resultWithElementsFound.length === 0) {
2966
+ if (Date.now() - state.startTime > timeout) {
2967
+ throw new Error(`Text ${text} not found in page`);
2115
2968
  }
2116
2969
  await new Promise((resolve) => setTimeout(resolve, 1000));
2117
2970
  continue;
2118
2971
  }
2119
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2120
- return info;
2972
+ try {
2973
+ if (resultWithElementsFound[0].randomToken) {
2974
+ const frame = resultWithElementsFound[0].frame;
2975
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2976
+ await this._highlightElements(frame, dataAttribute);
2977
+ const element = await frame.locator(dataAttribute).first();
2978
+ if (element) {
2979
+ await this.scrollIfNeeded(element, state.info);
2980
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2981
+ }
2982
+ }
2983
+ await _screenshot(state, this);
2984
+ return state.info;
2985
+ }
2986
+ catch (error) {
2987
+ console.error(error);
2988
+ }
2121
2989
  }
2122
2990
  }
2123
2991
  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;
2992
+ await _commandError(state, e, this);
2131
2993
  }
2132
2994
  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
- });
2995
+ await _commandFinally(state, this);
2152
2996
  }
2153
2997
  }
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;
2998
+ async waitForTextToDisappear(text, options = {}, world = null) {
2999
+ text = unEscapeString(text);
3000
+ const state = {
3001
+ text_search: text,
3002
+ options,
3003
+ world,
3004
+ locate: false,
3005
+ scroll: false,
3006
+ highlight: false,
3007
+ type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
3008
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
3009
+ _text: `Verify the text '${text}' does not exist in page`,
3010
+ operation: "verifyTextNotExistInPage",
3011
+ log: "***** verify text " + text + " does not exist in page *****\n",
3012
+ };
3013
+ if (testForRegex(text)) {
3014
+ text = text.replace(/\\"/g, '"');
3015
+ }
3016
+ const timeout = this._getFindElementTimeout(options);
2160
3017
  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
3018
  const newValue = await this._replaceWithLocalData(text, world);
2165
3019
  if (newValue !== text) {
2166
3020
  this.logger.info(text + "=" + newValue);
2167
3021
  text = newValue;
2168
3022
  }
2169
- info.text = text;
2170
3023
  let dateAlternatives = findDateAlternatives(text);
2171
3024
  let numberAlternatives = findNumberAlternatives(text);
2172
3025
  try {
3026
+ await _preCommand(state, this);
3027
+ state.info.text = text;
3028
+ let resultWithElementsFound = {
3029
+ length: null, // initial cannot be 0
3030
+ };
2173
3031
  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
- }
3032
+ try {
3033
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3034
+ }
3035
+ catch (error) {
3036
+ // ignore
2196
3037
  }
2197
- info.results = results;
2198
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2199
3038
  if (resultWithElementsFound.length === 0) {
2200
- if (Date.now() - startTime > timeout) {
2201
- throw new Error(`Text ${text} not found in page`);
3039
+ await _screenshot(state, this);
3040
+ return state.info;
3041
+ }
3042
+ if (Date.now() - state.startTime > timeout) {
3043
+ throw new Error(`Text ${text} found in page`);
3044
+ }
3045
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3046
+ }
3047
+ }
3048
+ catch (e) {
3049
+ await _commandError(state, e, this);
3050
+ }
3051
+ finally {
3052
+ await _commandFinally(state, this);
3053
+ }
3054
+ }
3055
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
3056
+ textAnchor = unEscapeString(textAnchor);
3057
+ textToVerify = unEscapeString(textToVerify);
3058
+ const state = {
3059
+ text_search: textToVerify,
3060
+ options,
3061
+ world,
3062
+ locate: false,
3063
+ scroll: false,
3064
+ highlight: false,
3065
+ type: Types.VERIFY_TEXT_WITH_RELATION,
3066
+ text: `Verify text with relation to another text`,
3067
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
3068
+ operation: "verify_text_with_relation",
3069
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3070
+ };
3071
+ const timeout = this._getFindElementTimeout(options);
3072
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3073
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
3074
+ if (newValue !== textAnchor) {
3075
+ this.logger.info(textAnchor + "=" + newValue);
3076
+ textAnchor = newValue;
3077
+ }
3078
+ newValue = await this._replaceWithLocalData(textToVerify, world);
3079
+ if (newValue !== textToVerify) {
3080
+ this.logger.info(textToVerify + "=" + newValue);
3081
+ textToVerify = newValue;
3082
+ }
3083
+ let dateAlternatives = findDateAlternatives(textToVerify);
3084
+ let numberAlternatives = findNumberAlternatives(textToVerify);
3085
+ let foundAncore = false;
3086
+ try {
3087
+ await _preCommand(state, this);
3088
+ state.info.text = textToVerify;
3089
+ let resultWithElementsFound = {
3090
+ length: 0,
3091
+ };
3092
+ while (true) {
3093
+ try {
3094
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
3095
+ }
3096
+ catch (error) {
3097
+ // ignore
3098
+ }
3099
+ if (resultWithElementsFound.length === 0) {
3100
+ if (Date.now() - state.startTime > timeout) {
3101
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2202
3102
  }
2203
3103
  await new Promise((resolve) => setTimeout(resolve, 1000));
2204
3104
  continue;
2205
3105
  }
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");
3106
+ try {
3107
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
3108
+ foundAncore = true;
3109
+ const result = resultWithElementsFound[i];
3110
+ const token = result.randomToken;
3111
+ const frame = result.frame;
3112
+ let css = `[data-blinq-id-${token}]`;
3113
+ const climbArray1 = [];
3114
+ for (let i = 0; i < climb; i++) {
3115
+ climbArray1.push("..");
3116
+ }
3117
+ let climbXpath = "xpath=" + climbArray1.join("/");
3118
+ css = css + " >> " + climbXpath;
3119
+ const count = await frame.locator(css).count();
3120
+ for (let j = 0; j < count; j++) {
3121
+ const continer = await frame.locator(css).nth(j);
3122
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
3123
+ if (result.elementCount > 0) {
3124
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
3125
+ await this._highlightElements(frame, dataAttribute);
3126
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
3127
+ // if (world && world.screenshot && !world.screenshotPath) {
3128
+ // console.log(`Highlighting for vtrt while running from recorder`);
3129
+ // this._highlightElements(frame, dataAttribute)
3130
+ // .then(async () => {
3131
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
3132
+ // this._unhighlightElements(frame, dataAttribute).then(
3133
+ // () => {}
3134
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
3135
+ // );
3136
+ // })
3137
+ // .catch(e);
3138
+ // }
3139
+ //await this._highlightElements(frame, cssAnchor);
3140
+ const element = await frame.locator(dataAttribute).first();
3141
+ // await new Promise((resolve) => setTimeout(resolve, 100));
3142
+ // await this._unhighlightElements(frame, dataAttribute);
3143
+ if (element) {
3144
+ await this.scrollIfNeeded(element, state.info);
3145
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3146
+ }
3147
+ await _screenshot(state, this);
3148
+ return state.info;
3149
+ }
3150
+ }
2214
3151
  }
2215
3152
  }
2216
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2217
- return info;
3153
+ catch (error) {
3154
+ console.error(error);
3155
+ }
2218
3156
  }
2219
3157
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2220
3158
  }
2221
3159
  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;
3160
+ await _commandError(state, e, this);
2229
3161
  }
2230
3162
  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
- });
3163
+ await _commandFinally(state, this);
2250
3164
  }
2251
3165
  }
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";
3166
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
3167
+ const frames = this.page.frames();
3168
+ let results = [];
3169
+ let ignoreCase = false;
3170
+ for (let i = 0; i < frames.length; i++) {
3171
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
3172
+ result.frame = frames[i];
3173
+ const climbArray = [];
3174
+ for (let i = 0; i < climb; i++) {
3175
+ climbArray.push("..");
3176
+ }
3177
+ let climbXpath = "xpath=" + climbArray.join("/");
3178
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
3179
+ const count = await frames[i].locator(newLocator).count();
3180
+ if (count > 0) {
3181
+ result.elementCount = count;
3182
+ result.locator = newLocator;
3183
+ results.push(result);
3184
+ }
2259
3185
  }
2260
- return serviceUrl;
3186
+ // state.info.results = results;
3187
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3188
+ return resultWithElementsFound;
2261
3189
  }
2262
3190
  async visualVerification(text, options = {}, world = null) {
2263
3191
  const startTime = Date.now();
@@ -2273,14 +3201,17 @@ class StableBrowser {
2273
3201
  throw new Error("TOKEN is not set");
2274
3202
  }
2275
3203
  try {
2276
- let serviceUrl = this._getServerUrl();
3204
+ let serviceUrl = _getServerUrl();
2277
3205
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2278
3206
  info.screenshotPath = screenshotPath;
2279
3207
  const screenshot = await this.takeScreenshot();
2280
- const request = {
2281
- method: "POST",
3208
+ let request = {
3209
+ method: "post",
3210
+ maxBodyLength: Infinity,
2282
3211
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2283
3212
  headers: {
3213
+ "x-bvt-project-id": path.basename(this.project_path),
3214
+ "x-source": "aaa",
2284
3215
  "Content-Type": "application/json",
2285
3216
  Authorization: `Bearer ${process.env.TOKEN}`,
2286
3217
  },
@@ -2289,7 +3220,7 @@ class StableBrowser {
2289
3220
  screenshot: screenshot,
2290
3221
  }),
2291
3222
  };
2292
- let result = await this.context.api.request(request);
3223
+ const result = await axios.request(request);
2293
3224
  if (result.data.status !== true) {
2294
3225
  throw new Error("Visual validation failed");
2295
3226
  }
@@ -2309,20 +3240,22 @@ class StableBrowser {
2309
3240
  info.screenshotPath = screenshotPath;
2310
3241
  Object.assign(e, { info: info });
2311
3242
  error = e;
2312
- throw e;
3243
+ // throw e;
3244
+ await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
2313
3245
  }
2314
3246
  finally {
2315
3247
  const endTime = Date.now();
2316
- this._reportToWorld(world, {
3248
+ _reportToWorld(world, {
2317
3249
  type: Types.VERIFY_VISUAL,
2318
3250
  text: "Visual verification",
3251
+ _text: "Visual verification of " + text,
2319
3252
  screenshotId,
2320
3253
  result: error
2321
3254
  ? {
2322
3255
  status: "FAILED",
2323
3256
  startTime,
2324
3257
  endTime,
2325
- message: error === null || error === void 0 ? void 0 : error.message,
3258
+ message: error?.message,
2326
3259
  }
2327
3260
  : {
2328
3261
  status: "PASSED",
@@ -2354,13 +3287,14 @@ class StableBrowser {
2354
3287
  this.logger.info("Table data verified");
2355
3288
  }
2356
3289
  async getTableData(selectors, _params = null, options = {}, world = null) {
2357
- this._validateSelectors(selectors);
3290
+ _validateSelectors(selectors);
2358
3291
  const startTime = Date.now();
2359
3292
  let error = null;
2360
3293
  let screenshotId = null;
2361
3294
  let screenshotPath = null;
2362
3295
  const info = {};
2363
3296
  info.log = "";
3297
+ info.locatorLog = new LocatorLog(selectors);
2364
3298
  info.operation = "getTableData";
2365
3299
  info.selectors = selectors;
2366
3300
  try {
@@ -2376,11 +3310,12 @@ class StableBrowser {
2376
3310
  info.screenshotPath = screenshotPath;
2377
3311
  Object.assign(e, { info: info });
2378
3312
  error = e;
2379
- throw e;
3313
+ // throw e;
3314
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2380
3315
  }
2381
3316
  finally {
2382
3317
  const endTime = Date.now();
2383
- this._reportToWorld(world, {
3318
+ _reportToWorld(world, {
2384
3319
  element_name: selectors.element_name,
2385
3320
  type: Types.GET_TABLE_DATA,
2386
3321
  text: "Get table data",
@@ -2390,7 +3325,7 @@ class StableBrowser {
2390
3325
  status: "FAILED",
2391
3326
  startTime,
2392
3327
  endTime,
2393
- message: error === null || error === void 0 ? void 0 : error.message,
3328
+ message: error?.message,
2394
3329
  }
2395
3330
  : {
2396
3331
  status: "PASSED",
@@ -2402,7 +3337,7 @@ class StableBrowser {
2402
3337
  }
2403
3338
  }
2404
3339
  async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
2405
- this._validateSelectors(selectors);
3340
+ _validateSelectors(selectors);
2406
3341
  if (!query) {
2407
3342
  throw new Error("query is null");
2408
3343
  }
@@ -2435,7 +3370,7 @@ class StableBrowser {
2435
3370
  info.operation = "analyzeTable";
2436
3371
  info.selectors = selectors;
2437
3372
  info.query = query;
2438
- query = this._fixUsingParams(query, _params);
3373
+ query = _fixUsingParams(query, _params);
2439
3374
  info.query_fixed = query;
2440
3375
  info.operator = operator;
2441
3376
  info.value = value;
@@ -2541,11 +3476,12 @@ class StableBrowser {
2541
3476
  info.screenshotPath = screenshotPath;
2542
3477
  Object.assign(e, { info: info });
2543
3478
  error = e;
2544
- throw e;
3479
+ // throw e;
3480
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2545
3481
  }
2546
3482
  finally {
2547
3483
  const endTime = Date.now();
2548
- this._reportToWorld(world, {
3484
+ _reportToWorld(world, {
2549
3485
  element_name: selectors.element_name,
2550
3486
  type: Types.ANALYZE_TABLE,
2551
3487
  text: "Analyze table",
@@ -2555,7 +3491,7 @@ class StableBrowser {
2555
3491
  status: "FAILED",
2556
3492
  startTime,
2557
3493
  endTime,
2558
- message: error === null || error === void 0 ? void 0 : error.message,
3494
+ message: error?.message,
2559
3495
  }
2560
3496
  : {
2561
3497
  status: "PASSED",
@@ -2566,28 +3502,51 @@ class StableBrowser {
2566
3502
  });
2567
3503
  }
2568
3504
  }
2569
- 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
- }
3505
+ /**
3506
+ * Explicit wait/sleep function that pauses execution for a specified duration
3507
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3508
+ * @param options - Optional configuration object
3509
+ * @param world - Optional world context
3510
+ * @returns Promise that resolves after the specified duration
3511
+ */
3512
+ async sleep(duration = 1000, options = {}, world = null) {
3513
+ const state = {
3514
+ duration,
3515
+ options,
3516
+ world,
3517
+ locate: false,
3518
+ scroll: false,
3519
+ screenshot: false,
3520
+ highlight: false,
3521
+ type: Types.SLEEP,
3522
+ text: `Sleep for ${duration} ms`,
3523
+ _text: `Sleep for ${duration} ms`,
3524
+ operation: "sleep",
3525
+ log: `***** Sleep for ${duration} ms *****\n`,
3526
+ };
3527
+ try {
3528
+ await _preCommand(state, this);
3529
+ if (duration < 0) {
3530
+ throw new Error("Sleep duration cannot be negative");
2585
3531
  }
3532
+ await new Promise((resolve) => setTimeout(resolve, duration));
3533
+ return state.info;
3534
+ }
3535
+ catch (e) {
3536
+ await _commandError(state, e, this);
3537
+ }
3538
+ finally {
3539
+ await _commandFinally(state, this);
3540
+ }
3541
+ }
3542
+ async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
3543
+ try {
3544
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
2586
3545
  }
2587
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2588
- return await decrypt(value, null, totpWait);
3546
+ catch (error) {
3547
+ this.logger.debug(error);
3548
+ throw error;
2589
3549
  }
2590
- return value;
2591
3550
  }
2592
3551
  _getLoadTimeout(options) {
2593
3552
  let timeout = 15000;
@@ -2599,6 +3558,37 @@ class StableBrowser {
2599
3558
  }
2600
3559
  return timeout;
2601
3560
  }
3561
+ _getFindElementTimeout(options) {
3562
+ if (options && options.timeout) {
3563
+ return options.timeout;
3564
+ }
3565
+ if (this.configuration.find_element_timeout) {
3566
+ return this.configuration.find_element_timeout;
3567
+ }
3568
+ return 30000;
3569
+ }
3570
+ async saveStoreState(path = null, world = null) {
3571
+ const storageState = await this.page.context().storageState();
3572
+ path = await this._replaceWithLocalData(path, this.world);
3573
+ //const testDataFile = _getDataFile(world, this.context, this);
3574
+ if (path) {
3575
+ // save { storageState: storageState } into the path
3576
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3577
+ }
3578
+ else {
3579
+ await this.setTestData({ storageState: storageState }, world);
3580
+ }
3581
+ }
3582
+ async restoreSaveState(path = null, world = null) {
3583
+ path = await this._replaceWithLocalData(path, this.world);
3584
+ await refreshBrowser(this, path, world);
3585
+ this.registerEventListeners(this.context);
3586
+ registerNetworkEvents(this.world, this, this.context, this.page);
3587
+ registerDownloadEvent(this.page, this.world, this.context);
3588
+ if (this.onRestoreSaveState) {
3589
+ this.onRestoreSaveState(path);
3590
+ }
3591
+ }
2602
3592
  async waitForPageLoad(options = {}, world = null) {
2603
3593
  let timeout = this._getLoadTimeout(options);
2604
3594
  const promiseArray = [];
@@ -2638,7 +3628,7 @@ class StableBrowser {
2638
3628
  await new Promise((resolve) => setTimeout(resolve, 2000));
2639
3629
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2640
3630
  const endTime = Date.now();
2641
- this._reportToWorld(world, {
3631
+ _reportToWorld(world, {
2642
3632
  type: Types.GET_PAGE_STATUS,
2643
3633
  text: "Wait for page load",
2644
3634
  screenshotId,
@@ -2647,7 +3637,7 @@ class StableBrowser {
2647
3637
  status: "FAILED",
2648
3638
  startTime,
2649
3639
  endTime,
2650
- message: error === null || error === void 0 ? void 0 : error.message,
3640
+ message: error?.message,
2651
3641
  }
2652
3642
  : {
2653
3643
  status: "PASSED",
@@ -2658,41 +3648,123 @@ class StableBrowser {
2658
3648
  }
2659
3649
  }
2660
3650
  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 = {};
3651
+ const state = {
3652
+ options,
3653
+ world,
3654
+ locate: false,
3655
+ scroll: false,
3656
+ highlight: false,
3657
+ type: Types.CLOSE_PAGE,
3658
+ text: `Close page`,
3659
+ _text: `Close the page`,
3660
+ operation: "closePage",
3661
+ log: "***** close page *****\n",
3662
+ throwError: false,
3663
+ };
2666
3664
  try {
3665
+ await _preCommand(state, this);
2667
3666
  await this.page.close();
2668
3667
  }
2669
3668
  catch (e) {
2670
3669
  console.log(".");
3670
+ await _commandError(state, e, this);
2671
3671
  }
2672
3672
  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,
3673
+ await _commandFinally(state, this);
3674
+ }
3675
+ }
3676
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3677
+ let operation = null;
3678
+ if (!options || !options.operation) {
3679
+ throw new Error("operation is not defined");
3680
+ }
3681
+ operation = options.operation;
3682
+ // validate operation is one of the supported operations
3683
+ if (operation != "click" && operation != "hover+click") {
3684
+ throw new Error("operation is not supported");
3685
+ }
3686
+ const state = {
3687
+ options,
3688
+ world,
3689
+ locate: false,
3690
+ scroll: false,
3691
+ highlight: false,
3692
+ type: Types.TABLE_OPERATION,
3693
+ text: `Table operation`,
3694
+ _text: `Table ${operation} operation`,
3695
+ operation: operation,
3696
+ log: "***** Table operation *****\n",
3697
+ };
3698
+ const timeout = this._getFindElementTimeout(options);
3699
+ try {
3700
+ await _preCommand(state, this);
3701
+ const start = Date.now();
3702
+ let cellArea = null;
3703
+ while (true) {
3704
+ try {
3705
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3706
+ if (cellArea) {
3707
+ break;
2686
3708
  }
2687
- : {
2688
- status: "PASSED",
2689
- startTime,
2690
- endTime,
2691
- },
2692
- info: info,
2693
- });
3709
+ }
3710
+ catch (e) {
3711
+ // ignore
3712
+ }
3713
+ if (Date.now() - start > timeout) {
3714
+ throw new Error(`Cell not found in table`);
3715
+ }
3716
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3717
+ }
3718
+ switch (operation) {
3719
+ case "click":
3720
+ if (!options.css) {
3721
+ // will click in the center of the cell
3722
+ let xOffset = 0;
3723
+ let yOffset = 0;
3724
+ if (options.xOffset) {
3725
+ xOffset = options.xOffset;
3726
+ }
3727
+ if (options.yOffset) {
3728
+ yOffset = options.yOffset;
3729
+ }
3730
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3731
+ }
3732
+ else {
3733
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3734
+ if (results.length === 0) {
3735
+ throw new Error(`Element not found in cell area`);
3736
+ }
3737
+ state.element = results[0];
3738
+ await performAction("click", state.element, options, this, state, _params);
3739
+ }
3740
+ break;
3741
+ case "hover+click":
3742
+ if (!options.css) {
3743
+ throw new Error("css is not defined");
3744
+ }
3745
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3746
+ if (results.length === 0) {
3747
+ throw new Error(`Element not found in cell area`);
3748
+ }
3749
+ state.element = results[0];
3750
+ await performAction("hover+click", state.element, options, this, state, _params);
3751
+ break;
3752
+ default:
3753
+ throw new Error("operation is not supported");
3754
+ }
3755
+ }
3756
+ catch (e) {
3757
+ await _commandError(state, e, this);
3758
+ }
3759
+ finally {
3760
+ await _commandFinally(state, this);
2694
3761
  }
2695
3762
  }
3763
+ saveTestDataAsGlobal(options, world) {
3764
+ const dataFile = _getDataFile(world, this.context, this);
3765
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
3766
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
3767
+ }
2696
3768
  async setViewportSize(width, hight, options = {}, world = null) {
2697
3769
  const startTime = Date.now();
2698
3770
  let error = null;
@@ -2710,21 +3782,23 @@ class StableBrowser {
2710
3782
  }
2711
3783
  catch (e) {
2712
3784
  console.log(".");
3785
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2713
3786
  }
2714
3787
  finally {
2715
3788
  await new Promise((resolve) => setTimeout(resolve, 2000));
2716
3789
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2717
3790
  const endTime = Date.now();
2718
- this._reportToWorld(world, {
3791
+ _reportToWorld(world, {
2719
3792
  type: Types.SET_VIEWPORT,
2720
3793
  text: "set viewport size to " + width + "x" + hight,
3794
+ _text: "Set the viewport size to " + width + "x" + hight,
2721
3795
  screenshotId,
2722
3796
  result: error
2723
3797
  ? {
2724
3798
  status: "FAILED",
2725
3799
  startTime,
2726
3800
  endTime,
2727
- message: error === null || error === void 0 ? void 0 : error.message,
3801
+ message: error?.message,
2728
3802
  }
2729
3803
  : {
2730
3804
  status: "PASSED",
@@ -2746,12 +3820,13 @@ class StableBrowser {
2746
3820
  }
2747
3821
  catch (e) {
2748
3822
  console.log(".");
3823
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2749
3824
  }
2750
3825
  finally {
2751
3826
  await new Promise((resolve) => setTimeout(resolve, 2000));
2752
3827
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2753
3828
  const endTime = Date.now();
2754
- this._reportToWorld(world, {
3829
+ _reportToWorld(world, {
2755
3830
  type: Types.GET_PAGE_STATUS,
2756
3831
  text: "page relaod",
2757
3832
  screenshotId,
@@ -2760,7 +3835,7 @@ class StableBrowser {
2760
3835
  status: "FAILED",
2761
3836
  startTime,
2762
3837
  endTime,
2763
- message: error === null || error === void 0 ? void 0 : error.message,
3838
+ message: error?.message,
2764
3839
  }
2765
3840
  : {
2766
3841
  status: "PASSED",
@@ -2787,11 +3862,195 @@ class StableBrowser {
2787
3862
  console.log("#-#");
2788
3863
  }
2789
3864
  }
2790
- _reportToWorld(world, properties) {
2791
- if (!world || !world.attach) {
2792
- return;
3865
+ async beforeScenario(world, scenario) {
3866
+ this.beforeScenarioCalled = true;
3867
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3868
+ this.scenarioName = scenario.pickle.name;
3869
+ }
3870
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3871
+ this.featureName = scenario.gherkinDocument.feature.name;
3872
+ }
3873
+ if (this.context) {
3874
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3875
+ }
3876
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3877
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3878
+ // check if @global_test_data tag is present
3879
+ if (this.tags.includes("@global_test_data")) {
3880
+ this.saveTestDataAsGlobal({}, world);
3881
+ }
3882
+ }
3883
+ // update test data based on feature/scenario
3884
+ let envName = null;
3885
+ if (this.context && this.context.environment) {
3886
+ envName = this.context.environment.name;
3887
+ }
3888
+ if (!process.env.TEMP_RUN) {
3889
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3890
+ }
3891
+ await loadBrunoParams(this.context, this.context.environment.name);
3892
+ }
3893
+ async afterScenario(world, scenario) { }
3894
+ async beforeStep(world, step) {
3895
+ if (!this.beforeScenarioCalled) {
3896
+ this.beforeScenario(world, step);
3897
+ }
3898
+ if (this.stepIndex === undefined) {
3899
+ this.stepIndex = 0;
3900
+ }
3901
+ else {
3902
+ this.stepIndex++;
3903
+ }
3904
+ if (step && step.pickleStep && step.pickleStep.text) {
3905
+ this.stepName = step.pickleStep.text;
3906
+ this.logger.info("step: " + this.stepName);
3907
+ }
3908
+ else if (step && step.text) {
3909
+ this.stepName = step.text;
3910
+ }
3911
+ else {
3912
+ this.stepName = "step " + this.stepIndex;
3913
+ }
3914
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3915
+ if (this.context.browserObject.context) {
3916
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3917
+ }
3918
+ }
3919
+ if (this.initSnapshotTaken === false) {
3920
+ this.initSnapshotTaken = true;
3921
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
3922
+ const snapshot = await this.getAriaSnapshot();
3923
+ if (snapshot) {
3924
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3925
+ }
3926
+ }
3927
+ }
3928
+ }
3929
+ async getAriaSnapshot() {
3930
+ try {
3931
+ // find the page url
3932
+ const url = await this.page.url();
3933
+ // extract the path from the url
3934
+ const path = new URL(url).pathname;
3935
+ // get the page title
3936
+ const title = await this.page.title();
3937
+ // go over other frams
3938
+ const frames = this.page.frames();
3939
+ const snapshots = [];
3940
+ const content = [`- path: ${path}`, `- title: ${title}`];
3941
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3942
+ for (let i = 0; i < frames.length; i++) {
3943
+ const frame = frames[i];
3944
+ try {
3945
+ // Ensure frame is attached and has body
3946
+ const body = frame.locator("body");
3947
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3948
+ const snapshot = await body.ariaSnapshot({ timeout });
3949
+ content.push(`- frame: ${i}`);
3950
+ content.push(snapshot);
3951
+ }
3952
+ catch (innerErr) { }
3953
+ }
3954
+ return content.join("\n");
3955
+ }
3956
+ catch (e) {
3957
+ console.log("Error in getAriaSnapshot");
3958
+ //console.debug(e);
3959
+ }
3960
+ return null;
3961
+ }
3962
+ /**
3963
+ * Sends command with custom payload to report.
3964
+ * @param commandText - Title of the command to be shown in the report.
3965
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3966
+ * @param content - Content of the command to be shown in the report.
3967
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3968
+ * @param world - Optional world context.
3969
+ * @public
3970
+ */
3971
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3972
+ const state = {
3973
+ options,
3974
+ world,
3975
+ locate: false,
3976
+ scroll: false,
3977
+ screenshot: options.screenshot ?? false,
3978
+ highlight: options.highlight ?? false,
3979
+ type: Types.REPORT_COMMAND,
3980
+ text: commandText,
3981
+ _text: commandText,
3982
+ operation: "report_command",
3983
+ log: "***** " + commandText + " *****\n",
3984
+ };
3985
+ try {
3986
+ await _preCommand(state, this);
3987
+ const payload = {
3988
+ type: options.type ?? "text",
3989
+ content: content,
3990
+ screenshotId: null,
3991
+ };
3992
+ state.payload = payload;
3993
+ if (commandStatus === "FAILED") {
3994
+ state.throwError = true;
3995
+ throw new Error("Command failed");
3996
+ }
3997
+ }
3998
+ catch (e) {
3999
+ await _commandError(state, e, this);
4000
+ }
4001
+ finally {
4002
+ await _commandFinally(state, this);
4003
+ }
4004
+ }
4005
+ async afterStep(world, step) {
4006
+ this.stepName = null;
4007
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4008
+ if (this.context.browserObject.context) {
4009
+ await this.context.browserObject.context.tracing.stopChunk({
4010
+ path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
4011
+ });
4012
+ if (world && world.attach) {
4013
+ await world.attach(JSON.stringify({
4014
+ type: "trace",
4015
+ traceFilePath: `trace-${this.stepIndex}.zip`,
4016
+ }), "application/json+trace");
4017
+ }
4018
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
4019
+ }
4020
+ }
4021
+ if (this.context) {
4022
+ this.context.examplesRow = null;
4023
+ }
4024
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4025
+ const snapshot = await this.getAriaSnapshot();
4026
+ if (snapshot) {
4027
+ const obj = {};
4028
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4029
+ }
4030
+ }
4031
+ if (!process.env.TEMP_RUN) {
4032
+ const state = {
4033
+ world,
4034
+ locate: false,
4035
+ scroll: false,
4036
+ screenshot: true,
4037
+ highlight: true,
4038
+ type: Types.STEP_COMPLETE,
4039
+ text: "end of scenario",
4040
+ _text: "end of scenario",
4041
+ operation: "step_complete",
4042
+ log: "***** " + "end of scenario" + " *****\n",
4043
+ };
4044
+ try {
4045
+ await _preCommand(state, this);
4046
+ }
4047
+ catch (e) {
4048
+ await _commandError(state, e, this);
4049
+ }
4050
+ finally {
4051
+ await _commandFinally(state, this);
4052
+ }
2793
4053
  }
2794
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2795
4054
  }
2796
4055
  }
2797
4056
  function createTimedPromise(promise, label) {
@@ -2799,151 +4058,5 @@ function createTimedPromise(promise, label) {
2799
4058
  .then((result) => ({ status: "fulfilled", label, result }))
2800
4059
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2801
4060
  }
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
4061
  export { StableBrowser };
2949
4062
  //# sourceMappingURL=stable_browser.js.map