automation_model 1.0.446-dev → 1.0.446

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 +2395 -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 +18 -10
@@ -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,84 +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
- this._validateSelectors(selectors);
1202
- const startTime = Date.now();
1203
- let error = null;
1204
- let screenshotId = null;
1205
- let screenshotPath = null;
1206
- const info = {};
1207
- info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1208
- info.operation = "clickType";
1209
- info.selectors = selectors;
1390
+ _value = unEscapeString(_value);
1210
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
+ }
1211
1408
  if (newValue !== _value) {
1212
1409
  //this.logger.info(_value + "=" + newValue);
1213
1410
  _value = newValue;
1214
1411
  }
1215
- info.value = _value;
1216
1412
  try {
1217
- let element = await this._locate(selectors, info, _params);
1218
- //insert red border around the element
1219
- await this.scrollIfNeeded(element, info);
1220
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1221
- await this._highlightElements(element);
1222
- if (options === null || options === undefined || !options.press) {
1413
+ await _preCommand(state, this);
1414
+ state.info.value = _value;
1415
+ if (!options.press) {
1223
1416
  try {
1224
- let currentValue = await element.inputValue();
1417
+ let currentValue = await state.element.inputValue();
1225
1418
  if (currentValue) {
1226
- await element.fill("");
1419
+ await state.element.fill("");
1227
1420
  }
1228
1421
  }
1229
1422
  catch (e) {
1230
1423
  this.logger.info("unable to clear input value");
1231
1424
  }
1232
1425
  }
1233
- if (options === null || options === undefined || options.press) {
1234
- try {
1235
- await element.click({ timeout: 5000 });
1236
- }
1237
- catch (e) {
1238
- await element.dispatchEvent("click");
1239
- }
1426
+ if (options.press) {
1427
+ options.timeout = 5000;
1428
+ await performAction("click", state.element, options, this, state, _params);
1240
1429
  }
1241
1430
  else {
1242
1431
  try {
1243
- await element.focus();
1432
+ await state.element.focus();
1244
1433
  }
1245
1434
  catch (e) {
1246
- await element.dispatchEvent("focus");
1435
+ await state.element.dispatchEvent("focus");
1247
1436
  }
1248
1437
  }
1249
1438
  await new Promise((resolve) => setTimeout(resolve, 500));
1250
- const valueSegment = _value.split("&&");
1439
+ const valueSegment = state.value.split("&&");
1251
1440
  for (let i = 0; i < valueSegment.length; i++) {
1252
1441
  if (i > 0) {
1253
1442
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1267,13 +1456,21 @@ class StableBrowser {
1267
1456
  await new Promise((resolve) => setTimeout(resolve, 500));
1268
1457
  }
1269
1458
  }
1459
+ //if (!this.fastMode) {
1460
+ await _screenshot(state, this);
1461
+ //}
1270
1462
  if (enter === true) {
1271
1463
  await new Promise((resolve) => setTimeout(resolve, 2000));
1272
1464
  await this.page.keyboard.press("Enter");
1273
1465
  await this.waitForPageLoad();
1274
1466
  }
1275
1467
  else if (enter === false) {
1276
- await element.dispatchEvent("change");
1468
+ try {
1469
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1470
+ }
1471
+ catch (e) {
1472
+ // ignore
1473
+ }
1277
1474
  //await this.page.keyboard.press("Tab");
1278
1475
  }
1279
1476
  else {
@@ -1282,111 +1479,95 @@ class StableBrowser {
1282
1479
  await this.waitForPageLoad();
1283
1480
  }
1284
1481
  }
1285
- return info;
1482
+ return state.info;
1286
1483
  }
1287
1484
  catch (e) {
1288
- //await this.closeUnexpectedPopups();
1289
- this.logger.error("fill failed " + JSON.stringify(info));
1290
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1291
- info.screenshotPath = screenshotPath;
1292
- Object.assign(e, { info: info });
1293
- error = e;
1294
- throw e;
1485
+ await _commandError(state, e, this);
1295
1486
  }
1296
1487
  finally {
1297
- const endTime = Date.now();
1298
- this._reportToWorld(world, {
1299
- element_name: selectors.element_name,
1300
- type: Types.FILL,
1301
- screenshotId,
1302
- value: _value,
1303
- text: `clickType input with value: ${_value}`,
1304
- result: error
1305
- ? {
1306
- status: "FAILED",
1307
- startTime,
1308
- endTime,
1309
- message: error === null || error === void 0 ? void 0 : error.message,
1310
- }
1311
- : {
1312
- status: "PASSED",
1313
- startTime,
1314
- endTime,
1315
- },
1316
- info: info,
1317
- });
1488
+ await _commandFinally(state, this);
1318
1489
  }
1319
1490
  }
1320
1491
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
1321
- this._validateSelectors(selectors);
1322
- const startTime = Date.now();
1323
- let error = null;
1324
- let screenshotId = null;
1325
- let screenshotPath = null;
1326
- const info = {};
1327
- info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1328
- info.operation = "fill";
1329
- info.selectors = selectors;
1330
- 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
+ };
1331
1503
  try {
1332
- let element = await this._locate(selectors, info, _params);
1333
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1334
- await this._highlightElements(element);
1335
- await element.fill(value);
1336
- await element.dispatchEvent("change");
1504
+ await _preCommand(state, this);
1505
+ await state.element.fill(value);
1506
+ await state.element.dispatchEvent("change");
1337
1507
  if (enter) {
1338
1508
  await new Promise((resolve) => setTimeout(resolve, 2000));
1339
1509
  await this.page.keyboard.press("Enter");
1340
1510
  }
1341
1511
  await this.waitForPageLoad();
1342
- return info;
1512
+ return state.info;
1343
1513
  }
1344
1514
  catch (e) {
1345
- //await this.closeUnexpectedPopups();
1346
- this.logger.error("fill failed " + info.log);
1347
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1348
- info.screenshotPath = screenshotPath;
1349
- Object.assign(e, { info: info });
1350
- error = e;
1351
- throw e;
1515
+ await _commandError(state, e, this);
1352
1516
  }
1353
1517
  finally {
1354
- const endTime = Date.now();
1355
- this._reportToWorld(world, {
1356
- element_name: selectors.element_name,
1357
- type: Types.FILL,
1358
- screenshotId,
1359
- value,
1360
- text: `Fill input with value: ${value}`,
1361
- result: error
1362
- ? {
1363
- status: "FAILED",
1364
- startTime,
1365
- endTime,
1366
- message: error === null || error === void 0 ? void 0 : error.message,
1367
- }
1368
- : {
1369
- status: "PASSED",
1370
- startTime,
1371
- endTime,
1372
- },
1373
- info: info,
1374
- });
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);
1375
1554
  }
1376
1555
  }
1377
1556
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1378
1557
  return await this._getText(selectors, 0, _params, options, info, world);
1379
1558
  }
1380
1559
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1381
- this._validateSelectors(selectors);
1560
+ const timeout = this._getFindElementTimeout(options);
1561
+ _validateSelectors(selectors);
1382
1562
  let screenshotId = null;
1383
1563
  let screenshotPath = null;
