automation_model 1.0.447-dev → 1.0.447

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