1384
1564
  if (!info.log) {
1385
1565
  info.log = "";
1566
+ info.locatorLog = new LocatorLog(selectors);
1386
1567
  }
1387
1568
  info.operation = "getText";
1388
1569
  info.selectors = selectors;
1389
- let element = await this._locate(selectors, info, _params);
1570
+ let element = await this._locate(selectors, info, _params, timeout);
1390
1571
  if (climb > 0) {
1391
1572
  const climbArray = [];
1392
1573
  for (let i = 0; i < climb; i++) {
@@ -1405,6 +1586,18 @@ class StableBrowser {
1405
1586
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1406
1587
  try {
1407
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
+ // }
1408
1601
  const elementText = await element.innerText();
1409
1602
  return {
1410
1603
  text: elementText,
@@ -1416,188 +1609,219 @@ class StableBrowser {
1416
1609
  }
1417
1610
  catch (e) {
1418
1611
  //await this.closeUnexpectedPopups();
1419
- this.logger.info("no innerText will use textContent");
1612
+ this.logger.info("no innerText, will use textContent");
1420
1613
  const elementText = await element.textContent();
1421
1614
  return { text: elementText, screenshotId, screenshotPath, value: value };
1422
1615
  }
1423
1616
  }
1424
1617
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1425
- var _a;
1426
- this._validateSelectors(selectors);
1427
1618
  if (!pattern) {
1428
1619
  throw new Error("pattern is null");
1429
1620
  }
1430
1621
  if (!text) {
1431
1622
  throw new Error("text is null");
1432
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
+ };
1433
1641
  const newValue = await this._replaceWithLocalData(text, world);
1434
1642
  if (newValue !== text) {
1435
1643
  this.logger.info(text + "=" + newValue);
1436
1644
  text = newValue;
1437
1645
  }
1438
- const startTime = Date.now();
1439
- let error = null;
1440
- let screenshotId = null;
1441
- let screenshotPath = null;
1442
- const info = {};
1443
- info.log =
1444
- "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1445
- info.operation = "containsPattern";
1446
- info.selectors = selectors;
1447
- info.value = text;
1448
- info.pattern = pattern;
1449
1646
  let foundObj = null;
1450
1647
  try {
1451
- 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);
1452
1651
  if (foundObj && foundObj.element) {
1453
- await this.scrollIfNeeded(foundObj.element, info);
1652
+ await this.scrollIfNeeded(foundObj.element, state.info);
1454
1653
  }
1455
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1654
+ await _screenshot(state, this);
1456
1655
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1457
1656
  pattern = pattern.replace("{text}", escapedText);
1458
1657
  let regex = new RegExp(pattern, "im");
1459
- 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))) {
1460
- 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;
1461
1660
  throw new Error("element doesn't contain text " + text);
1462
1661
  }
1463
- return info;
1662
+ return state.info;
1464
1663
  }
1465
1664
  catch (e) {
1466
- //await this.closeUnexpectedPopups();
1467
- this.logger.error("verify element contains text failed " + info.log);
1468
- this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
1469
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1470
- info.screenshotPath = screenshotPath;
1471
- Object.assign(e, { info: info });
1472
- error = e;
1473
- throw e;
1665
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1666
+ await _commandError(state, e, this);
1474
1667
  }
1475
1668
  finally {
1476
- const endTime = Date.now();
1477
- this._reportToWorld(world, {
1478
- element_name: selectors.element_name,
1479
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1480
- value: pattern,
1481
- text: `Verify element contains pattern: ${pattern}`,
1482
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1483
- result: error
1484
- ? {
1485
- status: "FAILED",
1486
- startTime,
1487
- endTime,
1488
- message: error === null || error === void 0 ? void 0 : error.message,
1489
- }
1490
- : {
1491
- status: "PASSED",
1492
- startTime,
1493
- endTime,
1494
- },
1495
- info: info,
1496
- });
1669
+ await _commandFinally(state, this);
1497
1670
  }
1498
1671
  }
1499
1672
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1500
- var _a, _b, _c;
1501
- this._validateSelectors(selectors);
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
+ };
1502
1690
  if (!text) {
1503
1691
  throw new Error("text is null");
1504
1692
  }
1505
- const startTime = Date.now();
1506
- let error = null;
1507
- let screenshotId = null;
1508
- let screenshotPath = null;
1509
- const info = {};
1510
- info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1511
- info.operation = "containsText";
1512
- info.selectors = selectors;
1693
+ text = unEscapeString(text);
1513
1694
  const newValue = await this._replaceWithLocalData(text, world);
1514
1695
  if (newValue !== text) {
1515
1696
  this.logger.info(text + "=" + newValue);
1516
1697
  text = newValue;
1517
1698
  }
1518
- info.value = text;
1519
1699
  let foundObj = null;
1520
1700
  try {
1521
- foundObj = await this._getText(selectors, climb, _params, options, info, world);
1522
- if (foundObj && foundObj.element) {
1523
- await this.scrollIfNeeded(foundObj.element, info);
1524
- }
1525
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1526
- const dateAlternatives = findDateAlternatives(text);
1527
- const numberAlternatives = findNumberAlternatives(text);
1528
- if (dateAlternatives.date) {
1529
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1530
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
1531
- ((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
1532
- 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
+ }
1533
1718
  }
1534
- }
1535
- throw new Error("element doesn't contain text " + text);
1536
- }
1537
- else if (numberAlternatives.number) {
1538
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1539
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(numberAlternatives.numbers[i])) ||
1540
- ((_b = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _b === void 0 ? void 0 : _b.includes(numberAlternatives.numbers[i]))) {
1541
- 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;
1542
1729
  }
1543
1730
  }
1544
- throw new Error("element doesn't contain text " + text);
1545
- }
1546
- 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))) {
1547
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1548
- info.value = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value;
1549
- 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
1550
1736
  }
1551
- return info;
1737
+ state.info.foundText = foundObj?.text;
1738
+ state.info.value = foundObj?.value;
1739
+ throw new Error("element doesn't contain text " + text);
1552
1740
  }
1553
1741
  catch (e) {
1554
- //await this.closeUnexpectedPopups();
1555
- this.logger.error("verify element contains text failed " + info.log);
1556
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1557
- info.screenshotPath = screenshotPath;
1558
- Object.assign(e, { info: info });
1559
- error = e;
1742
+ await _commandError(state, e, this);
1560
1743
  throw e;
1561
1744
  }
1562
1745
  finally {
1563
- const endTime = Date.now();
1564
- this._reportToWorld(world, {
1565
- element_name: selectors.element_name,
1566
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1567
- text: `Verify element contains text: ${text}`,
1568
- value: text,
1569
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1570
- result: error
1571
- ? {
1572
- status: "FAILED",
1573
- startTime,
1574
- endTime,
1575
- message: error === null || error === void 0 ? void 0 : error.message,
1576
- }
1577
- : {
1578
- status: "PASSED",
1579
- startTime,
1580
- endTime,
1581
- },
1582
- info: info,
1583
- });
1746
+ await _commandFinally(state, this);
1584
1747
  }
1585
1748
  }
1586
- _getDataFile(world = null) {
1587
- let dataFile = null;
1588
- if (world && world.reportFolder) {
1589
- 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");
1590
1772
  }
1591
- else if (this.reportFolder) {
1592
- 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");
1593
1775
  }
1594
- else if (this.context && this.context.reportFolder) {
1595
- dataFile = path.join(this.context.reportFolder, "data.json");
1776
+ else if (referanceSnapshot.startsWith("yaml:")) {
1777
+ text = referanceSnapshot.substring(5);
1596
1778
  }
1597
1779
  else {
1598
- dataFile = "data.json";
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);
1817
+ }
1818
+ catch (e) {
1819
+ await _commandError(state, e, this);
1820
+ throw e;
1821
+ }
1822
+ finally {
1823
+ await _commandFinally(state, this);
1599
1824
  }
1600
- return dataFile;
1601
1825
  }
1602
1826
  async waitForUserInput(message, world = null) {
1603
1827
  if (!message) {
@@ -1627,12 +1851,21 @@ class StableBrowser {
1627
1851
  return;
1628
1852
  }
1629
1853
  // if data file exists, load it
1630
- const dataFile = this._getDataFile(world);
1854
+ const dataFile = _getDataFile(world, this.context, this);
1631
1855
  let data = this.getTestData(world);
1632
1856
  // merge the testData with the existing data
1633
1857
  Object.assign(data, testData);
1634
1858
  // save the data to the file
1635
- fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1859
+ fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
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));
1636
1869
  }
1637
1870
  _getDataFilePath(fileName) {
1638
1871
  let dataFile = path.join(this.project_path, "data", fileName);
@@ -1730,7 +1963,7 @@ class StableBrowser {
1730
1963
  }
1731
1964
  }
1732
1965
  getTestData(world = null) {
1733
- const dataFile = this._getDataFile(world);
1966
+ const dataFile = _getDataFile(world, this.context, this);
1734
1967
  let data = {};
1735
1968
  if (fs.existsSync(dataFile)) {
1736
1969
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1762,11 +1995,9 @@ class StableBrowser {
1762
1995
  if (!fs.existsSync(world.screenshotPath)) {
1763
1996
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1764
1997
  }
1765
- let nextIndex = 1;
1766
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1767
- nextIndex++;
1768
- }
1769
- 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");
1770
2001
  try {
1771
2002
  await this.takeScreenshot(screenshotPath);
1772
2003
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1776,15 +2007,15 @@ class StableBrowser {
1776
2007
  // this.logger.info("unable to save screenshot " + screenshotPath);
1777
2008
  // }
1778
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
+ }
1779
2015
  }
1780
2016
  catch (e) {
1781
2017
  this.logger.info("unable to take screenshot, ignored");
1782
2018
  }
1783
- result.screenshotId = nextIndex;
1784
- result.screenshotPath = screenshotPath;
1785
- if (info && info.box) {
1786
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1787
- }
1788
2019
  }
1789
2020
  else if (options && options.screenshot) {
1790
2021
  result.screenshotPath = options.screenshotPath;
@@ -1809,7 +2040,6 @@ class StableBrowser {
1809
2040
  }
1810
2041
  async takeScreenshot(screenshotPath) {
1811
2042
  const playContext = this.context.playContext;
1812
- const client = await playContext.newCDPSession(this.page);
1813
2043
  // Using CDP to capture the screenshot
1814
2044
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
1815
2045
  document.body.scrollWidth,
@@ -1819,164 +2049,433 @@ class StableBrowser {
1819
2049
  document.body.clientWidth,
1820
2050
  document.documentElement.clientWidth,
1821
2051
  ])));
1822
- const viewportHeight = Math.max(...(await this.page.evaluate(() => [
1823
- document.body.scrollHeight,
1824
- document.documentElement.scrollHeight,
1825
- document.body.offsetHeight,
1826
- document.documentElement.offsetHeight,
1827
- document.body.clientHeight,
1828
- document.documentElement.clientHeight,
1829
- ])));
1830
- const { data } = await client.send("Page.captureScreenshot", {
1831
- format: "png",
1832
- // clip: {
1833
- // x: 0,
1834
- // y: 0,
1835
- // width: viewportWidth,
1836
- // height: viewportHeight,
1837
- // scale: 1,
1838
- // },
1839
- });
1840
- if (!screenshotPath) {
1841
- return data;
1842
- }
1843
- let screenshotBuffer = Buffer.from(data, "base64");
1844
- const sharpBuffer = sharp(screenshotBuffer);
1845
- const metadata = await sharpBuffer.metadata();
1846
- //check if you are on retina display and reduce the quality of the image
1847
- if (metadata.width > viewportWidth || metadata.height > viewportHeight) {
1848
- screenshotBuffer = await sharpBuffer
1849
- .resize(viewportWidth, viewportHeight, {
1850
- fit: sharp.fit.inside,
1851
- withoutEnlargement: true,
1852
- })
1853
- .toBuffer();
1854
- }
1855
- fs.writeFileSync(screenshotPath, screenshotBuffer);
1856
- 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;
1857
2100
  }
1858
2101
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1859
- this._validateSelectors(selectors);
1860
- const startTime = Date.now();
1861
- let error = null;
1862
- let screenshotId = null;
1863
- 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
+ };
1864
2112
  await new Promise((resolve) => setTimeout(resolve, 2000));
1865
- const info = {};
1866
- info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1867
- info.operation = "verify";
1868
- info.selectors = selectors;
1869
2113
  try {
1870
- const element = await this._locate(selectors, info, _params);
1871
- if (element) {
1872
- await this.scrollIfNeeded(element, info);
1873
- }
1874
- await this._highlightElements(element);
1875
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1876
- await expect(element).toHaveCount(1, { timeout: 10000 });
1877
- return info;
2114
+ await _preCommand(state, this);
2115
+ await expect(state.element).toHaveCount(1, { timeout: 10000 });
2116
+ return state.info;
1878
2117
  }
1879
2118
  catch (e) {
1880
- //await this.closeUnexpectedPopups();
1881
- this.logger.error("verify failed " + info.log);
1882
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1883
- info.screenshotPath = screenshotPath;
1884
- Object.assign(e, { info: info });
1885
- error = e;
1886
- throw e;
2119
+ await _commandError(state, e, this);
1887
2120
  }
1888
2121
  finally {
1889
- const endTime = Date.now();
1890
- this._reportToWorld(world, {
1891
- element_name: selectors.element_name,
1892
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1893
- text: "Verify element exists in page",
1894
- screenshotId,
1895
- result: error
1896
- ? {
1897
- status: "FAILED",
1898
- startTime,
1899
- endTime,
1900
- message: error === null || error === void 0 ? void 0 : error.message,
1901
- }
1902
- : {
1903
- status: "PASSED",
1904
- startTime,
1905
- endTime,
1906
- },
1907
- info: info,
1908
- });
2122
+ await _commandFinally(state, this);
1909
2123
  }
1910
2124
  }
1911
2125
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1912
- this._validateSelectors(selectors);
1913
- const startTime = Date.now();
1914
- let error = null;
1915
- let screenshotId = null;
1916
- 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
+ };
1917
2140
  await new Promise((resolve) => setTimeout(resolve, 2000));
1918
- const info = {};
1919
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1920
- info.operation = "extract";
1921
- info.selectors = selectors;
1922
2141
  try {
1923
- const element = await this._locate(selectors, info, _params);
1924
- await this._highlightElements(element);
1925
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2142
+ await _preCommand(state, this);
1926
2143
  switch (attribute) {
1927
2144
  case "inner_text":
1928
- info.value = await element.innerText();
2145
+ state.value = await state.element.innerText();
1929
2146
  break;
1930
2147
  case "href":
1931
- info.value = await element.getAttribute("href");
2148
+ state.value = await state.element.getAttribute("href");
1932
2149
  break;
1933
2150
  case "value":
1934
- info.value = await element.inputValue();
2151
+ state.value = await state.element.inputValue();
2152
+ break;
2153
+ case "text":
2154
+ state.value = await state.element.textContent();
1935
2155
  break;
1936
2156
  default:
1937
- info.value = await element.getAttribute(attribute);
2157
+ state.value = await state.element.getAttribute(attribute);
1938
2158
  break;
1939
2159
  }
1940
- this[variable] = info.value;
1941
- if (world) {
1942
- 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
+ }
1943
2177
  }
1944
- this.setTestData({ [variable]: info.value }, world);
1945
- this.logger.info("set test data: " + variable + "=" + info.value);
1946
- 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;
1947
2183
  }
1948
2184
  catch (e) {
1949
- //await this.closeUnexpectedPopups();
1950
- this.logger.error("extract failed " + info.log);
1951
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1952
- info.screenshotPath = screenshotPath;
1953
- Object.assign(e, { info: info });
1954
- error = e;
1955
- throw e;
2185
+ await _commandError(state, e, this);
1956
2186
  }
1957
2187
  finally {
1958
- const endTime = Date.now();
1959
- this._reportToWorld(world, {
1960
- element_name: selectors.element_name,
1961
- type: Types.EXTRACT_ATTRIBUTE,
1962
- variable: variable,
1963
- value: info.value,
1964
- text: "Extract attribute from element",
1965
- screenshotId,
1966
- result: error
1967
- ? {
1968
- status: "FAILED",
1969
- startTime,
1970
- endTime,
1971
- 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}`)) || "";
1972
2226
  }
1973
- : {
1974
- status: "PASSED",
1975
- startTime,
1976
- endTime,
1977
- },
1978
- info: info,
1979
- });
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);
1980
2479
  }
1981
2480
  }
1982
2481
  async extractEmailData(emailAddress, options, world) {
@@ -1997,7 +2496,7 @@ class StableBrowser {
1997
2496
  if (options && options.timeout) {
1998
2497
  timeout = options.timeout;
1999
2498
  }
2000
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2499
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2001
2500
  const request = {
2002
2501
  method: "POST",
2003
2502
  url: serviceUrl,
@@ -2053,7 +2552,8 @@ class StableBrowser {
2053
2552
  catch (e) {
2054
2553
  errorCount++;
2055
2554
  if (errorCount > 3) {
2056
- throw e;
2555
+ // throw e;
2556
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
2057
2557
  }
2058
2558
  // ignore
2059
2559
  }
@@ -2067,27 +2567,32 @@ class StableBrowser {
2067
2567
  async _highlightElements(scope, css) {
2068
2568
  try {
2069
2569
  if (!scope) {
2570
+ // console.log(`Scope is not defined`);
2070
2571
  return;
2071
2572
  }
2072
2573
  if (!css) {
2073
2574
  scope
2074
2575
  .evaluate((node) => {
2075
2576
  if (node && node.style) {
2076
- let originalBorder = node.style.border;
2077
- 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}`);
2078
2582
  if (window) {
2079
2583
  window.addEventListener("beforeunload", function (e) {
2080
- node.style.border = originalBorder;
2584
+ node.style.outline = originalOutline;
2081
2585
  });
2082
2586
  }
2083
2587
  setTimeout(function () {
2084
- node.style.border = originalBorder;
2588
+ node.style.outline = originalOutline;
2085
2589
  }, 2000);
2086
2590
  }
2087
2591
  })
2088
2592
  .then(() => { })
2089
2593
  .catch((e) => {
2090
2594
  // ignore
2595
+ // console.error(`Could not highlight node : ${e}`);
2091
2596
  });
2092
2597
  }
2093
2598
  else {
@@ -2103,17 +2608,18 @@ class StableBrowser {
2103
2608
  if (!element.style) {
2104
2609
  return;
2105
2610
  }
2106
- var originalBorder = element.style.border;
2611
+ let originalOutline = element.style.outline;
2612
+ element.__previousOutline = originalOutline;
2107
2613
  // Set the new border to be red and 2px solid
2108
- element.style.border = "2px solid red";
2614
+ element.style.outline = "2px solid red";
2109
2615
  if (window) {
2110
2616
  window.addEventListener("beforeunload", function (e) {
2111
- element.style.border = originalBorder;
2617
+ element.style.outline = originalOutline;
2112
2618
  });
2113
2619
  }
2114
2620
  // Set a timeout to revert to the original border after 2 seconds
2115
2621
  setTimeout(function () {
2116
- element.style.border = originalBorder;
2622
+ element.style.outline = originalOutline;
2117
2623
  }, 2000);
2118
2624
  }
2119
2625
  return;
@@ -2121,6 +2627,7 @@ class StableBrowser {
2121
2627
  .then(() => { })
2122
2628
  .catch((e) => {
2123
2629
  // ignore
2630
+ // console.error(`Could not highlight css: ${e}`);
2124
2631
  });
2125
2632
  }
2126
2633
  }
@@ -2128,173 +2635,563 @@ class StableBrowser {
2128
2635
  console.debug(error);
2129
2636
  }
2130
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
+ */
2131
2680
  async verifyPagePath(pathPart, options = {}, world = null) {
2132
- const startTime = Date.now();
2133
2681
  let error = null;
2134
2682
  let screenshotId = null;
2135
2683
  let screenshotPath = null;
2136
2684
  await new Promise((resolve) => setTimeout(resolve, 2000));
2137
- const info = {};
2138
- info.log = "***** verify page path " + pathPart + " *****\n";
2139
- info.operation = "verifyPagePath";
2140
- const newValue = await this._replaceWithLocalData(pathPart, world);
2141
- if (newValue !== pathPart) {
2142
- this.logger.info(pathPart + "=" + newValue);
2143
- pathPart = newValue;
2685
+ const info = {};
2686
+ info.log = "***** verify page path " + pathPart + " *****\n";
2687
+ info.operation = "verifyPagePath";
2688
+ const newValue = await this._replaceWithLocalData(pathPart, world);
2689
+ if (newValue !== pathPart) {
2690
+ this.logger.info(pathPart + "=" + newValue);
2691
+ pathPart = newValue;
2692
+ }
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;
2144
2955
  }
2145
- info.pathPart = pathPart;
2956
+ let dateAlternatives = findDateAlternatives(text);
2957
+ let numberAlternatives = findNumberAlternatives(text);
2146
2958
  try {
2147
- for (let i = 0; i < 30; i++) {
2148
- const url = await this.page.url();
2149
- if (!url.includes(pathPart)) {
2150
- if (i === 29) {
2151
- 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`);
2152
2974
  }
2153
2975
  await new Promise((resolve) => setTimeout(resolve, 1000));
2154
2976
  continue;
2155
2977
  }
2156
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2157
- 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
+ }
2158
2995
  }
2159
2996
  }
2160
2997
  catch (e) {
2161
- //await this.closeUnexpectedPopups();
2162
- this.logger.error("verify page path failed " + info.log);
2163
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2164
- info.screenshotPath = screenshotPath;
2165
- Object.assign(e, { info: info });
2166
- error = e;
2167
- throw e;
2998
+ await _commandError(state, e, this);
2168
2999
  }
2169
3000
  finally {
2170
- const endTime = Date.now();
2171
- this._reportToWorld(world, {
2172
- type: Types.VERIFY_PAGE_PATH,
2173
- text: "Verify page path",
2174
- screenshotId,
2175
- result: error
2176
- ? {
2177
- status: "FAILED",
2178
- startTime,
2179
- endTime,
2180
- message: error === null || error === void 0 ? void 0 : error.message,
2181
- }
2182
- : {
2183
- status: "PASSED",
2184
- startTime,
2185
- endTime,
2186
- },
2187
- info: info,
2188
- });
3001
+ await _commandFinally(state, this);
2189
3002
  }
2190
3003
  }
2191
- async verifyTextExistInPage(text, options = {}, world = null) {
2192
- const startTime = Date.now();
2193
- const timeout = this._getLoadTimeout(options);
2194
- let error = null;
2195
- let screenshotId = null;
2196
- let screenshotPath = null;
3004
+ async waitForTextToDisappear(text, options = {}, world = null) {
3005
+ text = unEscapeString(text);
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);
2197
3023
  await new Promise((resolve) => setTimeout(resolve, 2000));
2198
- const info = {};
2199
- info.log = "***** verify text " + text + " exists in page *****\n";
2200
- info.operation = "verifyTextExistInPage";
2201
3024
  const newValue = await this._replaceWithLocalData(text, world);
2202
3025
  if (newValue !== text) {
2203
3026
  this.logger.info(text + "=" + newValue);
2204
3027
  text = newValue;
2205
3028
  }
2206
- info.text = text;
2207
3029
  let dateAlternatives = findDateAlternatives(text);
2208
3030
  let numberAlternatives = findNumberAlternatives(text);
2209
3031
  try {
3032
+ await _preCommand(state, this);
3033
+ state.info.text = text;
3034
+ let resultWithElementsFound = {
3035
+ length: null, // initial cannot be 0
3036
+ };
2210
3037
  while (true) {
2211
- const frames = this.page.frames();
2212
- let results = [];
2213
- for (let i = 0; i < frames.length; i++) {
2214
- if (dateAlternatives.date) {
2215
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2216
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
2217
- result.frame = frames[i];
2218
- results.push(result);
2219
- }
2220
- }
2221
- else if (numberAlternatives.number) {
2222
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2223
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
2224
- result.frame = frames[i];
2225
- results.push(result);
2226
- }
2227
- }
2228
- else {
2229
- const result = await this._locateElementByText(frames[i], text, "*", true, {});
2230
- result.frame = frames[i];
2231
- results.push(result);
2232
- }
3038
+ try {
3039
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3040
+ }
3041
+ catch (error) {
3042
+ // ignore
2233
3043
  }
2234
- info.results = results;
2235
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2236
3044
  if (resultWithElementsFound.length === 0) {
2237
- if (Date.now() - startTime > timeout) {
2238
- 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`);
2239
3108
  }
2240
3109
  await new Promise((resolve) => setTimeout(resolve, 1000));
2241
3110
  continue;
2242
3111
  }
2243
- if (resultWithElementsFound[0].randomToken) {
2244
- const frame = resultWithElementsFound[0].frame;
2245
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2246
- await this._highlightElements(frame, dataAttribute);
2247
- const element = await frame.$(dataAttribute);
2248
- if (element) {
2249
- await this.scrollIfNeeded(element, info);
2250
- 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
+ }
2251
3157
  }
2252
3158
  }
2253
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2254
- return info;
3159
+ catch (error) {
3160
+ console.error(error);
3161
+ }
2255
3162
  }
2256
3163
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2257
3164
  }
2258
3165
  catch (e) {
2259
- //await this.closeUnexpectedPopups();
2260
- this.logger.error("verify text exist in page failed " + info.log);
2261
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2262
- info.screenshotPath = screenshotPath;
2263
- Object.assign(e, { info: info });
2264
- error = e;
2265
- throw e;
3166
+ await _commandError(state, e, this);
2266
3167
  }
2267
3168
  finally {
2268
- const endTime = Date.now();
2269
- this._reportToWorld(world, {
2270
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2271
- text: "Verify text exists in page",
2272
- screenshotId,
2273
- result: error
2274
- ? {
2275
- status: "FAILED",
2276
- startTime,
2277
- endTime,
2278
- message: error === null || error === void 0 ? void 0 : error.message,
2279
- }
2280
- : {
2281
- status: "PASSED",
2282
- startTime,
2283
- endTime,
2284
- },
2285
- info: info,
2286
- });
3169
+ await _commandFinally(state, this);
2287
3170
  }
2288
3171
  }
2289
- _getServerUrl() {
2290
- let serviceUrl = "https://api.blinq.io";
2291
- if (process.env.NODE_ENV_BLINQ === "dev") {
2292
- serviceUrl = "https://dev.api.blinq.io";
2293
- }
2294
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2295
- 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
+ }
2296
3191
  }
2297
- return serviceUrl;
3192
+ // state.info.results = results;
3193
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3194
+ return resultWithElementsFound;
2298
3195
  }
2299
3196
  async visualVerification(text, options = {}, world = null) {
2300
3197
  const startTime = Date.now();
@@ -2310,14 +3207,17 @@ class StableBrowser {
2310
3207
  throw new Error("TOKEN is not set");
2311
3208
  }
2312
3209
  try {
2313
- let serviceUrl = this._getServerUrl();
3210
+ let serviceUrl = _getServerUrl();
2314
3211
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2315
3212
  info.screenshotPath = screenshotPath;
2316
3213
  const screenshot = await this.takeScreenshot();
2317
- const request = {
2318
- method: "POST",
3214
+ let request = {
3215
+ method: "post",
3216
+ maxBodyLength: Infinity,
2319
3217
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2320
3218
  headers: {
3219
+ "x-bvt-project-id": path.basename(this.project_path),
3220
+ "x-source": "aaa",
2321
3221
  "Content-Type": "application/json",
2322
3222
  Authorization: `Bearer ${process.env.TOKEN}`,
2323
3223
  },
@@ -2326,7 +3226,7 @@ class StableBrowser {
2326
3226
  screenshot: screenshot,
2327
3227
  }),
2328
3228
  };
2329
- let result = await this.context.api.request(request);
3229
+ const result = await axios.request(request);
2330
3230
  if (result.data.status !== true) {
2331
3231
  throw new Error("Visual validation failed");
2332
3232
  }
@@ -2346,20 +3246,22 @@ class StableBrowser {
2346
3246
  info.screenshotPath = screenshotPath;
2347
3247
  Object.assign(e, { info: info });
2348
3248
  error = e;
2349
- throw e;
3249
+ // throw e;
3250
+ await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
2350
3251
  }
2351
3252
  finally {
2352
3253
  const endTime = Date.now();
2353
- this._reportToWorld(world, {
3254
+ _reportToWorld(world, {
2354
3255
  type: Types.VERIFY_VISUAL,
2355
3256
  text: "Visual verification",
3257
+ _text: "Visual verification of " + text,
2356
3258
  screenshotId,
2357
3259
  result: error
2358
3260
  ? {
2359
3261
  status: "FAILED",
2360
3262
  startTime,
2361
3263
  endTime,
2362
- message: error === null || error === void 0 ? void 0 : error.message,
3264
+ message: error?.message,
2363
3265
  }
2364
3266
  : {
2365
3267
  status: "PASSED",
@@ -2391,13 +3293,14 @@ class StableBrowser {
2391
3293
  this.logger.info("Table data verified");
2392
3294
  }
2393
3295
  async getTableData(selectors, _params = null, options = {}, world = null) {
2394
- this._validateSelectors(selectors);
3296
+ _validateSelectors(selectors);
2395
3297
  const startTime = Date.now();
2396
3298
  let error = null;
2397
3299
  let screenshotId = null;
2398
3300
  let screenshotPath = null;
2399
3301
  const info = {};
2400
3302
  info.log = "";
3303
+ info.locatorLog = new LocatorLog(selectors);
2401
3304
  info.operation = "getTableData";
2402
3305
  info.selectors = selectors;
2403
3306
  try {
@@ -2413,11 +3316,12 @@ class StableBrowser {
2413
3316
  info.screenshotPath = screenshotPath;
2414
3317
  Object.assign(e, { info: info });
2415
3318
  error = e;
2416
- throw e;
3319
+ // throw e;
3320
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2417
3321
  }
2418
3322
  finally {
2419
3323
  const endTime = Date.now();
2420
- this._reportToWorld(world, {
3324
+ _reportToWorld(world, {
2421
3325
  element_name: selectors.element_name,
2422
3326
  type: Types.GET_TABLE_DATA,
2423
3327
  text: "Get table data",
@@ -2427,7 +3331,7 @@ class StableBrowser {
2427
3331
  status: "FAILED",
2428
3332
  startTime,
2429
3333
  endTime,
2430
- message: error === null || error === void 0 ? void 0 : error.message,
3334
+ message: error?.message,
2431
3335
  }
2432
3336
  : {
2433
3337
  status: "PASSED",
@@ -2439,7 +3343,7 @@ class StableBrowser {
2439
3343
  }
2440
3344
  }
2441
3345
  async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
2442
- this._validateSelectors(selectors);
3346
+ _validateSelectors(selectors);
2443
3347
  if (!query) {
2444
3348
  throw new Error("query is null");
2445
3349
  }
@@ -2472,7 +3376,7 @@ class StableBrowser {
2472
3376
  info.operation = "analyzeTable";
2473
3377
  info.selectors = selectors;
2474
3378
  info.query = query;
2475
- query = this._fixUsingParams(query, _params);
3379
+ query = _fixUsingParams(query, _params);
2476
3380
  info.query_fixed = query;
2477
3381
  info.operator = operator;
2478
3382
  info.value = value;
@@ -2578,11 +3482,12 @@ class StableBrowser {
2578
3482
  info.screenshotPath = screenshotPath;
2579
3483
  Object.assign(e, { info: info });
2580
3484
  error = e;
2581
- throw e;
3485
+ // throw e;
3486
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2582
3487
  }
2583
3488
  finally {
2584
3489
  const endTime = Date.now();
2585
- this._reportToWorld(world, {
3490
+ _reportToWorld(world, {
2586
3491
  element_name: selectors.element_name,
2587
3492
  type: Types.ANALYZE_TABLE,
2588
3493
  text: "Analyze table",
@@ -2592,7 +3497,7 @@ class StableBrowser {
2592
3497
  status: "FAILED",
2593
3498
  startTime,
2594
3499
  endTime,
2595
- message: error === null || error === void 0 ? void 0 : error.message,
3500
+ message: error?.message,
2596
3501
  }
2597
3502
  : {
2598
3503
  status: "PASSED",
@@ -2603,28 +3508,51 @@ class StableBrowser {
2603
3508
  });
2604
3509
  }
2605
3510
  }
2606
- async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2607
- if (!value) {
2608
- return value;
2609
- }
2610
- // find all the accurance of {{(.*?)}} and replace with the value
2611
- let regex = /{{(.*?)}}/g;
2612
- let matches = value.match(regex);
2613
- if (matches) {
2614
- const testData = this.getTestData(world);
2615
- for (let i = 0; i < matches.length; i++) {
2616
- let match = matches[i];
2617
- let key = match.substring(2, match.length - 2);
2618
- let newValue = objectPath.get(testData, key, null);
2619
- if (newValue !== null) {
2620
- value = value.replace(match, newValue);
2621
- }
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");
2622
3537
  }
3538
+ await new Promise((resolve) => setTimeout(resolve, duration));
3539
+ return state.info;
3540
+ }
3541
+ catch (e) {
3542
+ await _commandError(state, e, this);
3543
+ }
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);
2623
3551
  }
2624
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2625
- return await decrypt(value, null, totpWait);
3552
+ catch (error) {
3553
+ this.logger.debug(error);
3554
+ throw error;
2626
3555
  }
2627
- return value;
2628
3556
  }
2629
3557
  _getLoadTimeout(options) {
2630
3558
  let timeout = 15000;
@@ -2636,6 +3564,37 @@ class StableBrowser {
2636
3564
  }
2637
3565
  return timeout;
2638
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
+ }
2639
3598
  async waitForPageLoad(options = {}, world = null) {
2640
3599
  let timeout = this._getLoadTimeout(options);
2641
3600
  const promiseArray = [];
@@ -2675,7 +3634,7 @@ class StableBrowser {
2675
3634
  await new Promise((resolve) => setTimeout(resolve, 2000));
2676
3635
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2677
3636
  const endTime = Date.now();
2678
- this._reportToWorld(world, {
3637
+ _reportToWorld(world, {
2679
3638
  type: Types.GET_PAGE_STATUS,
2680
3639
  text: "Wait for page load",
2681
3640
  screenshotId,
@@ -2684,7 +3643,7 @@ class StableBrowser {
2684
3643
  status: "FAILED",
2685
3644
  startTime,
2686
3645
  endTime,
2687
- message: error === null || error === void 0 ? void 0 : error.message,
3646
+ message: error?.message,
2688
3647
  }
2689
3648
  : {
2690
3649
  status: "PASSED",
@@ -2695,41 +3654,123 @@ class StableBrowser {
2695
3654
  }
2696
3655
  }
2697
3656
  async closePage(options = {}, world = null) {
2698
- const startTime = Date.now();
2699
- let error = null;
2700
- let screenshotId = null;
2701
- let screenshotPath = null;
2702
- 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
+ };
2703
3670
  try {
3671
+ await _preCommand(state, this);
2704
3672
  await this.page.close();
2705
3673
  }
2706
3674
  catch (e) {
2707
3675
  console.log(".");
3676
+ await _commandError(state, e, this);
2708
3677
  }
2709
3678
  finally {
2710
- await new Promise((resolve) => setTimeout(resolve, 2000));
2711
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2712
- const endTime = Date.now();
2713
- this._reportToWorld(world, {
2714
- type: Types.CLOSE_PAGE,
2715
- text: "close page",
2716
- screenshotId,
2717
- result: error
2718
- ? {
2719
- status: "FAILED",
2720
- startTime,
2721
- endTime,
2722
- 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;
2723
3714
  }
2724
- : {
2725
- status: "PASSED",
2726
- startTime,
2727
- endTime,
2728
- },
2729
- info: info,
2730
- });
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);
2731
3767
  }
2732
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
+ }
2733
3774
  async setViewportSize(width, hight, options = {}, world = null) {
2734
3775
  const startTime = Date.now();
2735
3776
  let error = null;
@@ -2747,21 +3788,23 @@ class StableBrowser {
2747
3788
  }
2748
3789
  catch (e) {
2749
3790
  console.log(".");
3791
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2750
3792
  }
2751
3793
  finally {
2752
3794
  await new Promise((resolve) => setTimeout(resolve, 2000));
2753
3795
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2754
3796
  const endTime = Date.now();
2755
- this._reportToWorld(world, {
3797
+ _reportToWorld(world, {
2756
3798
  type: Types.SET_VIEWPORT,
2757
3799
  text: "set viewport size to " + width + "x" + hight,
3800
+ _text: "Set the viewport size to " + width + "x" + hight,
2758
3801
  screenshotId,
2759
3802
  result: error
2760
3803
  ? {
2761
3804
  status: "FAILED",
2762
3805
  startTime,
2763
3806
  endTime,
2764
- message: error === null || error === void 0 ? void 0 : error.message,
3807
+ message: error?.message,
2765
3808
  }
2766
3809
  : {
2767
3810
  status: "PASSED",
@@ -2783,12 +3826,13 @@ class StableBrowser {
2783
3826
  }
2784
3827
  catch (e) {
2785
3828
  console.log(".");
3829
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2786
3830
  }
2787
3831
  finally {
2788
3832
  await new Promise((resolve) => setTimeout(resolve, 2000));
2789
3833
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2790
3834
  const endTime = Date.now();
2791
- this._reportToWorld(world, {
3835
+ _reportToWorld(world, {
2792
3836
  type: Types.GET_PAGE_STATUS,
2793
3837
  text: "page relaod",
2794
3838
  screenshotId,
@@ -2797,7 +3841,7 @@ class StableBrowser {
2797
3841
  status: "FAILED",
2798
3842
  startTime,
2799
3843
  endTime,
2800
- message: error === null || error === void 0 ? void 0 : error.message,
3844
+ message: error?.message,
2801
3845
  }
2802
3846
  : {
2803
3847
  status: "PASSED",
@@ -2824,11 +3868,195 @@ class StableBrowser {
2824
3868
  console.log("#-#");
2825
3869
  }
2826
3870
  }
2827
- _reportToWorld(world, properties) {
2828
- if (!world || !world.attach) {
2829
- return;
3871
+ async beforeScenario(world, scenario) {
3872
+ this.beforeScenarioCalled = true;
3873
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3874
+ this.scenarioName = scenario.pickle.name;
3875
+ }
3876
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3877
+ this.featureName = scenario.gherkinDocument.feature.name;
3878
+ }
3879
+ if (this.context) {
3880
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3881
+ }
3882
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3883
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3884
+ // check if @global_test_data tag is present
3885
+ if (this.tags.includes("@global_test_data")) {
3886
+ this.saveTestDataAsGlobal({}, world);
3887
+ }
3888
+ }
3889
+ // update test data based on feature/scenario
3890
+ let envName = null;
3891
+ if (this.context && this.context.environment) {
3892
+ envName = this.context.environment.name;
3893
+ }
3894
+ if (!process.env.TEMP_RUN) {
3895
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3896
+ }
3897
+ await loadBrunoParams(this.context, this.context.environment.name);
3898
+ }
3899
+ async afterScenario(world, scenario) { }
3900
+ async beforeStep(world, step) {
3901
+ if (!this.beforeScenarioCalled) {
3902
+ this.beforeScenario(world, step);
3903
+ }
3904
+ if (this.stepIndex === undefined) {
3905
+ this.stepIndex = 0;
3906
+ }
3907
+ else {
3908
+ this.stepIndex++;
3909
+ }
3910
+ if (step && step.pickleStep && step.pickleStep.text) {
3911
+ this.stepName = step.pickleStep.text;
3912
+ this.logger.info("step: " + this.stepName);
3913
+ }
3914
+ else if (step && step.text) {
3915
+ this.stepName = step.text;
3916
+ }
3917
+ else {
3918
+ this.stepName = "step " + this.stepIndex;
3919
+ }
3920
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3921
+ if (this.context.browserObject.context) {
3922
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3923
+ }
3924
+ }
3925
+ if (this.initSnapshotTaken === false) {
3926
+ this.initSnapshotTaken = true;
3927
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
3928
+ const snapshot = await this.getAriaSnapshot();
3929
+ if (snapshot) {
3930
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3931
+ }
3932
+ }
3933
+ }
3934
+ }
3935
+ async getAriaSnapshot() {
3936
+ try {
3937
+ // find the page url
3938
+ const url = await this.page.url();
3939
+ // extract the path from the url
3940
+ const path = new URL(url).pathname;
3941
+ // get the page title
3942
+ const title = await this.page.title();
3943
+ // go over other frams
3944
+ const frames = this.page.frames();
3945
+ const snapshots = [];
3946
+ const content = [`- path: ${path}`, `- title: ${title}`];
3947
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3948
+ for (let i = 0; i < frames.length; i++) {
3949
+ const frame = frames[i];
3950
+ try {
3951
+ // Ensure frame is attached and has body
3952
+ const body = frame.locator("body");
3953
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3954
+ const snapshot = await body.ariaSnapshot({ timeout });
3955
+ content.push(`- frame: ${i}`);
3956
+ content.push(snapshot);
3957
+ }
3958
+ catch (innerErr) { }
3959
+ }
3960
+ return content.join("\n");
3961
+ }
3962
+ catch (e) {
3963
+ console.log("Error in getAriaSnapshot");
3964
+ //console.debug(e);
3965
+ }
3966
+ return null;
3967
+ }
3968
+ /**
3969
+ * Sends command with custom payload to report.
3970
+ * @param commandText - Title of the command to be shown in the report.
3971
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3972
+ * @param content - Content of the command to be shown in the report.
3973
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3974
+ * @param world - Optional world context.
3975
+ * @public
3976
+ */
3977
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3978
+ const state = {
3979
+ options,
3980
+ world,
3981
+ locate: false,
3982
+ scroll: false,
3983
+ screenshot: options.screenshot ?? false,
3984
+ highlight: options.highlight ?? false,
3985
+ type: Types.REPORT_COMMAND,
3986
+ text: commandText,
3987
+ _text: commandText,
3988
+ operation: "report_command",
3989
+ log: "***** " + commandText + " *****\n",
3990
+ };
3991
+ try {
3992
+ await _preCommand(state, this);
3993
+ const payload = {
3994
+ type: options.type ?? "text",
3995
+ content: content,
3996
+ screenshotId: null,
3997
+ };
3998
+ state.payload = payload;
3999
+ if (commandStatus === "FAILED") {
4000
+ state.throwError = true;
4001
+ throw new Error("Command failed");
4002
+ }
4003
+ }
4004
+ catch (e) {
4005
+ await _commandError(state, e, this);
4006
+ }
4007
+ finally {
4008
+ await _commandFinally(state, this);
4009
+ }
4010
+ }
4011
+ async afterStep(world, step) {
4012
+ this.stepName = null;
4013
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4014
+ if (this.context.browserObject.context) {
4015
+ await this.context.browserObject.context.tracing.stopChunk({
4016
+ path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
4017
+ });
4018
+ if (world && world.attach) {
4019
+ await world.attach(JSON.stringify({
4020
+ type: "trace",
4021
+ traceFilePath: `trace-${this.stepIndex}.zip`,
4022
+ }), "application/json+trace");
4023
+ }
4024
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
4025
+ }
4026
+ }
4027
+ if (this.context) {
4028
+ this.context.examplesRow = null;
4029
+ }
4030
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4031
+ const snapshot = await this.getAriaSnapshot();
4032
+ if (snapshot) {
4033
+ const obj = {};
4034
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4035
+ }
4036
+ }
4037
+ if (!process.env.TEMP_RUN) {
4038
+ const state = {
4039
+ world,
4040
+ locate: false,
4041
+ scroll: false,
4042
+ screenshot: true,
4043
+ highlight: true,
4044
+ type: Types.STEP_COMPLETE,
4045
+ text: "end of scenario",
4046
+ _text: "end of scenario",
4047
+ operation: "step_complete",
4048
+ log: "***** " + "end of scenario" + " *****\n",
4049
+ };
4050
+ try {
4051
+ await _preCommand(state, this);
4052
+ }
4053
+ catch (e) {
4054
+ await _commandError(state, e, this);
4055
+ }
4056
+ finally {
4057
+ await _commandFinally(state, this);
4058
+ }
2830
4059
  }
2831
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2832
4060
  }
2833
4061
  }
2834
4062
  function createTimedPromise(promise, label) {
@@ -2836,151 +4064,5 @@ function createTimedPromise(promise, label) {
2836
4064
  .then((result) => ({ status: "fulfilled", label, result }))
2837
4065
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2838
4066
  }
2839
- const KEYBOARD_EVENTS = [
2840
- "ALT",
2841
- "AltGraph",
2842
- "CapsLock",
2843
- "Control",
2844
- "Fn",
2845
- "FnLock",
2846
- "Hyper",
2847
- "Meta",
2848
- "NumLock",
2849
- "ScrollLock",
2850
- "Shift",
2851
- "Super",
2852
- "Symbol",
2853
- "SymbolLock",
2854
- "Enter",
2855
- "Tab",
2856
- "ArrowDown",
2857
- "ArrowLeft",
2858
- "ArrowRight",
2859
- "ArrowUp",
2860
- "End",
2861
- "Home",
2862
- "PageDown",
2863
- "PageUp",
2864
- "Backspace",
2865
- "Clear",
2866
- "Copy",
2867
- "CrSel",
2868
- "Cut",
2869
- "Delete",
2870
- "EraseEof",
2871
- "ExSel",
2872
- "Insert",
2873
- "Paste",
2874
- "Redo",
2875
- "Undo",
2876
- "Accept",
2877
- "Again",
2878
- "Attn",
2879
- "Cancel",
2880
- "ContextMenu",
2881
- "Escape",
2882
- "Execute",
2883
- "Find",
2884
- "Finish",
2885
- "Help",
2886
- "Pause",
2887
- "Play",
2888
- "Props",
2889
- "Select",
2890
- "ZoomIn",
2891
- "ZoomOut",
2892
- "BrightnessDown",
2893
- "BrightnessUp",
2894
- "Eject",
2895
- "LogOff",
2896
- "Power",
2897
- "PowerOff",
2898
- "PrintScreen",
2899
- "Hibernate",
2900
- "Standby",
2901
- "WakeUp",
2902
- "AllCandidates",
2903
- "Alphanumeric",
2904
- "CodeInput",
2905
- "Compose",
2906
- "Convert",
2907
- "Dead",
2908
- "FinalMode",
2909
- "GroupFirst",
2910
- "GroupLast",
2911
- "GroupNext",
2912
- "GroupPrevious",
2913
- "ModeChange",
2914
- "NextCandidate",
2915
- "NonConvert",
2916
- "PreviousCandidate",
2917
- "Process",
2918
- "SingleCandidate",
2919
- "HangulMode",
2920
- "HanjaMode",
2921
- "JunjaMode",
2922
- "Eisu",
2923
- "Hankaku",
2924
- "Hiragana",
2925
- "HiraganaKatakana",
2926
- "KanaMode",
2927
- "KanjiMode",
2928
- "Katakana",
2929
- "Romaji",
2930
- "Zenkaku",
2931
- "ZenkakuHanaku",
2932
- "F1",
2933
- "F2",
2934
- "F3",
2935
- "F4",
2936
- "F5",
2937
- "F6",
2938
- "F7",
2939
- "F8",
2940
- "F9",
2941
- "F10",
2942
- "F11",
2943
- "F12",
2944
- "Soft1",
2945
- "Soft2",
2946
- "Soft3",
2947
- "Soft4",
2948
- "ChannelDown",
2949
- "ChannelUp",
2950
- "Close",
2951
- "MailForward",
2952
- "MailReply",
2953
- "MailSend",
2954
- "MediaFastForward",
2955
- "MediaPause",
2956
- "MediaPlay",
2957
- "MediaPlayPause",
2958
- "MediaRecord",
2959
- "MediaRewind",
2960
- "MediaStop",
2961
- "MediaTrackNext",
2962
- "MediaTrackPrevious",
2963
- "AudioBalanceLeft",
2964
- "AudioBalanceRight",
2965
- "AudioBassBoostDown",
2966
- "AudioBassBoostToggle",
2967
- "AudioBassBoostUp",
2968
- "AudioFaderFront",
2969
- "AudioFaderRear",
2970
- "AudioSurroundModeNext",
2971
- "AudioTrebleDown",
2972
- "AudioTrebleUp",
2973
- "AudioVolumeDown",
2974
- "AudioVolumeMute",
2975
- "AudioVolumeUp",
2976
- "MicrophoneToggle",
2977
- "MicrophoneVolumeDown",
2978
- "MicrophoneVolumeMute",
2979
- "MicrophoneVolumeUp",
2980
- "TV",
2981
- "TV3DMode",
2982
- "TVAntennaCable",
2983
- "TVAudioDescription",
2984
- ];
2985
4067
  export { StableBrowser };
2986
4068
  //# sourceMappingURL=stable_browser.js.map