automation_model 1.0.488-dev → 1.0.488

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 (77) hide show
  1. package/README.md +133 -0
  2. package/lib/analyze_helper.js.map +1 -1
  3. package/lib/api.d.ts +2 -2
  4. package/lib/api.js +151 -120
  5. package/lib/api.js.map +1 -1
  6. package/lib/auto_page.d.ts +7 -2
  7. package/lib/auto_page.js +309 -29
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.d.ts +6 -3
  10. package/lib/browser_manager.js +194 -46
  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/check_performance.d.ts +1 -0
  16. package/lib/check_performance.js +57 -0
  17. package/lib/check_performance.js.map +1 -0
  18. package/lib/command_common.d.ts +5 -4
  19. package/lib/command_common.js +126 -21
  20. package/lib/command_common.js.map +1 -1
  21. package/lib/date_time.js.map +1 -1
  22. package/lib/drawRect.js.map +1 -1
  23. package/lib/environment.d.ts +1 -0
  24. package/lib/environment.js +1 -0
  25. package/lib/environment.js.map +1 -1
  26. package/lib/error-messages.d.ts +6 -0
  27. package/lib/error-messages.js +206 -0
  28. package/lib/error-messages.js.map +1 -0
  29. package/lib/file_checker.d.ts +1 -0
  30. package/lib/file_checker.js +172 -0
  31. package/lib/file_checker.js.map +1 -0
  32. package/lib/find_function.js.map +1 -1
  33. package/lib/generation_scripts.d.ts +4 -0
  34. package/lib/generation_scripts.js +2 -0
  35. package/lib/generation_scripts.js.map +1 -0
  36. package/lib/index.d.ts +3 -0
  37. package/lib/index.js +4 -0
  38. package/lib/index.js.map +1 -1
  39. package/lib/init_browser.d.ts +4 -3
  40. package/lib/init_browser.js +160 -83
  41. package/lib/init_browser.js.map +1 -1
  42. package/lib/locate_element.js +16 -14
  43. package/lib/locate_element.js.map +1 -1
  44. package/lib/locator.d.ts +37 -0
  45. package/lib/locator.js +172 -0
  46. package/lib/locator.js.map +1 -1
  47. package/lib/locator_log.d.ts +26 -0
  48. package/lib/locator_log.js +69 -0
  49. package/lib/locator_log.js.map +1 -0
  50. package/lib/network.d.ts +5 -0
  51. package/lib/network.js +494 -0
  52. package/lib/network.js.map +1 -0
  53. package/lib/route.d.ts +83 -0
  54. package/lib/route.js +691 -0
  55. package/lib/route.js.map +1 -0
  56. package/lib/scripts/axe.mini.js +24005 -0
  57. package/lib/snapshot_validation.d.ts +37 -0
  58. package/lib/snapshot_validation.js +360 -0
  59. package/lib/snapshot_validation.js.map +1 -0
  60. package/lib/stable_browser.d.ts +147 -47
  61. package/lib/stable_browser.js +2587 -828
  62. package/lib/stable_browser.js.map +1 -1
  63. package/lib/table.d.ts +15 -0
  64. package/lib/table.js +257 -0
  65. package/lib/table.js.map +1 -0
  66. package/lib/table_analyze.js.map +1 -1
  67. package/lib/table_helper.d.ts +19 -0
  68. package/lib/table_helper.js +130 -0
  69. package/lib/table_helper.js.map +1 -0
  70. package/lib/test_context.d.ts +6 -0
  71. package/lib/test_context.js +5 -0
  72. package/lib/test_context.js.map +1 -1
  73. package/lib/utils.d.ts +38 -3
  74. package/lib/utils.js +748 -36
  75. package/lib/utils.js.map +1 -1
  76. package/package.json +31 -13
  77. package/lib/axe/axe.mini.js +0 -12
@@ -1,4 +1,5 @@
1
1
  // @ts-nocheck
2
+ import { check_performance } from "./check_performance.js";
2
3
  import { expect } from "@playwright/test";
3
4
  import dayjs from "dayjs";
4
5
  import fs from "fs";
@@ -10,58 +11,102 @@ import { getDateTimeValue } from "./date_time.js";
10
11
  import drawRectangle from "./drawRect.js";
11
12
  //import { closeUnexpectedPopups } from "./popups.js";
12
13
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { maskValue, replaceWithLocalTestData } from "./utils.js";
14
+ import errorStackParser from "error-stack-parser";
15
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
14
16
  import csv from "csv-parser";
15
17
  import { Readable } from "node:stream";
16
18
  import readline from "readline";
17
- import { getContext } from "./init_browser.js";
19
+ import { getContext, refreshBrowser } from "./init_browser.js";
20
+ import { getTestData } from "./auto_page.js";
18
21
  import { locate_element } from "./locate_element.js";
19
- import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot } from "./command_common.js";
20
- const Types = {
22
+ import { randomUUID } from "crypto";
23
+ import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
24
+ import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
25
+ import { LocatorLog } from "./locator_log.js";
26
+ import axios from "axios";
27
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
28
+ import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
29
+ import { loadBrunoParams } from "./bruno.js";
30
+ import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
31
+ import { existsSync } from "node:fs";
32
+ export const Types = {
21
33
  CLICK: "click_element",
34
+ WAIT_ELEMENT: "wait_element",
22
35
  NAVIGATE: "navigate",
36
+ GO_BACK: "go_back",
37
+ GO_FORWARD: "go_forward",
23
38
  FILL: "fill_element",
24
- EXECUTE: "execute_page_method",
25
- OPEN: "open_environment",
39
+ EXECUTE: "execute_page_method", //
40
+ OPEN: "open_environment", //
26
41
  COMPLETE: "step_complete",
27
42
  ASK: "information_needed",
28
- GET_PAGE_STATUS: "get_page_status",
29
- CLICK_ROW_ACTION: "click_row_action",
43
+ GET_PAGE_STATUS: "get_page_status", ///
44
+ CLICK_ROW_ACTION: "click_row_action", //
30
45
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
46
+ VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
47
+ VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
31
48
  ANALYZE_TABLE: "analyze_table",
32
- SELECT: "select_combobox",
49
+ SELECT: "select_combobox", //
50
+ VERIFY_PROPERTY: "verify_element_property",
33
51
  VERIFY_PAGE_PATH: "verify_page_path",
52
+ VERIFY_PAGE_TITLE: "verify_page_title",
34
53
  TYPE_PRESS: "type_press",
35
54
  PRESS: "press_key",
36
55
  HOVER: "hover_element",
37
56
  CHECK: "check_element",
38
57
  UNCHECK: "uncheck_element",
39
58
  EXTRACT: "extract_attribute",
59
+ EXTRACT_PROPERTY: "extract_property",
40
60
  CLOSE_PAGE: "close_page",
61
+ TABLE_OPERATION: "table_operation",
41
62
  SET_DATE_TIME: "set_date_time",
42
63
  SET_VIEWPORT: "set_viewport",
43
64
  VERIFY_VISUAL: "verify_visual",
44
65
  LOAD_DATA: "load_data",
45
66
  SET_INPUT: "set_input",
67
+ WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
68
+ VERIFY_ATTRIBUTE: "verify_element_attribute",
69
+ VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
70
+ BRUNO: "bruno",
71
+ VERIFY_FILE_EXISTS: "verify_file_exists",
72
+ SET_INPUT_FILES: "set_input_files",
73
+ SNAPSHOT_VALIDATION: "snapshot_validation",
74
+ REPORT_COMMAND: "report_command",
75
+ STEP_COMPLETE: "step_complete",
76
+ SLEEP: "sleep",
77
+ CONDITIONAL_WAIT: "conditional_wait",
46
78
  };
47
79
  export const apps = {};
80
+ const formatElementName = (elementName) => {
81
+ return elementName ? JSON.stringify(elementName) : "element";
82
+ };
48
83
  class StableBrowser {
49
84
  browser;
50
85
  page;
51
86
  logger;
52
87
  context;
53
88
  world;
89
+ fastMode;
90
+ stepTags;
54
91
  project_path = null;
55
92
  webLogFile = null;
56
93
  networkLogger = null;
57
94
  configuration = null;
58
95
  appName = "main";
59
- constructor(browser, page, logger = null, context = null, world = null) {
96
+ tags = null;
97
+ isRecording = false;
98
+ initSnapshotTaken = false;
99
+ onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
100
+ // set to true if the step issue a report
101
+ inStepReport = false;
102
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
60
103
  this.browser = browser;
61
104
  this.page = page;
62
105
  this.logger = logger;
63
106
  this.context = context;
64
107
  this.world = world;
108
+ this.fastMode = fastMode;
109
+ this.stepTags = stepTags;
65
110
  if (!this.logger) {
66
111
  this.logger = console;
67
112
  }
@@ -90,20 +135,54 @@ class StableBrowser {
90
135
  context.pages = [this.page];
91
136
  const logFolder = path.join(this.project_path, "logs", "web");
92
137
  this.world = world;
138
+ if (this.configuration && this.configuration.fastMode === true) {
139
+ this.fastMode = true;
140
+ }
141
+ if (process.env.FAST_MODE === "true") {
142
+ // console.log("Fast mode enabled from environment variable");
143
+ this.fastMode = true;
144
+ }
145
+ if (process.env.FAST_MODE === "false") {
146
+ this.fastMode = false;
147
+ }
148
+ if (this.context) {
149
+ this.context.fastMode = this.fastMode;
150
+ }
93
151
  this.registerEventListeners(this.context);
152
+ registerNetworkEvents(this.world, this, this.context, this.page);
153
+ registerDownloadEvent(this.page, this.world, this.context);
94
154
  }
95
155
  registerEventListeners(context) {
96
156
  this.registerConsoleLogListener(this.page, context);
97
- this.registerRequestListener(this.page, context, this.webLogFile);
157
+ // this.registerRequestListener(this.page, context, this.webLogFile);
98
158
  if (!context.pageLoading) {
99
159
  context.pageLoading = { status: false };
100
160
  }
161
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
162
+ this.page.on("dialog", (dialog) => dialog.accept());
163
+ }
101
164
  context.playContext.on("page", async function (page) {
165
+ if (this.configuration && this.configuration.closePopups === true) {
166
+ console.log("close unexpected popups");
167
+ await page.close();
168
+ return;
169
+ }
102
170
  context.pageLoading.status = true;
103
171
  this.page = page;
172
+ try {
173
+ if (this.configuration && this.configuration.acceptDialog) {
174
+ await page.on("dialog", (dialog) => dialog.accept());
175
+ }
176
+ }
177
+ catch (error) {
178
+ console.error("Error on dialog accept registration", error);
179
+ }
104
180
  context.page = page;
105
181
  context.pages.push(page);
182
+ registerNetworkEvents(this.world, this, context, this.page);
183
+ registerDownloadEvent(this.page, this.world, context);
106
184
  page.on("close", async () => {
185
+ // return if browser context is already closed
107
186
  if (this.context && this.context.pages && this.context.pages.length > 1) {
108
187
  this.context.pages.pop();
109
188
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -113,7 +192,12 @@ class StableBrowser {
113
192
  console.log("Switched to page " + title);
114
193
  }
115
194
  catch (error) {
116
- console.error("Error on page close", error);
195
+ if (error?.message?.includes("Target page, context or browser has been closed")) {
196
+ // Ignore this error
197
+ }
198
+ else {
199
+ console.error("Error on page close", error);
200
+ }
117
201
  }
118
202
  }
119
203
  });
@@ -122,7 +206,12 @@ class StableBrowser {
122
206
  console.log("Switch page: " + (await page.title()));
123
207
  }
124
208
  catch (e) {
125
- this.logger.error("error on page load " + e);
209
+ if (e?.message?.includes("Target page, context or browser has been closed")) {
210
+ // Ignore this error
211
+ }
212
+ else {
213
+ this.logger.error("error on page load " + e);
214
+ }
126
215
  }
127
216
  context.pageLoading.status = false;
128
217
  }.bind(this));
@@ -134,7 +223,7 @@ class StableBrowser {
134
223
  }
135
224
  let newContextCreated = false;
136
225
  if (!apps[appName]) {
137
- let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this);
226
+ let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
138
227
  newContextCreated = true;
139
228
  apps[appName] = {
140
229
  context: newContext,
@@ -143,31 +232,41 @@ class StableBrowser {
143
232
  };
144
233
  }
145
234
  const tempContext = {};
146
- this._copyContext(this, tempContext);
147
- this._copyContext(apps[appName], this);
235
+ _copyContext(this, tempContext);
236
+ _copyContext(apps[appName], this);
148
237
  apps[this.appName] = tempContext;
149
238
  this.appName = appName;
150
239
  if (newContextCreated) {
151
240
  this.registerEventListeners(this.context);
152
241
  await this.goto(this.context.environment.baseUrl);
153
- await this.waitForPageLoad();
242
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
243
+ await this.waitForPageLoad();
244
+ }
154
245
  }
155
246
  }
156
- _copyContext(from, to) {
157
- to.browser = from.browser;
158
- to.page = from.page;
159
- to.context = from.context;
160
- }
161
- getWebLogFile(logFolder) {
162
- if (!fs.existsSync(logFolder)) {
163
- fs.mkdirSync(logFolder, { recursive: true });
247
+ async switchTab(tabTitleOrIndex) {
248
+ // first check if the tabNameOrIndex is a number
249
+ let index = parseInt(tabTitleOrIndex);
250
+ if (!isNaN(index)) {
251
+ if (index >= 0 && index < this.context.pages.length) {
252
+ this.page = this.context.pages[index];
253
+ this.context.page = this.page;
254
+ await this.page.bringToFront();
255
+ return;
256
+ }
164
257
  }
165
- let nextIndex = 1;
166
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
167
- nextIndex++;
258
+ // if the tabNameOrIndex is a string, find the tab by name
259
+ for (let i = 0; i < this.context.pages.length; i++) {
260
+ let page = this.context.pages[i];
261
+ let title = await page.title();
262
+ if (title.includes(tabTitleOrIndex)) {
263
+ this.page = page;
264
+ this.context.page = this.page;
265
+ await this.page.bringToFront();
266
+ return;
267
+ }
168
268
  }
169
- const fileName = nextIndex + ".json";
170
- return path.join(logFolder, fileName);
269
+ throw new Error("Tab not found: " + tabTitleOrIndex);
171
270
  }
172
271
  registerConsoleLogListener(page, context) {
173
272
  if (!this.context.webLogger) {
@@ -218,7 +317,7 @@ class StableBrowser {
218
317
  this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
219
318
  }
220
319
  catch (error) {
221
- console.error("Error in request listener", error);
320
+ // console.error("Error in request listener", error);
222
321
  context.networkLogger.push({
223
322
  error: "not able to listen",
224
323
  message: error.message,
@@ -232,55 +331,114 @@ class StableBrowser {
232
331
  // async closeUnexpectedPopups() {
233
332
  // await closeUnexpectedPopups(this.page);
234
333
  // }
235
- async goto(url) {
334
+ async goto(url, world = null, options = {}) {
335
+ if (!url) {
336
+ throw new Error("url is null, verify that the environment file is correct");
337
+ }
338
+ url = await this._replaceWithLocalData(url, this.world);
236
339
  if (!url.startsWith("http")) {
237
340
  url = "https://" + url;
238
341
  }
239
- await this.page.goto(url, {
240
- timeout: 60000,
241
- });
242
- }
243
- _fixUsingParams(text, _params) {
244
- if (!_params || typeof text !== "string") {
245
- return text;
342
+ const state = {
343
+ value: url,
344
+ world: world,
345
+ type: Types.NAVIGATE,
346
+ text: `Navigate Page to: ${url}`,
347
+ operation: "goto",
348
+ log: "***** navigate page to " + url + " *****\n",
349
+ info: {},
350
+ locate: false,
351
+ scroll: false,
352
+ screenshot: false,
353
+ highlight: false,
354
+ };
355
+ let timeout = 60000;
356
+ if (options && options["timeout"]) {
357
+ timeout = options["timeout"];
246
358
  }
247
- for (let key in _params) {
248
- let regValue = key;
249
- if (key.startsWith("_")) {
250
- // remove the _ prefix
251
- regValue = key.substring(1);
252
- }
253
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
359
+ try {
360
+ await _preCommand(state, this);
361
+ await this.page.goto(url, {
362
+ timeout: timeout,
363
+ });
364
+ await _screenshot(state, this);
365
+ }
366
+ catch (error) {
367
+ console.error("Error on goto", error);
368
+ _commandError(state, error, this);
369
+ }
370
+ finally {
371
+ await _commandFinally(state, this);
254
372
  }
255
- return text;
256
373
  }
257
- _fixLocatorUsingParams(locator, _params) {
258
- // check if not null
259
- if (!locator) {
260
- return locator;
374
+ async goBack(options, world = null) {
375
+ const state = {
376
+ value: "",
377
+ world: world,
378
+ type: Types.GO_BACK,
379
+ text: `Browser navigate back`,
380
+ operation: "goBack",
381
+ log: "***** navigate back *****\n",
382
+ info: {},
383
+ locate: false,
384
+ scroll: false,
385
+ screenshot: false,
386
+ highlight: false,
387
+ };
388
+ try {
389
+ await _preCommand(state, this);
390
+ await this.page.goBack({
391
+ waitUntil: "load",
392
+ });
393
+ await _screenshot(state, this);
394
+ }
395
+ catch (error) {
396
+ console.error("Error on goBack", error);
397
+ _commandError(state, error, this);
398
+ }
399
+ finally {
400
+ await _commandFinally(state, this);
261
401
  }
262
- // clone the locator
263
- locator = JSON.parse(JSON.stringify(locator));
264
- this.scanAndManipulate(locator, _params);
265
- return locator;
266
402
  }
267
- _isObject(value) {
268
- return value && typeof value === "object" && value.constructor === Object;
403
+ async goForward(options, world = null) {
404
+ const state = {
405
+ value: "",
406
+ world: world,
407
+ type: Types.GO_FORWARD,
408
+ text: `Browser navigate forward`,
409
+ operation: "goForward",
410
+ log: "***** navigate forward *****\n",
411
+ info: {},
412
+ locate: false,
413
+ scroll: false,
414
+ screenshot: false,
415
+ highlight: false,
416
+ };
417
+ try {
418
+ await _preCommand(state, this);
419
+ await this.page.goForward({
420
+ waitUntil: "load",
421
+ });
422
+ await _screenshot(state, this);
423
+ }
424
+ catch (error) {
425
+ console.error("Error on goForward", error);
426
+ _commandError(state, error, this);
427
+ }
428
+ finally {
429
+ await _commandFinally(state, this);
430
+ }
269
431
  }
270
- scanAndManipulate(currentObj, _params) {
271
- for (const key in currentObj) {
272
- if (typeof currentObj[key] === "string") {
273
- // Perform string manipulation
274
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
275
- }
276
- else if (this._isObject(currentObj[key])) {
277
- // Recursively scan nested objects
278
- this.scanAndManipulate(currentObj[key], _params);
432
+ async _getLocator(locator, scope, _params) {
433
+ locator = _fixLocatorUsingParams(locator, _params);
434
+ // locator = await this._replaceWithLocalData(locator);
435
+ for (let key in locator) {
436
+ if (typeof locator[key] !== "string")
437
+ continue;
438
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
439
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
279
440
  }
280
441
  }
281
- }
282
- _getLocator(locator, scope, _params) {
283
- locator = this._fixLocatorUsingParams(locator, _params);
284
442
  let locatorReturn;
285
443
  if (locator.role) {
286
444
  if (locator.role[1].nameReg) {
@@ -288,7 +446,7 @@ class StableBrowser {
288
446
  delete locator.role[1].nameReg;
289
447
  }
290
448
  // if (locator.role[1].name) {
291
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
449
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
292
450
  // }
293
451
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
294
452
  }
@@ -331,213 +489,171 @@ class StableBrowser {
331
489
  if (css && css.locator) {
332
490
  css = css.locator;
333
491
  }
334
- let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, false, _params);
492
+ let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
335
493
  if (result.elementCount === 0) {
336
494
  return;
337
495
  }
338
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
496
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
339
497
  // css climb to parent element
340
498
  const climbArray = [];
341
499
  for (let i = 0; i < climb; i++) {
342
500
  climbArray.push("..");
343
501
  }
344
502
  let climbXpath = "xpath=" + climbArray.join("/");
345
- return textElementCss + " >> " + climbXpath + " >> " + css;
346
- }
347
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
348
- //const stringifyText = JSON.stringify(text);
349
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
350
- function isParent(parent, child) {
351
- let currentNode = child.parentNode;
352
- while (currentNode !== null) {
353
- if (currentNode === parent) {
354
- return true;
355
- }
356
- currentNode = currentNode.parentNode;
357
- }
358
- return false;
359
- }
360
- document.isParent = isParent;
361
- function getRegex(str) {
362
- const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
363
- if (!match) {
364
- return null;
365
- }
366
- let [_, pattern, flags] = match;
367
- return new RegExp(pattern, flags);
368
- }
369
- document.getRegex = getRegex;
370
- function collectAllShadowDomElements(element, result = []) {
371
- // Check and add the element if it has a shadow root
372
- if (element.shadowRoot) {
373
- result.push(element);
374
- // Also search within the shadow root
375
- document.collectAllShadowDomElements(element.shadowRoot, result);
376
- }
377
- // Iterate over child nodes
378
- element.childNodes.forEach((child) => {
379
- // Recursively call the function for each child node
380
- document.collectAllShadowDomElements(child, result);
381
- });
382
- return result;
383
- }
384
- document.collectAllShadowDomElements = collectAllShadowDomElements;
385
- if (!tag) {
386
- tag = "*";
387
- }
388
- let regexpSearch = document.getRegex(text);
389
- if (regexpSearch) {
390
- regex = true;
391
- }
392
- let elements = Array.from(document.querySelectorAll(tag));
393
- let shadowHosts = [];
394
- document.collectAllShadowDomElements(document, shadowHosts);
395
- for (let i = 0; i < shadowHosts.length; i++) {
396
- let shadowElement = shadowHosts[i].shadowRoot;
397
- if (!shadowElement) {
398
- console.log("shadowElement is null, for host " + shadowHosts[i]);
399
- continue;
400
- }
401
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
402
- elements = elements.concat(shadowElements);
403
- }
404
- let randomToken = null;
405
- const foundElements = [];
406
- if (regex) {
407
- if (!regexpSearch) {
408
- regexpSearch = new RegExp(text, "im");
409
- }
410
- for (let i = 0; i < elements.length; i++) {
411
- const element = elements[i];
412
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
413
- (element.value && regexpSearch.test(element.value))) {
414
- foundElements.push(element);
415
- }
416
- }
417
- }
418
- else {
419
- text = text.trim();
420
- for (let i = 0; i < elements.length; i++) {
421
- const element = elements[i];
422
- if (partial) {
423
- if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
424
- (element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
425
- foundElements.push(element);
426
- }
427
- }
428
- else {
429
- if ((element.innerText && element.innerText.trim() === text) ||
430
- (element.value && element.value === text)) {
431
- foundElements.push(element);
432
- }
433
- }
434
- }
435
- }
436
- let noChildElements = [];
437
- for (let i = 0; i < foundElements.length; i++) {
438
- let element = foundElements[i];
439
- let hasChild = false;
440
- for (let j = 0; j < foundElements.length; j++) {
441
- if (i === j) {
442
- continue;
443
- }
444
- if (isParent(element, foundElements[j])) {
445
- hasChild = true;
446
- break;
503
+ let resultCss = textElementCss + " >> " + climbXpath;
504
+ if (css) {
505
+ resultCss = resultCss + " >> " + css;
506
+ }
507
+ return resultCss;
508
+ }
509
+ async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
510
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
511
+ const locator = scope.locator(query);
512
+ const count = await locator.count();
513
+ if (!tag1) {
514
+ tag1 = "*";
515
+ }
516
+ const randomToken = Math.random().toString(36).substring(7);
517
+ let tagCount = 0;
518
+ for (let i = 0; i < count; i++) {
519
+ const element = locator.nth(i);
520
+ // check if the tag matches
521
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
522
+ if (!tag.startsWith("*")) {
523
+ if (el.tagName.toLowerCase() !== tag) {
524
+ return false;
447
525
  }
448
526
  }
449
- if (!hasChild) {
450
- noChildElements.push(element);
527
+ if (!el.setAttribute) {
528
+ el = el.parentElement;
451
529
  }
530
+ el.setAttribute("data-blinq-id-" + randomToken, "");
531
+ return true;
532
+ }, [tag1, randomToken]))) {
533
+ continue;
452
534
  }
453
- let elementCount = 0;
454
- if (noChildElements.length > 0) {
455
- for (let i = 0; i < noChildElements.length; i++) {
456
- if (randomToken === null) {
457
- randomToken = Math.random().toString(36).substring(7);
458
- }
459
- let element = noChildElements[i];
460
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
461
- elementCount++;
462
- }
463
- }
464
- return { elementCount: elementCount, randomToken: randomToken };
465
- }, [text1, tag1, regex1, partial1]);
535
+ tagCount++;
536
+ }
537
+ return { elementCount: tagCount, randomToken };
466
538
  }
467
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
539
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
540
+ if (!info) {
541
+ info = {};
542
+ }
543
+ if (!info.failCause) {
544
+ info.failCause = {};
545
+ }
546
+ if (!info.log) {
547
+ info.log = "";
548
+ info.locatorLog = new LocatorLog(selectorHierarchy);
549
+ }
468
550
  let locatorSearch = selectorHierarchy[index];
469
551
  try {
470
- locatorSearch = JSON.parse(this._fixUsingParams(JSON.stringify(locatorSearch), _params));
552
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
471
553
  }
472
554
  catch (e) {
473
555
  console.error(e);
474
556
  }
557
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
475
558
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
476
559
  let locator = null;
477
560
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
478
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
561
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
562
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
479
563
  if (!locatorString) {
564
+ info.failCause.textNotFound = true;
565
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
480
566
  return;
481
567
  }
482
- locator = this._getLocator({ css: locatorString }, scope, _params);
568
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
483
569
  }
484
570
  else if (locatorSearch.text) {
485
- let result = await this._locateElementByText(scope, this._fixUsingParams(locatorSearch.text, _params), locatorSearch.tag, false, locatorSearch.partial === true, _params);
571
+ let text = _fixUsingParams(locatorSearch.text, _params);
572
+ let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
486
573
  if (result.elementCount === 0) {
574
+ info.failCause.textNotFound = true;
575
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
487
576
  return;
488
577
  }
489
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
578
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
490
579
  if (locatorSearch.childCss) {
491
580
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
492
581
  }
493
- locator = this._getLocator(locatorSearch, scope, _params);
582
+ locator = await this._getLocator(locatorSearch, scope, _params);
494
583
  }
495
584
  else {
496
- locator = this._getLocator(locatorSearch, scope, _params);
585
+ locator = await this._getLocator(locatorSearch, scope, _params);
497
586
  }
498
587
  // let cssHref = false;
499
588
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
500
589
  // cssHref = true;
501
590
  // }
502
591
  let count = await locator.count();
592
+ if (count > 0 && !info.failCause.count) {
593
+ info.failCause.count = count;
594
+ }
503
595
  //info.log += "total elements found " + count + "\n";
504
596
  //let visibleCount = 0;
505
597
  let visibleLocator = null;
506
- if (locatorSearch.index && locatorSearch.index < count) {
598
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
507
599
  foundLocators.push(locator.nth(locatorSearch.index));
600
+ if (info.locatorLog) {
601
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
602
+ }
508
603
  return;
509
604
  }
605
+ if (info.locatorLog && count === 0 && logErrors) {
606
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
607
+ }
510
608
  for (let j = 0; j < count; j++) {
511
609
  let visible = await locator.nth(j).isVisible();
512
610
  const enabled = await locator.nth(j).isEnabled();
513
611
  if (!visibleOnly) {
514
612
  visible = true;
515
613
  }
516
- if (visible && enabled) {
614
+ if (visible && (allowDisabled || enabled)) {
517
615
  foundLocators.push(locator.nth(j));
616
+ if (info.locatorLog) {
617
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
618
+ }
518
619
  }
519
- else {
620
+ else if (logErrors) {
621
+ info.failCause.visible = visible;
622
+ info.failCause.enabled = enabled;
520
623
  if (!info.printMessages) {
521
624
  info.printMessages = {};
522
625
  }
626
+ if (info.locatorLog && !visible) {
627
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
628
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
629
+ }
630
+ if (info.locatorLog && !enabled) {
631
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
632
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
633
+ }
523
634
  if (!info.printMessages[j.toString()]) {
524
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
635
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
525
636
  info.printMessages[j.toString()] = true;
526
637
  }
527
638
  }
528
639
  }
529
640
  }
530
641
  async closeUnexpectedPopups(info, _params) {
642
+ if (!info) {
643
+ info = {};
644
+ info.failCause = {};
645
+ info.log = "";
646
+ }
531
647
  if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
532
648
  if (!info) {
533
649
  info = {};
534
650
  }
535
- info.log += "scan for popup handlers" + "\n";
651
+ //info.log += "scan for popup handlers" + "\n";
536
652
  const handlerGroup = [];
537
653
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
538
654
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
539
655
  }
540
- const scopes = [this.page, ...this.page.frames()];
656
+ const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
541
657
  let result = null;
542
658
  let scope = null;
543
659
  for (let i = 0; i < scopes.length; i++) {
@@ -559,33 +675,218 @@ class StableBrowser {
559
675
  }
560
676
  if (result.foundElements.length > 0) {
561
677
  let dialogCloseLocator = result.foundElements[0].locator;
562
- await dialogCloseLocator.click();
563
- // wait for the dialog to close
564
- await dialogCloseLocator.waitFor({ state: "hidden" });
678
+ try {
679
+ await scope?.evaluate(() => {
680
+ window.__isClosingPopups = true;
681
+ });
682
+ await dialogCloseLocator.click();
683
+ // wait for the dialog to close
684
+ await dialogCloseLocator.waitFor({ state: "hidden" });
685
+ }
686
+ catch (e) {
687
+ }
688
+ finally {
689
+ await scope?.evaluate(() => {
690
+ window.__isClosingPopups = false;
691
+ });
692
+ }
565
693
  return { rerun: true };
566
694
  }
567
695
  }
568
696
  }
569
697
  return { rerun: false };
570
698
  }
571
- async _locate(selectors, info, _params, timeout) {
699
+ getFilePath() {
700
+ const stackFrames = errorStackParser.parse(new Error());
701
+ const stackFrame = stackFrames.findLast((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
702
+ // return stackFrame?.fileName || null;
703
+ const filepath = stackFrame?.fileName;
704
+ if (filepath) {
705
+ let jsonFilePath = filepath.replace(".mjs", ".json");
706
+ if (existsSync(jsonFilePath)) {
707
+ return jsonFilePath;
708
+ }
709
+ const config = this.configuration ?? {};
710
+ if (!config?.locatorsMetadataDir) {
711
+ config.locatorsMetadataDir = "features/step_definitions/locators";
712
+ }
713
+ if (config && config.locatorsMetadataDir) {
714
+ jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
715
+ }
716
+ if (existsSync(jsonFilePath)) {
717
+ return jsonFilePath;
718
+ }
719
+ return null;
720
+ }
721
+ return null;
722
+ }
723
+ getFullElementLocators(selectors, filePath) {
724
+ if (!filePath || !existsSync(filePath)) {
725
+ return null;
726
+ }
727
+ const content = fs.readFileSync(filePath, "utf8");
728
+ try {
729
+ const allElements = JSON.parse(content);
730
+ const element_key = selectors?.element_key;
731
+ if (element_key && allElements[element_key]) {
732
+ return allElements[element_key];
733
+ }
734
+ for (const elementKey in allElements) {
735
+ const element = allElements[elementKey];
736
+ let foundStrategy = null;
737
+ for (const key in element) {
738
+ if (key === "strategy") {
739
+ continue;
740
+ }
741
+ const locators = element[key];
742
+ if (!locators || !locators.length) {
743
+ continue;
744
+ }
745
+ for (const locator of locators) {
746
+ delete locator.score;
747
+ }
748
+ if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
749
+ foundStrategy = key;
750
+ break;
751
+ }
752
+ }
753
+ if (foundStrategy) {
754
+ return element;
755
+ }
756
+ }
757
+ }
758
+ catch (error) {
759
+ console.error("Error parsing locators from file: " + filePath, error);
760
+ }
761
+ return null;
762
+ }
763
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
572
764
  if (!timeout) {
573
765
  timeout = 30000;
574
766
  }
767
+ let element = null;
768
+ let allStrategyLocators = null;
769
+ let selectedStrategy = null;
770
+ if (this.tryAllStrategies) {
771
+ allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
772
+ selectedStrategy = allStrategyLocators?.strategy;
773
+ }
575
774
  for (let i = 0; i < 3; i++) {
576
775
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
577
776
  for (let j = 0; j < selectors.locators.length; j++) {
578
777
  let selector = selectors.locators[j];
579
778
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
580
779
  }
581
- let element = await this._locate_internal(selectors, info, _params, timeout);
780
+ if (this.tryAllStrategies && selectedStrategy) {
781
+ const strategyLocators = allStrategyLocators[selectedStrategy];
782
+ let err;
783
+ if (strategyLocators && strategyLocators.length) {
784
+ try {
785
+ selectors.locators = strategyLocators;
786
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
787
+ info.selectedStrategy = selectedStrategy;
788
+ info.log += "element found using strategy " + selectedStrategy + "\n";
789
+ }
790
+ catch (error) {
791
+ err = error;
792
+ }
793
+ }
794
+ if (!element) {
795
+ for (const key in allStrategyLocators) {
796
+ if (key === "strategy" || key === selectedStrategy) {
797
+ continue;
798
+ }
799
+ const strategyLocators = allStrategyLocators[key];
800
+ if (strategyLocators && strategyLocators.length) {
801
+ try {
802
+ info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
803
+ selectors.locators = strategyLocators;
804
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
805
+ err = null;
806
+ info.selectedStrategy = key;
807
+ info.log += "element found using strategy " + key + "\n";
808
+ break;
809
+ }
810
+ catch (error) {
811
+ err = error;
812
+ }
813
+ }
814
+ }
815
+ }
816
+ if (err) {
817
+ throw err;
818
+ }
819
+ }
820
+ else {
821
+ element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
822
+ }
582
823
  if (!element.rerun) {
583
- return element;
824
+ let newElementSelector = "";
825
+ if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
826
+ const cssSelector = await element.evaluate((el) => {
827
+ function getCssSelector(el) {
828
+ if (!el || el.nodeType !== 1 || el === document.body)
829
+ return el.tagName.toLowerCase();
830
+ const parent = el.parentElement;
831
+ const tag = el.tagName.toLowerCase();
832
+ // Find the index of the element among its siblings of the same tag
833
+ let index = 1;
834
+ for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
835
+ if (sibling.tagName === el.tagName) {
836
+ index++;
837
+ }
838
+ }
839
+ // Use nth-child if necessary (i.e., if there's more than one of the same tag)
840
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
841
+ const needsNthChild = siblings.length > 1;
842
+ const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
843
+ return getCssSelector(parent) + " > " + selector;
844
+ }
845
+ const cssSelector = getCssSelector(el);
846
+ return cssSelector;
847
+ });
848
+ newElementSelector = cssSelector;
849
+ }
850
+ else {
851
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
852
+ if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
853
+ const dataAttribute = "data-blinq-id";
854
+ await element.evaluate((el, [dataAttribute, randomToken]) => {
855
+ el.setAttribute(dataAttribute, randomToken);
856
+ }, [dataAttribute, randomToken]);
857
+ newElementSelector = `[${dataAttribute}="${randomToken}"]`;
858
+ }
859
+ else {
860
+ // the default case just return the located element
861
+ // will not work for click and type if the locator is placeholder and the placeholder change due to the click event
862
+ return element;
863
+ }
864
+ }
865
+ const scope = element._frame ?? element.page();
866
+ let prefixSelector = "";
867
+ const frameControlSelector = " >> internal:control=enter-frame";
868
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
869
+ if (frameSelectorIndex !== -1) {
870
+ // remove everything after the >> internal:control=enter-frame
871
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
872
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
873
+ }
874
+ // if (element?._frame?._selector) {
875
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
876
+ // }
877
+ const newSelector = prefixSelector + newElementSelector;
878
+ return scope.locator(newSelector).first();
584
879
  }
585
880
  }
586
881
  throw new Error("unable to locate element " + JSON.stringify(selectors));
587
882
  }
588
- async _findFrameScope(selectors, timeout = 30000) {
883
+ async _findFrameScope(selectors, timeout = 30000, info) {
884
+ if (!info) {
885
+ info = {};
886
+ info.failCause = {};
887
+ info.log = "";
888
+ }
889
+ let startTime = Date.now();
589
890
  let scope = this.page;
590
891
  if (selectors.frame) {
591
892
  return selectors.frame;
@@ -595,7 +896,7 @@ class StableBrowser {
595
896
  for (let i = 0; i < frame.selectors.length; i++) {
596
897
  let frameLocator = frame.selectors[i];
597
898
  if (frameLocator.css) {
598
- let testframescope = framescope.frameLocator(frameLocator.css);
899
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
599
900
  if (frameLocator.index) {
600
901
  testframescope = framescope.nth(frameLocator.index);
601
902
  }
@@ -607,7 +908,7 @@ class StableBrowser {
607
908
  break;
608
909
  }
609
910
  catch (error) {
610
- console.error("frame not found " + frameLocator.css);
911
+ // console.error("frame not found " + frameLocator.css);
611
912
  }
612
913
  }
613
914
  }
@@ -616,9 +917,11 @@ class StableBrowser {
616
917
  }
617
918
  return framescope;
618
919
  };
920
+ let fLocator = null;
619
921
  while (true) {
620
922
  let frameFound = false;
621
923
  if (selectors.nestFrmLoc) {
924
+ fLocator = selectors.nestFrmLoc;
622
925
  scope = await findFrame(selectors.nestFrmLoc, scope);
623
926
  frameFound = true;
624
927
  break;
@@ -627,6 +930,7 @@ class StableBrowser {
627
930
  for (let i = 0; i < selectors.frameLocators.length; i++) {
628
931
  let frameLocator = selectors.frameLocators[i];
629
932
  if (frameLocator.css) {
933
+ fLocator = frameLocator.css;
630
934
  scope = scope.frameLocator(frameLocator.css);
631
935
  frameFound = true;
632
936
  break;
@@ -634,16 +938,25 @@ class StableBrowser {
634
938
  }
635
939
  }
636
940
  if (!frameFound && selectors.iframe_src) {
941
+ fLocator = selectors.iframe_src;
637
942
  scope = this.page.frame({ url: selectors.iframe_src });
638
943
  }
639
944
  if (!scope) {
640
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
641
- if (performance.now() - startTime > timeout) {
945
+ if (info && info.locatorLog) {
946
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
947
+ }
948
+ //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
949
+ if (Date.now() - startTime > timeout) {
950
+ info.failCause.iframeNotFound = true;
951
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
642
952
  throw new Error("unable to locate iframe " + selectors.iframe_src);
643
953
  }
644
954
  await new Promise((resolve) => setTimeout(resolve, 1000));
645
955
  }
646
956
  else {
957
+ if (info && info.locatorLog) {
958
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
959
+ }
647
960
  break;
648
961
  }
649
962
  }
@@ -653,20 +966,35 @@ class StableBrowser {
653
966
  }
654
967
  return scope;
655
968
  }
656
- async _getDocumentBody(selectors, timeout = 30000) {
657
- let scope = await this._findFrameScope(selectors, timeout);
969
+ async _getDocumentBody(selectors, timeout = 30000, info) {
970
+ let scope = await this._findFrameScope(selectors, timeout, info);
658
971
  return scope.evaluate(() => {
659
972
  var bodyContent = document.body.innerHTML;
660
973
  return bodyContent;
661
974
  });
662
975
  }
663
- async _locate_internal(selectors, info, _params, timeout = 30000) {
976
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
977
+ if (selectors.locators && Array.isArray(selectors.locators)) {
978
+ selectors.locators.forEach((locator) => {
979
+ locator.index = locator.index ?? 0;
980
+ locator.visible = locator.visible ?? true;
981
+ if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
982
+ locator.css = locator.css + " >> visible=true";
983
+ }
984
+ });
985
+ }
986
+ if (!info) {
987
+ info = {};
988
+ info.failCause = {};
989
+ info.log = "";
990
+ info.locatorLog = new LocatorLog(selectors);
991
+ }
664
992
  let highPriorityTimeout = 5000;
665
993
  let visibleOnlyTimeout = 6000;
666
- let startTime = performance.now();
994
+ let startTime = Date.now();
667
995
  let locatorsCount = 0;
996
+ let lazy_scroll = false;
668
997
  //let arrayMode = Array.isArray(selectors);
669
- let scope = await this._findFrameScope(selectors, timeout);
670
998
  let selectorsLocators = null;
671
999
  selectorsLocators = selectors.locators;
672
1000
  // group selectors by priority
@@ -694,6 +1022,7 @@ class StableBrowser {
694
1022
  let highPriorityOnly = true;
695
1023
  let visibleOnly = true;
696
1024
  while (true) {
1025
+ let scope = await this._findFrameScope(selectors, timeout, info);
697
1026
  locatorsCount = 0;
698
1027
  let result = [];
699
1028
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -702,18 +1031,13 @@ class StableBrowser {
702
1031
  }
703
1032
  // info.log += "scanning locators in priority 1" + "\n";
704
1033
  let onlyPriority3 = selectorsLocators[0].priority === 3;
705
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
1034
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
706
1035
  if (result.foundElements.length === 0) {
707
1036
  // info.log += "scanning locators in priority 2" + "\n";
708
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
1037
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
709
1038
  }
710
- if (result.foundElements.length === 0 && onlyPriority3) {
711
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
712
- }
713
- else {
714
- if (result.foundElements.length === 0 && !highPriorityOnly) {
715
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
716
- }
1039
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
1040
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
717
1041
  }
718
1042
  let foundElements = result.foundElements;
719
1043
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -753,24 +1077,43 @@ class StableBrowser {
753
1077
  return maxCountElement.locator;
754
1078
  }
755
1079
  }
756
- if (performance.now() - startTime > timeout) {
1080
+ if (Date.now() - startTime > timeout) {
757
1081
  break;
758
1082
  }
759
- if (performance.now() - startTime > highPriorityTimeout) {
760
- info.log += "high priority timeout, will try all elements" + "\n";
1083
+ if (Date.now() - startTime > highPriorityTimeout) {
1084
+ //info.log += "high priority timeout, will try all elements" + "\n";
761
1085
  highPriorityOnly = false;
1086
+ if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
1087
+ lazy_scroll = true;
1088
+ await scrollPageToLoadLazyElements(this.page);
1089
+ }
762
1090
  }
763
- if (performance.now() - startTime > visibleOnlyTimeout) {
764
- info.log += "visible only timeout, will try all elements" + "\n";
1091
+ if (Date.now() - startTime > visibleOnlyTimeout) {
1092
+ //info.log += "visible only timeout, will try all elements" + "\n";
765
1093
  visibleOnly = false;
766
1094
  }
767
1095
  await new Promise((resolve) => setTimeout(resolve, 1000));
1096
+ // sheck of more of half of the timeout has passed
1097
+ if (Date.now() - startTime > timeout / 2) {
1098
+ highPriorityOnly = false;
1099
+ visibleOnly = false;
1100
+ }
768
1101
  }
769
1102
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
770
- info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
1103
+ // if (info.locatorLog) {
1104
+ // const lines = info.locatorLog.toString().split("\n");
1105
+ // for (let line of lines) {
1106
+ // this.logger.debug(line);
1107
+ // }
1108
+ // }
1109
+ //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
1110
+ info.failCause.locatorNotFound = true;
1111
+ if (!info?.failCause?.lastError) {
1112
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
1113
+ }
771
1114
  throw new Error("failed to locate first element no elements found, " + info.log);
772
1115
  }
773
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
1116
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
774
1117
  let foundElements = [];
775
1118
  const result = {
776
1119
  foundElements: foundElements,
@@ -778,31 +1121,88 @@ class StableBrowser {
778
1121
  for (let i = 0; i < locatorsGroup.length; i++) {
779
1122
  let foundLocators = [];
780
1123
  try {
781
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
1124
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
782
1125
  }
783
1126
  catch (e) {
784
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
785
- this.logger.debug(e);
1127
+ // this call can fail it the browser is navigating
1128
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
1129
+ // this.logger.debug(e);
786
1130
  foundLocators = [];
787
1131
  try {
788
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
1132
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
789
1133
  }
790
1134
  catch (e) {
791
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
1135
+ if (logErrors) {
1136
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
1137
+ }
792
1138
  }
793
1139
  }
794
1140
  if (foundLocators.length === 1) {
1141
+ let box = null;
1142
+ if (!this.onlyFailuresScreenshot) {
1143
+ box = await foundLocators[0].boundingBox();
1144
+ }
795
1145
  result.foundElements.push({
796
1146
  locator: foundLocators[0],
797
- box: await foundLocators[0].boundingBox(),
1147
+ box: box,
798
1148
  unique: true,
799
1149
  });
800
1150
  result.locatorIndex = i;
801
1151
  }
1152
+ if (foundLocators.length > 1) {
1153
+ // remove elements that consume the same space with 10 pixels tolerance
1154
+ const boxes = [];
1155
+ for (let j = 0; j < foundLocators.length; j++) {
1156
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
1157
+ }
1158
+ for (let j = 0; j < boxes.length; j++) {
1159
+ for (let k = 0; k < boxes.length; k++) {
1160
+ if (j === k) {
1161
+ continue;
1162
+ }
1163
+ // check if x, y, width, height are the same with 10 pixels tolerance
1164
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
1165
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
1166
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
1167
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
1168
+ // as the element is not unique, will remove it
1169
+ boxes.splice(k, 1);
1170
+ k--;
1171
+ }
1172
+ }
1173
+ }
1174
+ if (boxes.length === 1) {
1175
+ result.foundElements.push({
1176
+ locator: boxes[0].locator.first(),
1177
+ box: boxes[0].box,
1178
+ unique: true,
1179
+ });
1180
+ result.locatorIndex = i;
1181
+ }
1182
+ else if (logErrors) {
1183
+ info.failCause.foundMultiple = true;
1184
+ if (info.locatorLog) {
1185
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
1186
+ }
1187
+ }
1188
+ }
802
1189
  }
803
1190
  return result;
804
1191
  }
805
1192
  async simpleClick(elementDescription, _params, options = {}, world = null) {
1193
+ const state = {
1194
+ locate: false,
1195
+ scroll: false,
1196
+ highlight: false,
1197
+ _params,
1198
+ options,
1199
+ world,
1200
+ type: Types.CLICK,
1201
+ text: "Click element",
1202
+ operation: "simpleClick",
1203
+ log: "***** click on " + elementDescription + " *****\n",
1204
+ };
1205
+ _preCommand(state, this);
806
1206
  const startTime = Date.now();
807
1207
  let timeout = 30000;
808
1208
  if (options && options.timeout) {
@@ -826,13 +1226,32 @@ class StableBrowser {
826
1226
  }
827
1227
  catch (e) {
828
1228
  if (performance.now() - startTime > timeout) {
829
- throw e;
1229
+ // throw e;
1230
+ try {
1231
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1232
+ }
1233
+ finally {
1234
+ await _commandFinally(state, this);
1235
+ }
830
1236
  }
831
1237
  }
832
1238
  await new Promise((resolve) => setTimeout(resolve, 3000));
833
1239
  }
834
1240
  }
835
1241
  async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
1242
+ const state = {
1243
+ locate: false,
1244
+ scroll: false,
1245
+ highlight: false,
1246
+ _params,
1247
+ options,
1248
+ world,
1249
+ type: Types.FILL,
1250
+ text: "Fill element",
1251
+ operation: "simpleClickType",
1252
+ log: "***** click type on " + elementDescription + " *****\n",
1253
+ };
1254
+ _preCommand(state, this);
836
1255
  const startTime = Date.now();
837
1256
  let timeout = 30000;
838
1257
  if (options && options.timeout) {
@@ -856,7 +1275,13 @@ class StableBrowser {
856
1275
  }
857
1276
  catch (e) {
858
1277
  if (performance.now() - startTime > timeout) {
859
- throw e;
1278
+ // throw e;
1279
+ try {
1280
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1281
+ }
1282
+ finally {
1283
+ await _commandFinally(state, this);
1284
+ }
860
1285
  }
861
1286
  }
862
1287
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -869,34 +1294,74 @@ class StableBrowser {
869
1294
  options,
870
1295
  world,
871
1296
  text: "Click element",
1297
+ _text: "Click on " + selectors.element_name,
872
1298
  type: Types.CLICK,
873
1299
  operation: "click",
874
1300
  log: "***** click on " + selectors.element_name + " *****\n",
875
1301
  };
1302
+ check_performance("click_all ***", this.context, true);
1303
+ let stepFastMode = this.stepTags.includes("fast-mode");
1304
+ if (stepFastMode) {
1305
+ state.onlyFailuresScreenshot = true;
1306
+ state.scroll = false;
1307
+ state.highlight = false;
1308
+ }
876
1309
  try {
1310
+ check_performance("click_preCommand", this.context, true);
877
1311
  await _preCommand(state, this);
878
- if (state.options && state.options.context) {
879
- state.selectors.locators[0].text = state.options.context;
880
- }
881
- try {
882
- await state.element.click();
883
- await new Promise((resolve) => setTimeout(resolve, 1000));
884
- }
885
- catch (e) {
886
- // await this.closeUnexpectedPopups();
887
- state.element = await this._locate(selectors, state.info, _params);
888
- await state.element.dispatchEvent("click");
889
- await new Promise((resolve) => setTimeout(resolve, 1000));
1312
+ check_performance("click_preCommand", this.context, false);
1313
+ await performAction("click", state.element, options, this, state, _params);
1314
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1315
+ check_performance("click_waitForPageLoad", this.context, true);
1316
+ await this.waitForPageLoad({ noSleep: true });
1317
+ check_performance("click_waitForPageLoad", this.context, false);
890
1318
  }
891
- await this.waitForPageLoad();
892
1319
  return state.info;
893
1320
  }
894
1321
  catch (e) {
895
1322
  await _commandError(state, e, this);
896
1323
  }
897
1324
  finally {
898
- _commandFinally(state, this);
1325
+ check_performance("click_commandFinally", this.context, true);
1326
+ await _commandFinally(state, this);
1327
+ check_performance("click_commandFinally", this.context, false);
1328
+ check_performance("click_all ***", this.context, false);
1329
+ if (this.context.profile) {
1330
+ console.log(JSON.stringify(this.context.profile, null, 2));
1331
+ }
1332
+ }
1333
+ }
1334
+ async waitForElement(selectors, _params, options = {}, world = null) {
1335
+ const timeout = this._getFindElementTimeout(options);
1336
+ const state = {
1337
+ selectors,
1338
+ _params,
1339
+ options,
1340
+ world,
1341
+ text: "Wait for element",
1342
+ _text: "Wait for " + selectors.element_name,
1343
+ type: Types.WAIT_ELEMENT,
1344
+ operation: "waitForElement",
1345
+ log: "***** wait for " + selectors.element_name + " *****\n",
1346
+ };
1347
+ let found = false;
1348
+ try {
1349
+ await _preCommand(state, this);
1350
+ // if (state.options && state.options.context) {
1351
+ // state.selectors.locators[0].text = state.options.context;
1352
+ // }
1353
+ await state.element.waitFor({ timeout: timeout });
1354
+ found = true;
1355
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1356
+ }
1357
+ catch (e) {
1358
+ console.error("Error on waitForElement", e);
1359
+ // await _commandError(state, e, this);
1360
+ }
1361
+ finally {
1362
+ await _commandFinally(state, this);
899
1363
  }
1364
+ return found;
900
1365
  }
901
1366
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
902
1367
  const state = {
@@ -906,6 +1371,7 @@ class StableBrowser {
906
1371
  world,
907
1372
  type: checked ? Types.CHECK : Types.UNCHECK,
908
1373
  text: checked ? `Check element` : `Uncheck element`,
1374
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
909
1375
  operation: "setCheck",
910
1376
  log: "***** check " + selectors.element_name + " *****\n",
911
1377
  };
@@ -915,30 +1381,53 @@ class StableBrowser {
915
1381
  // let element = await this._locate(selectors, info, _params);
916
1382
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
917
1383
  try {
918
- // await this._highlightElements(element);
919
- await state.element.setChecked(checked);
1384
+ // if (world && world.screenshot && !world.screenshotPath) {
1385
+ // console.log(`Highlighting while running from recorder`);
1386
+ await this._highlightElements(state.element);
1387
+ await state.element.setChecked(checked, { timeout: 2000 });
920
1388
  await new Promise((resolve) => setTimeout(resolve, 1000));
1389
+ // await this._unHighlightElements(element);
1390
+ // }
1391
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1392
+ // await this._unHighlightElements(element);
921
1393
  }
922
1394
  catch (e) {
923
1395
  if (e.message && e.message.includes("did not change its state")) {
924
1396
  this.logger.info("element did not change its state, ignoring...");
925
1397
  }
926
1398
  else {
1399
+ await new Promise((resolve) => setTimeout(resolve, 1000));
927
1400
  //await this.closeUnexpectedPopups();
928
1401
  state.info.log += "setCheck failed, will try again" + "\n";
929
- state.element = await this._locate(selectors, state.info, _params);
930
- await state.element.setChecked(checked, { timeout: 5000, force: true });
931
- await new Promise((resolve) => setTimeout(resolve, 1000));
1402
+ state.element_found = false;
1403
+ try {
1404
+ state.element = await this._locate(selectors, state.info, _params, 100);
1405
+ state.element_found = true;
1406
+ // check the check state
1407
+ }
1408
+ catch (error) {
1409
+ // element dismissed
1410
+ }
1411
+ if (state.element_found) {
1412
+ const isChecked = await state.element.isChecked();
1413
+ if (isChecked !== checked) {
1414
+ // perform click
1415
+ await state.element.click({ timeout: 2000, force: true });
1416
+ }
1417
+ else {
1418
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1419
+ }
1420
+ }
932
1421
  }
933
1422
  }
934
- await this.waitForPageLoad();
1423
+ //await this.waitForPageLoad();
935
1424
  return state.info;
936
1425
  }
937
1426
  catch (e) {
938
1427
  await _commandError(state, e, this);
939
1428
  }
940
1429
  finally {
941
- _commandFinally(state, this);
1430
+ await _commandFinally(state, this);
942
1431
  }
943
1432
  }
944
1433
  async hover(selectors, _params, options = {}, world = null) {
@@ -949,31 +1438,22 @@ class StableBrowser {
949
1438
  world,
950
1439
  type: Types.HOVER,
951
1440
  text: `Hover element`,
1441
+ _text: `Hover on ${selectors.element_name}`,
952
1442
  operation: "hover",
953
1443
  log: "***** hover " + selectors.element_name + " *****\n",
954
1444
  };
955
1445
  try {
956
1446
  await _preCommand(state, this);
957
- try {
958
- await state.element.hover();
959
- await new Promise((resolve) => setTimeout(resolve, 1000));
960
- }
961
- catch (e) {
962
- //await this.closeUnexpectedPopups();
963
- state.info.log += "hover failed, will try again" + "\n";
964
- state.element = await this._locate(selectors, state.info, _params);
965
- await state.element.hover({ timeout: 10000 });
966
- await new Promise((resolve) => setTimeout(resolve, 1000));
967
- }
1447
+ await performAction("hover", state.element, options, this, state, _params);
968
1448
  await _screenshot(state, this);
969
- await this.waitForPageLoad();
1449
+ //await this.waitForPageLoad();
970
1450
  return state.info;
971
1451
  }
972
1452
  catch (e) {
973
1453
  await _commandError(state, e, this);
974
1454
  }
975
1455
  finally {
976
- _commandFinally(state, this);
1456
+ await _commandFinally(state, this);
977
1457
  }
978
1458
  }
979
1459
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -988,6 +1468,7 @@ class StableBrowser {
988
1468
  value: values.toString(),
989
1469
  type: Types.SELECT,
990
1470
  text: `Select option: ${values}`,
1471
+ _text: `Select option: ${values} on ${selectors.element_name}`,
991
1472
  operation: "selectOption",
992
1473
  log: "***** select option " + selectors.element_name + " *****\n",
993
1474
  };
@@ -1001,14 +1482,14 @@ class StableBrowser {
1001
1482
  state.info.log += "selectOption failed, will try force" + "\n";
1002
1483
  await state.element.selectOption(values, { timeout: 10000, force: true });
1003
1484
  }
1004
- await this.waitForPageLoad();
1485
+ //await this.waitForPageLoad();
1005
1486
  return state.info;
1006
1487
  }
1007
1488
  catch (e) {
1008
1489
  await _commandError(state, e, this);
1009
1490
  }
1010
1491
  finally {
1011
- _commandFinally(state, this);
1492
+ await _commandFinally(state, this);
1012
1493
  }
1013
1494
  }
1014
1495
  async type(_value, _params = null, options = {}, world = null) {
@@ -1022,6 +1503,7 @@ class StableBrowser {
1022
1503
  highlight: false,
1023
1504
  type: Types.TYPE_PRESS,
1024
1505
  text: `Type value: ${_value}`,
1506
+ _text: `Type value: ${_value}`,
1025
1507
  operation: "type",
1026
1508
  log: "",
1027
1509
  };
@@ -1053,7 +1535,7 @@ class StableBrowser {
1053
1535
  await _commandError(state, e, this);
1054
1536
  }
1055
1537
  finally {
1056
- _commandFinally(state, this);
1538
+ await _commandFinally(state, this);
1057
1539
  }
1058
1540
  }
1059
1541
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1089,37 +1571,35 @@ class StableBrowser {
1089
1571
  await _commandError(state, e, this);
1090
1572
  }
1091
1573
  finally {
1092
- _commandFinally(state, this);
1574
+ await _commandFinally(state, this);
1093
1575
  }
1094
1576
  }
1095
1577
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1096
- _validateSelectors(selectors);
1097
- const startTime = Date.now();
1098
- let error = null;
1099
- let screenshotId = null;
1100
- let screenshotPath = null;
1101
- const info = {};
1102
- info.log = "";
1103
- info.operation = Types.SET_DATE_TIME;
1104
- info.selectors = selectors;
1105
- info.value = value;
1578
+ const state = {
1579
+ selectors,
1580
+ _params,
1581
+ value: await this._replaceWithLocalData(value, this),
1582
+ options,
1583
+ world,
1584
+ type: Types.SET_DATE_TIME,
1585
+ text: `Set date time value: ${value}`,
1586
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1587
+ operation: "setDateTime",
1588
+ log: "***** set date time value " + selectors.element_name + " *****\n",
1589
+ throwError: false,
1590
+ };
1106
1591
  try {
1107
- value = await this._replaceWithLocalData(value, this);
1108
- let element = await this._locate(selectors, info, _params);
1109
- //insert red border around the element
1110
- await this.scrollIfNeeded(element, info);
1111
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1112
- await this._highlightElements(element);
1592
+ await _preCommand(state, this);
1113
1593
  try {
1114
- await element.click();
1594
+ await performAction("click", state.element, options, this, state, _params);
1115
1595
  await new Promise((resolve) => setTimeout(resolve, 500));
1116
1596
  if (format) {
1117
- value = dayjs(value).format(format);
1118
- await element.fill(value);
1597
+ state.value = dayjs(state.value).format(format);
1598
+ await state.element.fill(state.value);
1119
1599
  }
1120
1600
  else {
1121
- const dateTimeValue = await getDateTimeValue({ value, element });
1122
- await element.evaluateHandle((el, dateTimeValue) => {
1601
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1602
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1123
1603
  el.value = ""; // clear input
1124
1604
  el.value = dateTimeValue;
1125
1605
  }, dateTimeValue);
@@ -1132,20 +1612,19 @@ class StableBrowser {
1132
1612
  }
1133
1613
  catch (err) {
1134
1614
  //await this.closeUnexpectedPopups();
1135
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1615
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1136
1616
  this.logger.info("Trying again");
1137
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1138
- info.screenshotPath = screenshotPath;
1139
- Object.assign(err, { info: info });
1617
+ await _screenshot(state, this);
1618
+ Object.assign(err, { info: state.info });
1140
1619
  await element.click();
1141
1620
  await new Promise((resolve) => setTimeout(resolve, 500));
1142
1621
  if (format) {
1143
- value = dayjs(value).format(format);
1144
- await element.fill(value);
1622
+ state.value = dayjs(state.value).format(format);
1623
+ await state.element.fill(state.value);
1145
1624
  }
1146
1625
  else {
1147
- const dateTimeValue = await getDateTimeValue({ value, element });
1148
- await element.evaluateHandle((el, dateTimeValue) => {
1626
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1627
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1149
1628
  el.value = ""; // clear input
1150
1629
  el.value = dateTimeValue;
1151
1630
  }, dateTimeValue);
@@ -1158,31 +1637,10 @@ class StableBrowser {
1158
1637
  }
1159
1638
  }
1160
1639
  catch (e) {
1161
- error = e;
1162
- throw e;
1640
+ await _commandError(state, e, this);
1163
1641
  }
1164
1642
  finally {
1165
- const endTime = Date.now();
1166
- this._reportToWorld(world, {
1167
- element_name: selectors.element_name,
1168
- type: Types.SET_DATE_TIME,
1169
- screenshotId,
1170
- value: value,
1171
- text: `setDateTime input with value: ${value}`,
1172
- result: error
1173
- ? {
1174
- status: "FAILED",
1175
- startTime,
1176
- endTime,
1177
- message: error === null || error === void 0 ? void 0 : error.message,
1178
- }
1179
- : {
1180
- status: "PASSED",
1181
- startTime,
1182
- endTime,
1183
- },
1184
- info: info,
1185
- });
1643
+ await _commandFinally(state, this);
1186
1644
  }
1187
1645
  }
1188
1646
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1197,17 +1655,29 @@ class StableBrowser {
1197
1655
  world,
1198
1656
  type: Types.FILL,
1199
1657
  text: `Click type input with value: ${_value}`,
1658
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1200
1659
  operation: "clickType",
1201
1660
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1202
1661
  };
1662
+ if (!options) {
1663
+ options = {};
1664
+ }
1203
1665
  if (newValue !== _value) {
1204
1666
  //this.logger.info(_value + "=" + newValue);
1205
1667
  _value = newValue;
1206
1668
  }
1207
1669
  try {
1208
1670
  await _preCommand(state, this);
1671
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1672
+ // tag the element
1673
+ let newElementSelector = await state.element.evaluate((el, token) => {
1674
+ // use attribute and not id
1675
+ const attrName = `data-blinq-id-${token}`;
1676
+ el.setAttribute(attrName, "");
1677
+ return `[${attrName}]`;
1678
+ }, randomToken);
1209
1679
  state.info.value = _value;
1210
- if (options === null || options === undefined || !options.press) {
1680
+ if (!options.press) {
1211
1681
  try {
1212
1682
  let currentValue = await state.element.inputValue();
1213
1683
  if (currentValue) {
@@ -1218,13 +1688,9 @@ class StableBrowser {
1218
1688
  this.logger.info("unable to clear input value");
1219
1689
  }
1220
1690
  }
1221
- if (options === null || options === undefined || options.press) {
1222
- try {
1223
- await state.element.click({ timeout: 5000 });
1224
- }
1225
- catch (e) {
1226
- await state.element.dispatchEvent("click");
1227
- }
1691
+ if (options.press) {
1692
+ options.timeout = 5000;
1693
+ await performAction("click", state.element, options, this, state, _params);
1228
1694
  }
1229
1695
  else {
1230
1696
  try {
@@ -1235,6 +1701,25 @@ class StableBrowser {
1235
1701
  }
1236
1702
  }
1237
1703
  await new Promise((resolve) => setTimeout(resolve, 500));
1704
+ // check if the element exist after the click (no wait)
1705
+ const count = await state.element.count({ timeout: 0 });
1706
+ if (count === 0) {
1707
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1708
+ const scope = state.element._frame ?? element.page();
1709
+ let prefixSelector = "";
1710
+ const frameControlSelector = " >> internal:control=enter-frame";
1711
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1712
+ if (frameSelectorIndex !== -1) {
1713
+ // remove everything after the >> internal:control=enter-frame
1714
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1715
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1716
+ }
1717
+ // if (element?._frame?._selector) {
1718
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1719
+ // }
1720
+ const newSelector = prefixSelector + newElementSelector;
1721
+ state.element = scope.locator(newSelector).first();
1722
+ }
1238
1723
  const valueSegment = state.value.split("&&");
1239
1724
  for (let i = 0; i < valueSegment.length; i++) {
1240
1725
  if (i > 0) {
@@ -1255,14 +1740,21 @@ class StableBrowser {
1255
1740
  await new Promise((resolve) => setTimeout(resolve, 500));
1256
1741
  }
1257
1742
  }
1743
+ //if (!this.fastMode) {
1258
1744
  await _screenshot(state, this);
1745
+ //}
1259
1746
  if (enter === true) {
1260
1747
  await new Promise((resolve) => setTimeout(resolve, 2000));
1261
1748
  await this.page.keyboard.press("Enter");
1262
1749
  await this.waitForPageLoad();
1263
1750
  }
1264
1751
  else if (enter === false) {
1265
- await state.element.dispatchEvent("change");
1752
+ try {
1753
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1754
+ }
1755
+ catch (e) {
1756
+ // ignore
1757
+ }
1266
1758
  //await this.page.keyboard.press("Tab");
1267
1759
  }
1268
1760
  else {
@@ -1277,7 +1769,7 @@ class StableBrowser {
1277
1769
  await _commandError(state, e, this);
1278
1770
  }
1279
1771
  finally {
1280
- _commandFinally(state, this);
1772
+ await _commandFinally(state, this);
1281
1773
  }
1282
1774
  }
1283
1775
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1299,30 +1791,67 @@ class StableBrowser {
1299
1791
  if (enter) {
1300
1792
  await new Promise((resolve) => setTimeout(resolve, 2000));
1301
1793
  await this.page.keyboard.press("Enter");
1794
+ await this.waitForPageLoad();
1795
+ }
1796
+ return state.info;
1797
+ }
1798
+ catch (e) {
1799
+ await _commandError(state, e, this);
1800
+ }
1801
+ finally {
1802
+ await _commandFinally(state, this);
1803
+ }
1804
+ }
1805
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1806
+ const state = {
1807
+ selectors,
1808
+ _params,
1809
+ files,
1810
+ value: '"' + files.join('", "') + '"',
1811
+ options,
1812
+ world,
1813
+ type: Types.SET_INPUT_FILES,
1814
+ text: `Set input files`,
1815
+ _text: `Set input files on ${selectors.element_name}`,
1816
+ operation: "setInputFiles",
1817
+ log: "***** set input files " + selectors.element_name + " *****\n",
1818
+ };
1819
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1820
+ try {
1821
+ await _preCommand(state, this);
1822
+ for (let i = 0; i < files.length; i++) {
1823
+ const file = files[i];
1824
+ const filePath = path.join(uploadsFolder, file);
1825
+ if (!fs.existsSync(filePath)) {
1826
+ throw new Error(`File not found: ${filePath}`);
1827
+ }
1828
+ state.files[i] = filePath;
1302
1829
  }
1303
- await this.waitForPageLoad();
1830
+ await state.element.setInputFiles(files);
1304
1831
  return state.info;
1305
1832
  }
1306
1833
  catch (e) {
1307
1834
  await _commandError(state, e, this);
1308
1835
  }
1309
1836
  finally {
1310
- _commandFinally(state, this);
1837
+ await _commandFinally(state, this);
1311
1838
  }
1312
1839
  }
1313
1840
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1314
1841
  return await this._getText(selectors, 0, _params, options, info, world);
1315
1842
  }
1316
1843
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1844
+ const timeout = this._getFindElementTimeout(options);
1317
1845
  _validateSelectors(selectors);
1318
1846
  let screenshotId = null;
1319
1847
  let screenshotPath = null;
1320
1848
  if (!info.log) {
1321
1849
  info.log = "";
1850
+ info.locatorLog = new LocatorLog(selectors);
1322
1851
  }
1323
1852
  info.operation = "getText";
1324
1853
  info.selectors = selectors;
1325
- let element = await this._locate(selectors, info, _params);
1854
+ let element = await this._locate(selectors, info, _params, timeout);
1326
1855
  if (climb > 0) {
1327
1856
  const climbArray = [];
1328
1857
  for (let i = 0; i < climb; i++) {
@@ -1341,6 +1870,18 @@ class StableBrowser {
1341
1870
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1342
1871
  try {
1343
1872
  await this._highlightElements(element);
1873
+ // if (world && world.screenshot && !world.screenshotPath) {
1874
+ // // console.log(`Highlighting for get text while running from recorder`);
1875
+ // this._highlightElements(element)
1876
+ // .then(async () => {
1877
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1878
+ // this._unhighlightElements(element).then(
1879
+ // () => {}
1880
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1881
+ // );
1882
+ // })
1883
+ // .catch(e);
1884
+ // }
1344
1885
  const elementText = await element.innerText();
1345
1886
  return {
1346
1887
  text: elementText,
@@ -1352,7 +1893,7 @@ class StableBrowser {
1352
1893
  }
1353
1894
  catch (e) {
1354
1895
  //await this.closeUnexpectedPopups();
1355
- this.logger.info("no innerText will use textContent");
1896
+ this.logger.info("no innerText, will use textContent");
1356
1897
  const elementText = await element.textContent();
1357
1898
  return { text: elementText, screenshotId, screenshotPath, value: value };
1358
1899
  }
@@ -1377,6 +1918,7 @@ class StableBrowser {
1377
1918
  highlight: false,
1378
1919
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1379
1920
  text: `Verify element contains pattern: ${pattern}`,
1921
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1380
1922
  operation: "containsPattern",
1381
1923
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1382
1924
  };
@@ -1408,10 +1950,12 @@ class StableBrowser {
1408
1950
  await _commandError(state, e, this);
1409
1951
  }
1410
1952
  finally {
1411
- _commandFinally(state, this);
1953
+ await _commandFinally(state, this);
1412
1954
  }
1413
1955
  }
1414
1956
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1957
+ const timeout = this._getFindElementTimeout(options);
1958
+ const startTime = Date.now();
1415
1959
  const state = {
1416
1960
  selectors,
1417
1961
  _params,
@@ -1438,61 +1982,137 @@ class StableBrowser {
1438
1982
  }
1439
1983
  let foundObj = null;
1440
1984
  try {
1441
- await _preCommand(state, this);
1442
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1443
- if (foundObj && foundObj.element) {
1444
- await this.scrollIfNeeded(foundObj.element, state.info);
1445
- }
1446
- await _screenshot(state, this);
1447
- const dateAlternatives = findDateAlternatives(text);
1448
- const numberAlternatives = findNumberAlternatives(text);
1449
- if (dateAlternatives.date) {
1450
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1451
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1452
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1453
- return state.info;
1985
+ while (Date.now() - startTime < timeout) {
1986
+ try {
1987
+ await _preCommand(state, this);
1988
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1989
+ if (foundObj && foundObj.element) {
1990
+ await this.scrollIfNeeded(foundObj.element, state.info);
1454
1991
  }
1455
- }
1456
- throw new Error("element doesn't contain text " + text);
1457
- }
1458
- else if (numberAlternatives.number) {
1459
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1460
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1461
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1992
+ await _screenshot(state, this);
1993
+ const dateAlternatives = findDateAlternatives(text);
1994
+ const numberAlternatives = findNumberAlternatives(text);
1995
+ if (dateAlternatives.date) {
1996
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1997
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1998
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1999
+ return state.info;
2000
+ }
2001
+ }
2002
+ }
2003
+ else if (numberAlternatives.number) {
2004
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
2005
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
2006
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
2007
+ return state.info;
2008
+ }
2009
+ }
2010
+ }
2011
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1462
2012
  return state.info;
1463
2013
  }
1464
2014
  }
1465
- throw new Error("element doesn't contain text " + text);
1466
- }
1467
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1468
- state.info.foundText = foundObj?.text;
1469
- state.info.value = foundObj?.value;
1470
- throw new Error("element doesn't contain text " + text);
2015
+ catch (e) {
2016
+ // Log error but continue retrying until timeout is reached
2017
+ this.logger.warn("Retrying containsText due to: " + e.message);
2018
+ }
2019
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1471
2020
  }
1472
- return state.info;
2021
+ state.info.foundText = foundObj?.text;
2022
+ state.info.value = foundObj?.value;
2023
+ throw new Error("element doesn't contain text " + text);
1473
2024
  }
1474
2025
  catch (e) {
1475
2026
  await _commandError(state, e, this);
2027
+ throw e;
1476
2028
  }
1477
2029
  finally {
1478
- _commandFinally(state, this);
2030
+ await _commandFinally(state, this);
1479
2031
  }
1480
2032
  }
1481
- _getDataFile(world = null) {
1482
- let dataFile = null;
1483
- if (world && world.reportFolder) {
1484
- dataFile = path.join(world.reportFolder, "data.json");
2033
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
2034
+ const timeout = this._getFindElementTimeout(options);
2035
+ const startTime = Date.now();
2036
+ const state = {
2037
+ _params,
2038
+ value: referanceSnapshot,
2039
+ options,
2040
+ world,
2041
+ locate: false,
2042
+ scroll: false,
2043
+ screenshot: true,
2044
+ highlight: false,
2045
+ type: Types.SNAPSHOT_VALIDATION,
2046
+ text: `verify snapshot: ${referanceSnapshot}`,
2047
+ operation: "snapshotValidation",
2048
+ log: "***** verify snapshot *****\n",
2049
+ };
2050
+ if (!referanceSnapshot) {
2051
+ throw new Error("referanceSnapshot is null");
2052
+ }
2053
+ let text = null;
2054
+ const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
2055
+ if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
2056
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
1485
2057
  }
1486
- else if (this.reportFolder) {
1487
- dataFile = path.join(this.reportFolder, "data.json");
2058
+ else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
2059
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
1488
2060
  }
1489
- else if (this.context && this.context.reportFolder) {
1490
- dataFile = path.join(this.context.reportFolder, "data.json");
2061
+ else if (referanceSnapshot.startsWith("yaml:")) {
2062
+ text = referanceSnapshot.substring(5);
1491
2063
  }
1492
2064
  else {
1493
- dataFile = "data.json";
2065
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
2066
+ }
2067
+ state.text = text;
2068
+ const newValue = await this._replaceWithLocalData(text, world);
2069
+ await _preCommand(state, this);
2070
+ let foundObj = null;
2071
+ try {
2072
+ let matchResult = null;
2073
+ while (Date.now() - startTime < timeout) {
2074
+ try {
2075
+ let scope = null;
2076
+ if (!frameSelectors) {
2077
+ scope = this.page;
2078
+ }
2079
+ else {
2080
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
2081
+ }
2082
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
2083
+ if (snapshot && snapshot.length <= 10) {
2084
+ console.log("Page snapshot length is suspiciously small:", snapshot);
2085
+ }
2086
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
2087
+ if (matchResult === undefined) {
2088
+ console.log("snapshotValidation returned undefined");
2089
+ }
2090
+ if (matchResult.errorLine !== -1) {
2091
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
2092
+ }
2093
+ // highlight and screenshot
2094
+ try {
2095
+ await await highlightSnapshot(newValue, scope);
2096
+ await _screenshot(state, this);
2097
+ }
2098
+ catch (e) { }
2099
+ return state.info;
2100
+ }
2101
+ catch (e) {
2102
+ // Log error but continue retrying until timeout is reached
2103
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
2104
+ }
2105
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
2106
+ }
2107
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
2108
+ }
2109
+ catch (e) {
2110
+ await _commandError(state, e, this);
2111
+ throw e;
2112
+ }
2113
+ finally {
2114
+ await _commandFinally(state, this);
1494
2115
  }
1495
- return dataFile;
1496
2116
  }
1497
2117
  async waitForUserInput(message, world = null) {
1498
2118
  if (!message) {
@@ -1522,13 +2142,22 @@ class StableBrowser {
1522
2142
  return;
1523
2143
  }
1524
2144
  // if data file exists, load it
1525
- const dataFile = this._getDataFile(world);
2145
+ const dataFile = _getDataFile(world, this.context, this);
1526
2146
  let data = this.getTestData(world);
1527
2147
  // merge the testData with the existing data
1528
2148
  Object.assign(data, testData);
1529
2149
  // save the data to the file
1530
2150
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1531
2151
  }
2152
+ overwriteTestData(testData, world = null) {
2153
+ if (!testData) {
2154
+ return;
2155
+ }
2156
+ // if data file exists, load it
2157
+ const dataFile = _getDataFile(world, this.context, this);
2158
+ // save the data to the file
2159
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
2160
+ }
1532
2161
  _getDataFilePath(fileName) {
1533
2162
  let dataFile = path.join(this.project_path, "data", fileName);
1534
2163
  if (fs.existsSync(dataFile)) {
@@ -1625,14 +2254,12 @@ class StableBrowser {
1625
2254
  }
1626
2255
  }
1627
2256
  getTestData(world = null) {
1628
- const dataFile = this._getDataFile(world);
1629
- let data = {};
1630
- if (fs.existsSync(dataFile)) {
1631
- data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1632
- }
1633
- return data;
2257
+ return _getTestData(world, this.context, this);
1634
2258
  }
1635
2259
  async _screenShot(options = {}, world = null, info = null) {
2260
+ if (!options) {
2261
+ options = {};
2262
+ }
1636
2263
  // collect url/path/title
1637
2264
  if (info) {
1638
2265
  if (!info.title) {
@@ -1657,13 +2284,11 @@ class StableBrowser {
1657
2284
  if (!fs.existsSync(world.screenshotPath)) {
1658
2285
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1659
2286
  }
1660
- let nextIndex = 1;
1661
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1662
- nextIndex++;
1663
- }
1664
- const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
2287
+ // to make sure the path doesn't start with -
2288
+ const uuidStr = "id_" + randomUUID();
2289
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1665
2290
  try {
1666
- await this.takeScreenshot(screenshotPath);
2291
+ await this.takeScreenshot(screenshotPath, options.fullPage === true);
1667
2292
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1668
2293
  // // save the buffer to the screenshot path asynchrously
1669
2294
  // fs.writeFile(screenshotPath, buffer, (err) => {
@@ -1671,20 +2296,20 @@ class StableBrowser {
1671
2296
  // this.logger.info("unable to save screenshot " + screenshotPath);
1672
2297
  // }
1673
2298
  // });
2299
+ result.screenshotId = uuidStr;
2300
+ result.screenshotPath = screenshotPath;
2301
+ if (info && info.box) {
2302
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
2303
+ }
1674
2304
  }
1675
2305
  catch (e) {
1676
2306
  this.logger.info("unable to take screenshot, ignored");
1677
2307
  }
1678
- result.screenshotId = nextIndex;
1679
- result.screenshotPath = screenshotPath;
1680
- if (info && info.box) {
1681
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1682
- }
1683
2308
  }
1684
2309
  else if (options && options.screenshot) {
1685
2310
  result.screenshotPath = options.screenshotPath;
1686
2311
  try {
1687
- await this.takeScreenshot(options.screenshotPath);
2312
+ await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
1688
2313
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1689
2314
  // // save the buffer to the screenshot path asynchrously
1690
2315
  // fs.writeFile(options.screenshotPath, buffer, (err) => {
@@ -1702,7 +2327,7 @@ class StableBrowser {
1702
2327
  }
1703
2328
  return result;
1704
2329
  }
1705
- async takeScreenshot(screenshotPath) {
2330
+ async takeScreenshot(screenshotPath, fullPage = false) {
1706
2331
  const playContext = this.context.playContext;
1707
2332
  // Using CDP to capture the screenshot
1708
2333
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
@@ -1714,17 +2339,20 @@ class StableBrowser {
1714
2339
  document.documentElement.clientWidth,
1715
2340
  ])));
1716
2341
  let screenshotBuffer = null;
2342
+ // if (focusedElement) {
2343
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2344
+ // await this._unhighlightElements(focusedElement);
2345
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2346
+ // console.log(`Unhighlighted previous element`);
2347
+ // }
2348
+ // if (focusedElement) {
2349
+ // await this._highlightElements(focusedElement);
2350
+ // }
1717
2351
  if (this.context.browserName === "chromium") {
1718
2352
  const client = await playContext.newCDPSession(this.page);
1719
2353
  const { data } = await client.send("Page.captureScreenshot", {
1720
2354
  format: "png",
1721
- // clip: {
1722
- // x: 0,
1723
- // y: 0,
1724
- // width: viewportWidth,
1725
- // height: viewportHeight,
1726
- // scale: 1,
1727
- // },
2355
+ captureBeyondViewport: fullPage,
1728
2356
  });
1729
2357
  await client.detach();
1730
2358
  if (!screenshotPath) {
@@ -1733,8 +2361,12 @@ class StableBrowser {
1733
2361
  screenshotBuffer = Buffer.from(data, "base64");
1734
2362
  }
1735
2363
  else {
1736
- screenshotBuffer = await this.page.screenshot();
2364
+ screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
1737
2365
  }
2366
+ // if (focusedElement) {
2367
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2368
+ // await this._unhighlightElements(focusedElement);
2369
+ // }
1738
2370
  let image = await Jimp.read(screenshotBuffer);
1739
2371
  // Get the image dimensions
1740
2372
  const { width, height } = image.bitmap;
@@ -1747,6 +2379,7 @@ class StableBrowser {
1747
2379
  else {
1748
2380
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1749
2381
  }
2382
+ return screenshotBuffer;
1750
2383
  }
1751
2384
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1752
2385
  const state = {
@@ -1769,116 +2402,535 @@ class StableBrowser {
1769
2402
  await _commandError(state, e, this);
1770
2403
  }
1771
2404
  finally {
1772
- _commandFinally(state, this);
2405
+ await _commandFinally(state, this);
1773
2406
  }
1774
2407
  }
1775
2408
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1776
- _validateSelectors(selectors);
1777
- const startTime = Date.now();
1778
- let error = null;
1779
- let screenshotId = null;
1780
- let screenshotPath = null;
2409
+ const state = {
2410
+ selectors,
2411
+ _params,
2412
+ attribute,
2413
+ variable,
2414
+ options,
2415
+ world,
2416
+ type: Types.EXTRACT,
2417
+ text: `Extract attribute from element`,
2418
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
2419
+ operation: "extractAttribute",
2420
+ log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
2421
+ allowDisabled: true,
2422
+ };
1781
2423
  await new Promise((resolve) => setTimeout(resolve, 2000));
1782
- const info = {};
1783
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1784
- info.operation = "extract";
1785
- info.selectors = selectors;
1786
2424
  try {
1787
- const element = await this._locate(selectors, info, _params);
1788
- await this._highlightElements(element);
1789
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2425
+ await _preCommand(state, this);
1790
2426
  switch (attribute) {
1791
2427
  case "inner_text":
1792
- info.value = await element.innerText();
2428
+ state.value = await state.element.innerText();
1793
2429
  break;
1794
2430
  case "href":
1795
- info.value = await element.getAttribute("href");
2431
+ state.value = await state.element.getAttribute("href");
1796
2432
  break;
1797
2433
  case "value":
1798
- info.value = await element.inputValue();
2434
+ state.value = await state.element.inputValue();
2435
+ break;
2436
+ case "text":
2437
+ state.value = await state.element.textContent();
1799
2438
  break;
1800
2439
  default:
1801
- info.value = await element.getAttribute(attribute);
2440
+ state.value = await state.element.getAttribute(attribute);
1802
2441
  break;
1803
2442
  }
1804
- this[variable] = info.value;
1805
- if (world) {
1806
- world[variable] = info.value;
2443
+ if (options !== null) {
2444
+ if (options.regex && options.regex !== "") {
2445
+ // Construct a regex pattern from the provided string
2446
+ const regex = options.regex.slice(1, -1);
2447
+ const regexPattern = new RegExp(regex, "g");
2448
+ const matches = state.value.match(regexPattern);
2449
+ if (matches) {
2450
+ let newValue = "";
2451
+ for (const match of matches) {
2452
+ newValue += match;
2453
+ }
2454
+ state.value = newValue;
2455
+ }
2456
+ }
2457
+ if (options.trimSpaces && options.trimSpaces === true) {
2458
+ state.value = state.value.trim();
2459
+ }
1807
2460
  }
1808
- this.setTestData({ [variable]: info.value }, world);
1809
- this.logger.info("set test data: " + variable + "=" + info.value);
1810
- return info;
2461
+ state.info.value = state.value;
2462
+ this.setTestData({ [variable]: state.value }, world);
2463
+ this.logger.info("set test data: " + variable + "=" + state.value);
2464
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2465
+ return state.info;
1811
2466
  }
1812
2467
  catch (e) {
1813
- //await this.closeUnexpectedPopups();
1814
- this.logger.error("extract failed " + info.log);
1815
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1816
- info.screenshotPath = screenshotPath;
1817
- Object.assign(e, { info: info });
1818
- error = e;
1819
- throw e;
2468
+ await _commandError(state, e, this);
1820
2469
  }
1821
2470
  finally {
1822
- const endTime = Date.now();
1823
- this._reportToWorld(world, {
1824
- element_name: selectors.element_name,
1825
- type: Types.EXTRACT_ATTRIBUTE,
1826
- variable: variable,
1827
- value: info.value,
1828
- text: "Extract attribute from element",
1829
- screenshotId,
1830
- result: error
1831
- ? {
1832
- status: "FAILED",
1833
- startTime,
1834
- endTime,
1835
- message: error?.message,
1836
- }
1837
- : {
1838
- status: "PASSED",
1839
- startTime,
1840
- endTime,
1841
- },
1842
- info: info,
1843
- });
2471
+ await _commandFinally(state, this);
1844
2472
  }
1845
2473
  }
1846
- async extractEmailData(emailAddress, options, world) {
1847
- if (!emailAddress) {
1848
- throw new Error("email address is null");
1849
- }
1850
- // check if address contain @
1851
- if (emailAddress.indexOf("@") === -1) {
1852
- emailAddress = emailAddress + "@blinq-mail.io";
1853
- }
1854
- else {
1855
- if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1856
- throw new Error("email address should end with @blinq-mail.io");
2474
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2475
+ const state = {
2476
+ selectors,
2477
+ _params,
2478
+ property,
2479
+ variable,
2480
+ options,
2481
+ world,
2482
+ type: Types.EXTRACT_PROPERTY,
2483
+ text: `Extract property from element`,
2484
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2485
+ operation: "extractProperty",
2486
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2487
+ allowDisabled: true,
2488
+ };
2489
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2490
+ try {
2491
+ await _preCommand(state, this);
2492
+ switch (property) {
2493
+ case "inner_text":
2494
+ state.value = await state.element.innerText();
2495
+ break;
2496
+ case "href":
2497
+ state.value = await state.element.getAttribute("href");
2498
+ break;
2499
+ case "value":
2500
+ state.value = await state.element.inputValue();
2501
+ break;
2502
+ case "text":
2503
+ state.value = await state.element.textContent();
2504
+ break;
2505
+ default:
2506
+ if (property.startsWith("dataset.")) {
2507
+ const dataAttribute = property.substring(8);
2508
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2509
+ }
2510
+ else {
2511
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2512
+ }
2513
+ }
2514
+ if (options !== null) {
2515
+ if (options.regex && options.regex !== "") {
2516
+ // Construct a regex pattern from the provided string
2517
+ const regex = options.regex.slice(1, -1);
2518
+ const regexPattern = new RegExp(regex, "g");
2519
+ const matches = state.value.match(regexPattern);
2520
+ if (matches) {
2521
+ let newValue = "";
2522
+ for (const match of matches) {
2523
+ newValue += match;
2524
+ }
2525
+ state.value = newValue;
2526
+ }
2527
+ }
2528
+ if (options.trimSpaces && options.trimSpaces === true) {
2529
+ state.value = state.value.trim();
2530
+ }
1857
2531
  }
2532
+ state.info.value = state.value;
2533
+ this.setTestData({ [variable]: state.value }, world);
2534
+ this.logger.info("set test data: " + variable + "=" + state.value);
2535
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2536
+ return state.info;
1858
2537
  }
1859
- const startTime = Date.now();
1860
- let timeout = 60000;
1861
- if (options && options.timeout) {
1862
- timeout = options.timeout;
2538
+ catch (e) {
2539
+ await _commandError(state, e, this);
1863
2540
  }
1864
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1865
- const request = {
1866
- method: "POST",
1867
- url: serviceUrl,
1868
- headers: {
1869
- "Content-Type": "application/json",
1870
- Authorization: `Bearer ${process.env.TOKEN}`,
1871
- },
1872
- data: JSON.stringify({
1873
- email: emailAddress,
1874
- }),
1875
- };
1876
- let errorCount = 0;
1877
- while (true) {
1878
- try {
1879
- let result = await this.context.api.request(request);
1880
- // the response body expected to be the following:
1881
- // {
2541
+ finally {
2542
+ await _commandFinally(state, this);
2543
+ }
2544
+ }
2545
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2546
+ const state = {
2547
+ selectors,
2548
+ _params,
2549
+ attribute,
2550
+ value,
2551
+ options,
2552
+ world,
2553
+ type: Types.VERIFY_ATTRIBUTE,
2554
+ highlight: true,
2555
+ screenshot: true,
2556
+ text: `Verify element attribute`,
2557
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2558
+ operation: "verifyAttribute",
2559
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2560
+ allowDisabled: true,
2561
+ };
2562
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2563
+ let val;
2564
+ let expectedValue;
2565
+ try {
2566
+ await _preCommand(state, this);
2567
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2568
+ state.info.expectedValue = expectedValue;
2569
+ switch (attribute) {
2570
+ case "innerText":
2571
+ val = String(await state.element.innerText());
2572
+ break;
2573
+ case "text":
2574
+ val = String(await state.element.textContent());
2575
+ break;
2576
+ case "value":
2577
+ val = String(await state.element.inputValue());
2578
+ break;
2579
+ case "checked":
2580
+ val = String(await state.element.isChecked());
2581
+ break;
2582
+ case "disabled":
2583
+ val = String(await state.element.isDisabled());
2584
+ break;
2585
+ case "readOnly":
2586
+ const isEditable = await state.element.isEditable();
2587
+ val = String(!isEditable);
2588
+ break;
2589
+ default:
2590
+ val = String(await state.element.getAttribute(attribute));
2591
+ break;
2592
+ }
2593
+ state.info.value = val;
2594
+ let regex;
2595
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2596
+ const patternBody = expectedValue.slice(1, -1);
2597
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2598
+ regex = new RegExp(processedPattern, "gs");
2599
+ state.info.regex = true;
2600
+ }
2601
+ else {
2602
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2603
+ regex = new RegExp(escapedPattern, "g");
2604
+ }
2605
+ if (attribute === "innerText") {
2606
+ if (state.info.regex) {
2607
+ if (!regex.test(val)) {
2608
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2609
+ state.info.failCause.assertionFailed = true;
2610
+ state.info.failCause.lastError = errorMessage;
2611
+ throw new Error(errorMessage);
2612
+ }
2613
+ }
2614
+ else {
2615
+ const valLines = val.split("\n");
2616
+ const expectedLines = expectedValue.split("\n");
2617
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2618
+ if (!isPart) {
2619
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2620
+ state.info.failCause.assertionFailed = true;
2621
+ state.info.failCause.lastError = errorMessage;
2622
+ throw new Error(errorMessage);
2623
+ }
2624
+ }
2625
+ }
2626
+ else {
2627
+ if (!val.match(regex)) {
2628
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2629
+ state.info.failCause.assertionFailed = true;
2630
+ state.info.failCause.lastError = errorMessage;
2631
+ throw new Error(errorMessage);
2632
+ }
2633
+ }
2634
+ return state.info;
2635
+ }
2636
+ catch (e) {
2637
+ await _commandError(state, e, this);
2638
+ }
2639
+ finally {
2640
+ await _commandFinally(state, this);
2641
+ }
2642
+ }
2643
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2644
+ const state = {
2645
+ selectors,
2646
+ _params,
2647
+ property,
2648
+ value,
2649
+ options,
2650
+ world,
2651
+ type: Types.VERIFY_PROPERTY,
2652
+ highlight: true,
2653
+ screenshot: true,
2654
+ text: `Verify element property`,
2655
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2656
+ operation: "verifyProperty",
2657
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2658
+ allowDisabled: true,
2659
+ };
2660
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2661
+ let val;
2662
+ let expectedValue;
2663
+ try {
2664
+ await _preCommand(state, this);
2665
+ expectedValue = await this._replaceWithLocalData(value, world);
2666
+ state.info.expectedValue = expectedValue;
2667
+ switch (property) {
2668
+ case "innerText":
2669
+ val = String(await state.element.innerText());
2670
+ break;
2671
+ case "text":
2672
+ val = String(await state.element.textContent());
2673
+ break;
2674
+ case "value":
2675
+ val = String(await state.element.inputValue());
2676
+ break;
2677
+ case "checked":
2678
+ val = String(await state.element.isChecked());
2679
+ break;
2680
+ case "disabled":
2681
+ val = String(await state.element.isDisabled());
2682
+ break;
2683
+ case "readOnly":
2684
+ const isEditable = await state.element.isEditable();
2685
+ val = String(!isEditable);
2686
+ break;
2687
+ case "innerHTML":
2688
+ val = String(await state.element.innerHTML());
2689
+ break;
2690
+ case "outerHTML":
2691
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2692
+ break;
2693
+ default:
2694
+ if (property.startsWith("dataset.")) {
2695
+ const dataAttribute = property.substring(8);
2696
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2697
+ }
2698
+ else {
2699
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2700
+ }
2701
+ }
2702
+ // Helper function to remove all style="" attributes
2703
+ const removeStyleAttributes = (htmlString) => {
2704
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2705
+ };
2706
+ // Remove style attributes for innerHTML and outerHTML properties
2707
+ if (property === "innerHTML" || property === "outerHTML") {
2708
+ val = removeStyleAttributes(val);
2709
+ expectedValue = removeStyleAttributes(expectedValue);
2710
+ }
2711
+ state.info.value = val;
2712
+ let regex;
2713
+ state.info.value = val;
2714
+ const isRegex = expectedValue.startsWith("regex:");
2715
+ const isContains = expectedValue.startsWith("contains:");
2716
+ const isExact = expectedValue.startsWith("exact:");
2717
+ let matchPassed = false;
2718
+ if (isRegex) {
2719
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2720
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2721
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2722
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2723
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2724
+ const regex = new RegExp(patternBody, flags);
2725
+ state.info.regex = true;
2726
+ matchPassed = regex.test(val);
2727
+ }
2728
+ else {
2729
+ // Fallback: treat as literal
2730
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2731
+ const regex = new RegExp(escapedPattern, "g");
2732
+ matchPassed = regex.test(val);
2733
+ }
2734
+ }
2735
+ else if (isContains) {
2736
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2737
+ matchPassed = val.includes(containsValue);
2738
+ }
2739
+ else if (isExact) {
2740
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2741
+ matchPassed = val === exactValue;
2742
+ }
2743
+ else if (property === "innerText") {
2744
+ // Default innerText logic
2745
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2746
+ const valLines = val.split("\n");
2747
+ const expectedLines = normalizedExpectedValue.split("\n");
2748
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2749
+ }
2750
+ else {
2751
+ // Fallback exact or loose match
2752
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2753
+ const regex = new RegExp(escapedPattern, "g");
2754
+ matchPassed = regex.test(val);
2755
+ }
2756
+ if (!matchPassed) {
2757
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2758
+ state.info.failCause.assertionFailed = true;
2759
+ state.info.failCause.lastError = errorMessage;
2760
+ throw new Error(errorMessage);
2761
+ }
2762
+ return state.info;
2763
+ }
2764
+ catch (e) {
2765
+ await _commandError(state, e, this);
2766
+ }
2767
+ finally {
2768
+ await _commandFinally(state, this);
2769
+ }
2770
+ }
2771
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2772
+ // Convert timeout from seconds to milliseconds
2773
+ const timeoutMs = timeout * 1000;
2774
+ const state = {
2775
+ selectors,
2776
+ _params,
2777
+ condition,
2778
+ timeout: timeoutMs, // Store as milliseconds for internal use
2779
+ options,
2780
+ world,
2781
+ type: Types.CONDITIONAL_WAIT,
2782
+ highlight: true,
2783
+ screenshot: true,
2784
+ text: `Conditional wait for element`,
2785
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2786
+ operation: "conditionalWait",
2787
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2788
+ allowDisabled: true,
2789
+ info: {},
2790
+ };
2791
+ state.options ??= { timeout: timeoutMs };
2792
+ // Initialize startTime outside try block to ensure it's always accessible
2793
+ const startTime = Date.now();
2794
+ let conditionMet = false;
2795
+ let currentValue = null;
2796
+ let lastError = null;
2797
+ // Main retry loop - continues until timeout or condition is met
2798
+ while (Date.now() - startTime < timeoutMs) {
2799
+ const elapsedTime = Date.now() - startTime;
2800
+ const remainingTime = timeoutMs - elapsedTime;
2801
+ try {
2802
+ // Try to execute _preCommand (element location)
2803
+ await _preCommand(state, this);
2804
+ // If _preCommand succeeds, start condition checking
2805
+ const checkCondition = async () => {
2806
+ try {
2807
+ switch (condition.toLowerCase()) {
2808
+ case "checked":
2809
+ currentValue = await state.element.isChecked();
2810
+ return currentValue === true;
2811
+ case "unchecked":
2812
+ currentValue = await state.element.isChecked();
2813
+ return currentValue === false;
2814
+ case "visible":
2815
+ currentValue = await state.element.isVisible();
2816
+ return currentValue === true;
2817
+ case "hidden":
2818
+ currentValue = await state.element.isVisible();
2819
+ return currentValue === false;
2820
+ case "enabled":
2821
+ currentValue = await state.element.isDisabled();
2822
+ return currentValue === false;
2823
+ case "disabled":
2824
+ currentValue = await state.element.isDisabled();
2825
+ return currentValue === true;
2826
+ case "editable":
2827
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2828
+ currentValue = await state.element.isContentEditable();
2829
+ return currentValue === true;
2830
+ default:
2831
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2832
+ state.info.success = false;
2833
+ return false;
2834
+ }
2835
+ }
2836
+ catch (error) {
2837
+ // Don't throw here, just return false to continue retrying
2838
+ return false;
2839
+ }
2840
+ };
2841
+ // Inner loop for condition checking (once element is located)
2842
+ while (Date.now() - startTime < timeoutMs) {
2843
+ const currentElapsedTime = Date.now() - startTime;
2844
+ conditionMet = await checkCondition();
2845
+ if (conditionMet) {
2846
+ break;
2847
+ }
2848
+ // Check if we still have time for another attempt
2849
+ if (Date.now() - startTime + 50 < timeoutMs) {
2850
+ await new Promise((res) => setTimeout(res, 50));
2851
+ }
2852
+ else {
2853
+ break;
2854
+ }
2855
+ }
2856
+ // If we got here and condition is met, break out of main loop
2857
+ if (conditionMet) {
2858
+ break;
2859
+ }
2860
+ // If condition not met but no exception, we've timed out
2861
+ break;
2862
+ }
2863
+ catch (e) {
2864
+ lastError = e;
2865
+ const currentElapsedTime = Date.now() - startTime;
2866
+ const timeLeft = timeoutMs - currentElapsedTime;
2867
+ // Check if we have enough time left to retry
2868
+ if (timeLeft > 100) {
2869
+ await new Promise((resolve) => setTimeout(resolve, 50));
2870
+ }
2871
+ else {
2872
+ break;
2873
+ }
2874
+ }
2875
+ }
2876
+ const actualWaitTime = Date.now() - startTime;
2877
+ state.info = {
2878
+ success: conditionMet,
2879
+ conditionMet,
2880
+ actualWaitTime,
2881
+ currentValue,
2882
+ lastError: lastError?.message || null,
2883
+ message: conditionMet
2884
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2885
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2886
+ };
2887
+ if (lastError) {
2888
+ state.log += `Last error: ${lastError.message}\n`;
2889
+ }
2890
+ try {
2891
+ await _commandFinally(state, this);
2892
+ }
2893
+ catch (finallyError) {
2894
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2895
+ }
2896
+ return state.info;
2897
+ }
2898
+ async extractEmailData(emailAddress, options, world) {
2899
+ if (!emailAddress) {
2900
+ throw new Error("email address is null");
2901
+ }
2902
+ // check if address contain @
2903
+ if (emailAddress.indexOf("@") === -1) {
2904
+ emailAddress = emailAddress + "@blinq-mail.io";
2905
+ }
2906
+ else {
2907
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
2908
+ throw new Error("email address should end with @blinq-mail.io");
2909
+ }
2910
+ }
2911
+ const startTime = Date.now();
2912
+ let timeout = 60000;
2913
+ if (options && options.timeout) {
2914
+ timeout = options.timeout;
2915
+ }
2916
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2917
+ const request = {
2918
+ method: "POST",
2919
+ url: serviceUrl,
2920
+ headers: {
2921
+ "Content-Type": "application/json",
2922
+ Authorization: `Bearer ${process.env.TOKEN}`,
2923
+ },
2924
+ data: JSON.stringify({
2925
+ email: emailAddress,
2926
+ }),
2927
+ };
2928
+ let errorCount = 0;
2929
+ while (true) {
2930
+ try {
2931
+ let result = await this.context.api.request(request);
2932
+ // the response body expected to be the following:
2933
+ // {
1882
2934
  // "status": true,
1883
2935
  // "content": {
1884
2936
  // "url": "",
@@ -1917,7 +2969,8 @@ class StableBrowser {
1917
2969
  catch (e) {
1918
2970
  errorCount++;
1919
2971
  if (errorCount > 3) {
1920
- throw e;
2972
+ // throw e;
2973
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
1921
2974
  }
1922
2975
  // ignore
1923
2976
  }
@@ -1931,27 +2984,32 @@ class StableBrowser {
1931
2984
  async _highlightElements(scope, css) {
1932
2985
  try {
1933
2986
  if (!scope) {
2987
+ // console.log(`Scope is not defined`);
1934
2988
  return;
1935
2989
  }
1936
2990
  if (!css) {
1937
2991
  scope
1938
2992
  .evaluate((node) => {
1939
2993
  if (node && node.style) {
1940
- let originalBorder = node.style.border;
1941
- node.style.border = "2px solid red";
2994
+ let originalOutline = node.style.outline;
2995
+ // console.log(`Original outline was: ${originalOutline}`);
2996
+ // node.__previousOutline = originalOutline;
2997
+ node.style.outline = "2px solid red";
2998
+ // console.log(`New outline is: ${node.style.outline}`);
1942
2999
  if (window) {
1943
3000
  window.addEventListener("beforeunload", function (e) {
1944
- node.style.border = originalBorder;
3001
+ node.style.outline = originalOutline;
1945
3002
  });
1946
3003
  }
1947
3004
  setTimeout(function () {
1948
- node.style.border = originalBorder;
3005
+ node.style.outline = originalOutline;
1949
3006
  }, 2000);
1950
3007
  }
1951
3008
  })
1952
3009
  .then(() => { })
1953
3010
  .catch((e) => {
1954
3011
  // ignore
3012
+ // console.error(`Could not highlight node : ${e}`);
1955
3013
  });
1956
3014
  }
1957
3015
  else {
@@ -1967,17 +3025,18 @@ class StableBrowser {
1967
3025
  if (!element.style) {
1968
3026
  return;
1969
3027
  }
1970
- var originalBorder = element.style.border;
3028
+ let originalOutline = element.style.outline;
3029
+ element.__previousOutline = originalOutline;
1971
3030
  // Set the new border to be red and 2px solid
1972
- element.style.border = "2px solid red";
3031
+ element.style.outline = "2px solid red";
1973
3032
  if (window) {
1974
3033
  window.addEventListener("beforeunload", function (e) {
1975
- element.style.border = originalBorder;
3034
+ element.style.outline = originalOutline;
1976
3035
  });
1977
3036
  }
1978
3037
  // Set a timeout to revert to the original border after 2 seconds
1979
3038
  setTimeout(function () {
1980
- element.style.border = originalBorder;
3039
+ element.style.outline = originalOutline;
1981
3040
  }, 2000);
1982
3041
  }
1983
3042
  return;
@@ -1985,6 +3044,7 @@ class StableBrowser {
1985
3044
  .then(() => { })
1986
3045
  .catch((e) => {
1987
3046
  // ignore
3047
+ // console.error(`Could not highlight css: ${e}`);
1988
3048
  });
1989
3049
  }
1990
3050
  }
@@ -1992,8 +3052,49 @@ class StableBrowser {
1992
3052
  console.debug(error);
1993
3053
  }
1994
3054
  }
3055
+ _matcher(text) {
3056
+ if (!text) {
3057
+ return { matcher: "contains", queryText: "" };
3058
+ }
3059
+ if (text.length < 2) {
3060
+ return { matcher: "contains", queryText: text };
3061
+ }
3062
+ const split = text.split(":");
3063
+ const matcher = split[0].toLowerCase();
3064
+ const queryText = split.slice(1).join(":").trim();
3065
+ return { matcher, queryText };
3066
+ }
3067
+ _getDomain(url) {
3068
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
3069
+ return "";
3070
+ }
3071
+ let hostnameFragments = url.split("/")[2].split(".");
3072
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
3073
+ return hostnameFragments.join("-").split(":").join("-");
3074
+ }
3075
+ let n = hostnameFragments.length;
3076
+ let fragments = [...hostnameFragments];
3077
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
3078
+ hostnameFragments.pop();
3079
+ n = hostnameFragments.length;
3080
+ }
3081
+ if (n == 0) {
3082
+ if (fragments[0] === "www")
3083
+ fragments = fragments.slice(1);
3084
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
3085
+ }
3086
+ if (hostnameFragments[0] === "www")
3087
+ hostnameFragments = hostnameFragments.slice(1);
3088
+ return hostnameFragments.join(".");
3089
+ }
3090
+ /**
3091
+ * Verify the page path matches the given path.
3092
+ * @param {string} pathPart - The path to verify.
3093
+ * @param {object} options - Options for verification.
3094
+ * @param {object} world - The world context.
3095
+ * @returns {Promise<object>} - The state info after verification.
3096
+ */
1995
3097
  async verifyPagePath(pathPart, options = {}, world = null) {
1996
- const startTime = Date.now();
1997
3098
  let error = null;
1998
3099
  let screenshotId = null;
1999
3100
  let screenshotPath = null;
@@ -2007,159 +3108,534 @@ class StableBrowser {
2007
3108
  pathPart = newValue;
2008
3109
  }
2009
3110
  info.pathPart = pathPart;
3111
+ const { matcher, queryText } = this._matcher(pathPart);
3112
+ const state = {
3113
+ text_search: queryText,
3114
+ options,
3115
+ world,
3116
+ locate: false,
3117
+ scroll: false,
3118
+ highlight: false,
3119
+ type: Types.VERIFY_PAGE_PATH,
3120
+ text: `Verify the page url is ${queryText}`,
3121
+ _text: `Verify the page url is ${queryText}`,
3122
+ operation: "verifyPagePath",
3123
+ log: "***** verify page url is " + queryText + " *****\n",
3124
+ };
2010
3125
  try {
3126
+ await _preCommand(state, this);
3127
+ state.info.text = queryText;
2011
3128
  for (let i = 0; i < 30; i++) {
2012
3129
  const url = await this.page.url();
2013
- if (!url.includes(pathPart)) {
2014
- if (i === 29) {
2015
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
3130
+ switch (matcher) {
3131
+ case "exact":
3132
+ if (url !== queryText) {
3133
+ if (i === 29) {
3134
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
3135
+ }
3136
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3137
+ continue;
3138
+ }
3139
+ break;
3140
+ case "contains":
3141
+ if (!url.includes(queryText)) {
3142
+ if (i === 29) {
3143
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
3144
+ }
3145
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3146
+ continue;
3147
+ }
3148
+ break;
3149
+ case "starts-with":
3150
+ {
3151
+ const domain = this._getDomain(url);
3152
+ if (domain.length > 0 && domain !== queryText) {
3153
+ if (i === 29) {
3154
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
3155
+ }
3156
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3157
+ continue;
3158
+ }
3159
+ }
3160
+ break;
3161
+ case "ends-with":
3162
+ {
3163
+ const urlObj = new URL(url);
3164
+ let route = "/";
3165
+ if (urlObj.pathname !== "/") {
3166
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
3167
+ }
3168
+ else {
3169
+ route = "/";
3170
+ }
3171
+ if (route !== queryText) {
3172
+ if (i === 29) {
3173
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
3174
+ }
3175
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3176
+ continue;
3177
+ }
3178
+ }
3179
+ break;
3180
+ case "regex":
3181
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3182
+ if (!regex.test(url)) {
3183
+ if (i === 29) {
3184
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
3185
+ }
3186
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3187
+ continue;
3188
+ }
3189
+ break;
3190
+ default:
3191
+ console.log("Unknown matching type, defaulting to contains matching");
3192
+ if (!url.includes(pathPart)) {
3193
+ if (i === 29) {
3194
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
3195
+ }
3196
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3197
+ continue;
3198
+ }
3199
+ }
3200
+ await _screenshot(state, this);
3201
+ return state.info;
3202
+ }
3203
+ }
3204
+ catch (e) {
3205
+ state.info.failCause.lastError = e.message;
3206
+ state.info.failCause.assertionFailed = true;
3207
+ await _commandError(state, e, this);
3208
+ }
3209
+ finally {
3210
+ await _commandFinally(state, this);
3211
+ }
3212
+ }
3213
+ /**
3214
+ * Verify the page title matches the given title.
3215
+ * @param {string} title - The title to verify.
3216
+ * @param {object} options - Options for verification.
3217
+ * @param {object} world - The world context.
3218
+ * @returns {Promise<object>} - The state info after verification.
3219
+ */
3220
+ async verifyPageTitle(title, options = {}, world = null) {
3221
+ let error = null;
3222
+ let screenshotId = null;
3223
+ let screenshotPath = null;
3224
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3225
+ const newValue = await this._replaceWithLocalData(title, world);
3226
+ if (newValue !== title) {
3227
+ this.logger.info(title + "=" + newValue);
3228
+ title = newValue;
3229
+ }
3230
+ const { matcher, queryText } = this._matcher(title);
3231
+ const state = {
3232
+ text_search: queryText,
3233
+ options,
3234
+ world,
3235
+ locate: false,
3236
+ scroll: false,
3237
+ highlight: false,
3238
+ type: Types.VERIFY_PAGE_TITLE,
3239
+ text: `Verify the page title is ${queryText}`,
3240
+ _text: `Verify the page title is ${queryText}`,
3241
+ operation: "verifyPageTitle",
3242
+ log: "***** verify page title is " + queryText + " *****\n",
3243
+ };
3244
+ try {
3245
+ await _preCommand(state, this);
3246
+ state.info.text = queryText;
3247
+ for (let i = 0; i < 30; i++) {
3248
+ const foundTitle = await this.page.title();
3249
+ switch (matcher) {
3250
+ case "exact":
3251
+ if (foundTitle !== queryText) {
3252
+ if (i === 29) {
3253
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
3254
+ }
3255
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3256
+ continue;
3257
+ }
3258
+ break;
3259
+ case "contains":
3260
+ if (!foundTitle.includes(queryText)) {
3261
+ if (i === 29) {
3262
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
3263
+ }
3264
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3265
+ continue;
3266
+ }
3267
+ break;
3268
+ case "starts-with":
3269
+ if (!foundTitle.startsWith(queryText)) {
3270
+ if (i === 29) {
3271
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
3272
+ }
3273
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3274
+ continue;
3275
+ }
3276
+ break;
3277
+ case "ends-with":
3278
+ if (!foundTitle.endsWith(queryText)) {
3279
+ if (i === 29) {
3280
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
3281
+ }
3282
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3283
+ continue;
3284
+ }
3285
+ break;
3286
+ case "regex":
3287
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3288
+ if (!regex.test(foundTitle)) {
3289
+ if (i === 29) {
3290
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
3291
+ }
3292
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3293
+ continue;
3294
+ }
3295
+ break;
3296
+ default:
3297
+ console.log("Unknown matching type, defaulting to contains matching");
3298
+ if (!foundTitle.includes(title)) {
3299
+ if (i === 29) {
3300
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
3301
+ }
3302
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3303
+ continue;
3304
+ }
3305
+ }
3306
+ await _screenshot(state, this);
3307
+ return state.info;
3308
+ }
3309
+ }
3310
+ catch (e) {
3311
+ state.info.failCause.lastError = e.message;
3312
+ state.info.failCause.assertionFailed = true;
3313
+ await _commandError(state, e, this);
3314
+ }
3315
+ finally {
3316
+ await _commandFinally(state, this);
3317
+ }
3318
+ }
3319
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
3320
+ const frames = this.page.frames();
3321
+ let results = [];
3322
+ // let ignoreCase = false;
3323
+ for (let i = 0; i < frames.length; i++) {
3324
+ if (dateAlternatives.date) {
3325
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
3326
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3327
+ result.frame = frames[i];
3328
+ results.push(result);
3329
+ }
3330
+ }
3331
+ else if (numberAlternatives.number) {
3332
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
3333
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3334
+ result.frame = frames[i];
3335
+ results.push(result);
3336
+ }
3337
+ }
3338
+ else {
3339
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
3340
+ result.frame = frames[i];
3341
+ results.push(result);
3342
+ }
3343
+ }
3344
+ state.info.results = results;
3345
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3346
+ return resultWithElementsFound;
3347
+ }
3348
+ async verifyTextExistInPage(text, options = {}, world = null) {
3349
+ text = unEscapeString(text);
3350
+ const state = {
3351
+ text_search: text,
3352
+ options,
3353
+ world,
3354
+ locate: false,
3355
+ scroll: false,
3356
+ highlight: false,
3357
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
3358
+ text: `Verify the text '${maskValue(text)}' exists in page`,
3359
+ _text: `Verify the text '${text}' exists in page`,
3360
+ operation: "verifyTextExistInPage",
3361
+ log: "***** verify text " + text + " exists in page *****\n",
3362
+ };
3363
+ if (testForRegex(text)) {
3364
+ text = text.replace(/\\"/g, '"');
3365
+ }
3366
+ const timeout = this._getFindElementTimeout(options);
3367
+ //if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
3368
+ let stepFastMode = this.stepTags.includes("fast-mode");
3369
+ if (!stepFastMode) {
3370
+ if (!this.fastMode) {
3371
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3372
+ }
3373
+ else {
3374
+ await new Promise((resolve) => setTimeout(resolve, 500));
3375
+ }
3376
+ }
3377
+ const newValue = await this._replaceWithLocalData(text, world);
3378
+ if (newValue !== text) {
3379
+ this.logger.info(text + "=" + newValue);
3380
+ text = newValue;
3381
+ }
3382
+ let dateAlternatives = findDateAlternatives(text);
3383
+ let numberAlternatives = findNumberAlternatives(text);
3384
+ if (stepFastMode) {
3385
+ state.onlyFailuresScreenshot = true;
3386
+ state.scroll = false;
3387
+ state.highlight = false;
3388
+ }
3389
+ try {
3390
+ await _preCommand(state, this);
3391
+ state.info.text = text;
3392
+ while (true) {
3393
+ let resultWithElementsFound = {
3394
+ length: 0,
3395
+ };
3396
+ try {
3397
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3398
+ }
3399
+ catch (error) {
3400
+ // ignore
3401
+ }
3402
+ if (resultWithElementsFound.length === 0) {
3403
+ if (Date.now() - state.startTime > timeout) {
3404
+ throw new Error(`Text ${text} not found in page`);
2016
3405
  }
2017
3406
  await new Promise((resolve) => setTimeout(resolve, 1000));
2018
3407
  continue;
2019
3408
  }
2020
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2021
- return info;
3409
+ try {
3410
+ if (resultWithElementsFound[0].randomToken) {
3411
+ const frame = resultWithElementsFound[0].frame;
3412
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
3413
+ await this._highlightElements(frame, dataAttribute);
3414
+ const element = await frame.locator(dataAttribute).first();
3415
+ if (element) {
3416
+ await this.scrollIfNeeded(element, state.info);
3417
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3418
+ }
3419
+ }
3420
+ await _screenshot(state, this);
3421
+ return state.info;
3422
+ }
3423
+ catch (error) {
3424
+ console.error(error);
3425
+ }
2022
3426
  }
2023
3427
  }
2024
3428
  catch (e) {
2025
- //await this.closeUnexpectedPopups();
2026
- this.logger.error("verify page path failed " + info.log);
2027
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2028
- info.screenshotPath = screenshotPath;
2029
- Object.assign(e, { info: info });
2030
- error = e;
2031
- throw e;
3429
+ await _commandError(state, e, this);
2032
3430
  }
2033
3431
  finally {
2034
- const endTime = Date.now();
2035
- this._reportToWorld(world, {
2036
- type: Types.VERIFY_PAGE_PATH,
2037
- text: "Verify page path",
2038
- screenshotId,
2039
- result: error
2040
- ? {
2041
- status: "FAILED",
2042
- startTime,
2043
- endTime,
2044
- message: error?.message,
2045
- }
2046
- : {
2047
- status: "PASSED",
2048
- startTime,
2049
- endTime,
2050
- },
2051
- info: info,
2052
- });
3432
+ await _commandFinally(state, this);
3433
+ }
3434
+ }
3435
+ async waitForTextToDisappear(text, options = {}, world = null) {
3436
+ text = unEscapeString(text);
3437
+ const state = {
3438
+ text_search: text,
3439
+ options,
3440
+ world,
3441
+ locate: false,
3442
+ scroll: false,
3443
+ highlight: false,
3444
+ type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
3445
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
3446
+ _text: `Verify the text '${text}' does not exist in page`,
3447
+ operation: "verifyTextNotExistInPage",
3448
+ log: "***** verify text " + text + " does not exist in page *****\n",
3449
+ };
3450
+ if (testForRegex(text)) {
3451
+ text = text.replace(/\\"/g, '"');
3452
+ }
3453
+ const timeout = this._getFindElementTimeout(options);
3454
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3455
+ const newValue = await this._replaceWithLocalData(text, world);
3456
+ if (newValue !== text) {
3457
+ this.logger.info(text + "=" + newValue);
3458
+ text = newValue;
3459
+ }
3460
+ let dateAlternatives = findDateAlternatives(text);
3461
+ let numberAlternatives = findNumberAlternatives(text);
3462
+ try {
3463
+ await _preCommand(state, this);
3464
+ state.info.text = text;
3465
+ let resultWithElementsFound = {
3466
+ length: null, // initial cannot be 0
3467
+ };
3468
+ while (true) {
3469
+ try {
3470
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3471
+ }
3472
+ catch (error) {
3473
+ // ignore
3474
+ }
3475
+ if (resultWithElementsFound.length === 0) {
3476
+ await _screenshot(state, this);
3477
+ return state.info;
3478
+ }
3479
+ if (Date.now() - state.startTime > timeout) {
3480
+ throw new Error(`Text ${text} found in page`);
3481
+ }
3482
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3483
+ }
3484
+ }
3485
+ catch (e) {
3486
+ await _commandError(state, e, this);
3487
+ }
3488
+ finally {
3489
+ await _commandFinally(state, this);
2053
3490
  }
2054
3491
  }
2055
- async verifyTextExistInPage(text, options = {}, world = null) {
2056
- text = unEscapeString(text);
2057
- const startTime = Date.now();
2058
- const timeout = this._getLoadTimeout(options);
2059
- let error = null;
2060
- let screenshotId = null;
2061
- let screenshotPath = null;
3492
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
3493
+ textAnchor = unEscapeString(textAnchor);
3494
+ textToVerify = unEscapeString(textToVerify);
3495
+ const state = {
3496
+ text_search: textToVerify,
3497
+ options,
3498
+ world,
3499
+ locate: false,
3500
+ scroll: false,
3501
+ highlight: false,
3502
+ type: Types.VERIFY_TEXT_WITH_RELATION,
3503
+ text: `Verify text with relation to another text`,
3504
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
3505
+ operation: "verify_text_with_relation",
3506
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3507
+ };
3508
+ const cmdStartTime = Date.now();
3509
+ let cmdEndTime = null;
3510
+ const timeout = this._getFindElementTimeout(options);
2062
3511
  await new Promise((resolve) => setTimeout(resolve, 2000));
2063
- const info = {};
2064
- info.log = "***** verify text " + text + " exists in page *****\n";
2065
- info.operation = "verifyTextExistInPage";
2066
- const newValue = await this._replaceWithLocalData(text, world);
2067
- if (newValue !== text) {
2068
- this.logger.info(text + "=" + newValue);
2069
- text = newValue;
2070
- }
2071
- info.text = text;
2072
- let dateAlternatives = findDateAlternatives(text);
2073
- let numberAlternatives = findNumberAlternatives(text);
3512
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
3513
+ if (newValue !== textAnchor) {
3514
+ this.logger.info(textAnchor + "=" + newValue);
3515
+ textAnchor = newValue;
3516
+ }
3517
+ newValue = await this._replaceWithLocalData(textToVerify, world);
3518
+ if (newValue !== textToVerify) {
3519
+ this.logger.info(textToVerify + "=" + newValue);
3520
+ textToVerify = newValue;
3521
+ }
3522
+ let dateAlternatives = findDateAlternatives(textToVerify);
3523
+ let numberAlternatives = findNumberAlternatives(textToVerify);
3524
+ let foundAncore = false;
2074
3525
  try {
3526
+ await _preCommand(state, this);
3527
+ state.info.text = textToVerify;
3528
+ let resultWithElementsFound = {
3529
+ length: 0,
3530
+ };
2075
3531
  while (true) {
2076
- const frames = this.page.frames();
2077
- let results = [];
2078
- for (let i = 0; i < frames.length; i++) {
2079
- if (dateAlternatives.date) {
2080
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2081
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
2082
- result.frame = frames[i];
2083
- results.push(result);
2084
- }
2085
- }
2086
- else if (numberAlternatives.number) {
2087
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2088
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
2089
- result.frame = frames[i];
2090
- results.push(result);
2091
- }
2092
- }
2093
- else {
2094
- const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
2095
- result.frame = frames[i];
2096
- results.push(result);
2097
- }
3532
+ try {
3533
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
3534
+ }
3535
+ catch (error) {
3536
+ // ignore
2098
3537
  }
2099
- info.results = results;
2100
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2101
3538
  if (resultWithElementsFound.length === 0) {
2102
- if (Date.now() - startTime > timeout) {
2103
- throw new Error(`Text ${text} not found in page`);
3539
+ if (Date.now() - state.startTime > timeout) {
3540
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2104
3541
  }
2105
3542
  await new Promise((resolve) => setTimeout(resolve, 1000));
2106
3543
  continue;
2107
3544
  }
2108
- if (resultWithElementsFound[0].randomToken) {
2109
- const frame = resultWithElementsFound[0].frame;
2110
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2111
- await this._highlightElements(frame, dataAttribute);
2112
- const element = await frame.$(dataAttribute);
2113
- if (element) {
2114
- await this.scrollIfNeeded(element, info);
2115
- await element.dispatchEvent("bvt_verify_page_contains_text");
3545
+ else {
3546
+ cmdEndTime = Date.now();
3547
+ if (cmdEndTime - cmdStartTime > 55000) {
3548
+ if (foundAncore) {
3549
+ throw new Error(`Text ${textToVerify} not found in page`);
3550
+ }
3551
+ else {
3552
+ throw new Error(`Text ${textAnchor} not found in page`);
3553
+ }
3554
+ }
3555
+ }
3556
+ try {
3557
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
3558
+ foundAncore = true;
3559
+ const result = resultWithElementsFound[i];
3560
+ const token = result.randomToken;
3561
+ const frame = result.frame;
3562
+ let css = `[data-blinq-id-${token}]`;
3563
+ const climbArray1 = [];
3564
+ for (let i = 0; i < climb; i++) {
3565
+ climbArray1.push("..");
3566
+ }
3567
+ let climbXpath = "xpath=" + climbArray1.join("/");
3568
+ css = css + " >> " + climbXpath;
3569
+ const count = await frame.locator(css).count();
3570
+ for (let j = 0; j < count; j++) {
3571
+ const continer = await frame.locator(css).nth(j);
3572
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
3573
+ if (result.elementCount > 0) {
3574
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
3575
+ await this._highlightElements(frame, dataAttribute);
3576
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
3577
+ // if (world && world.screenshot && !world.screenshotPath) {
3578
+ // console.log(`Highlighting for vtrt while running from recorder`);
3579
+ // this._highlightElements(frame, dataAttribute)
3580
+ // .then(async () => {
3581
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
3582
+ // this._unhighlightElements(frame, dataAttribute).then(
3583
+ // () => {}
3584
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
3585
+ // );
3586
+ // })
3587
+ // .catch(e);
3588
+ // }
3589
+ //await this._highlightElements(frame, cssAnchor);
3590
+ const element = await frame.locator(dataAttribute).first();
3591
+ // await new Promise((resolve) => setTimeout(resolve, 100));
3592
+ // await this._unhighlightElements(frame, dataAttribute);
3593
+ if (element) {
3594
+ await this.scrollIfNeeded(element, state.info);
3595
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3596
+ }
3597
+ await _screenshot(state, this);
3598
+ return state.info;
3599
+ }
3600
+ }
2116
3601
  }
2117
3602
  }
2118
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2119
- return info;
3603
+ catch (error) {
3604
+ console.error(error);
3605
+ }
2120
3606
  }
2121
3607
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2122
3608
  }
2123
3609
  catch (e) {
2124
- //await this.closeUnexpectedPopups();
2125
- this.logger.error("verify text exist in page failed " + info.log);
2126
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2127
- info.screenshotPath = screenshotPath;
2128
- Object.assign(e, { info: info });
2129
- error = e;
2130
- throw e;
3610
+ await _commandError(state, e, this);
2131
3611
  }
2132
3612
  finally {
2133
- const endTime = Date.now();
2134
- this._reportToWorld(world, {
2135
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2136
- text: "Verify text exists in page",
2137
- screenshotId,
2138
- result: error
2139
- ? {
2140
- status: "FAILED",
2141
- startTime,
2142
- endTime,
2143
- message: error?.message,
2144
- }
2145
- : {
2146
- status: "PASSED",
2147
- startTime,
2148
- endTime,
2149
- },
2150
- info: info,
2151
- });
3613
+ await _commandFinally(state, this);
2152
3614
  }
2153
3615
  }
2154
- _getServerUrl() {
2155
- let serviceUrl = "https://api.blinq.io";
2156
- if (process.env.NODE_ENV_BLINQ === "dev") {
2157
- serviceUrl = "https://dev.api.blinq.io";
2158
- }
2159
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2160
- serviceUrl = "https://stage.api.blinq.io";
3616
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
3617
+ const frames = this.page.frames();
3618
+ let results = [];
3619
+ let ignoreCase = false;
3620
+ for (let i = 0; i < frames.length; i++) {
3621
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
3622
+ result.frame = frames[i];
3623
+ const climbArray = [];
3624
+ for (let i = 0; i < climb; i++) {
3625
+ climbArray.push("..");
3626
+ }
3627
+ let climbXpath = "xpath=" + climbArray.join("/");
3628
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
3629
+ const count = await frames[i].locator(newLocator).count();
3630
+ if (count > 0) {
3631
+ result.elementCount = count;
3632
+ result.locator = newLocator;
3633
+ results.push(result);
3634
+ }
2161
3635
  }
2162
- return serviceUrl;
3636
+ // state.info.results = results;
3637
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3638
+ return resultWithElementsFound;
2163
3639
  }
2164
3640
  async visualVerification(text, options = {}, world = null) {
2165
3641
  const startTime = Date.now();
@@ -2175,14 +3651,17 @@ class StableBrowser {
2175
3651
  throw new Error("TOKEN is not set");
2176
3652
  }
2177
3653
  try {
2178
- let serviceUrl = this._getServerUrl();
3654
+ let serviceUrl = _getServerUrl();
2179
3655
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2180
3656
  info.screenshotPath = screenshotPath;
2181
3657
  const screenshot = await this.takeScreenshot();
2182
- const request = {
2183
- method: "POST",
3658
+ let request = {
3659
+ method: "post",
3660
+ maxBodyLength: Infinity,
2184
3661
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2185
3662
  headers: {
3663
+ "x-bvt-project-id": path.basename(this.project_path),
3664
+ "x-source": "aaa",
2186
3665
  "Content-Type": "application/json",
2187
3666
  Authorization: `Bearer ${process.env.TOKEN}`,
2188
3667
  },
@@ -2191,7 +3670,7 @@ class StableBrowser {
2191
3670
  screenshot: screenshot,
2192
3671
  }),
2193
3672
  };
2194
- let result = await this.context.api.request(request);
3673
+ const result = await axios.request(request);
2195
3674
  if (result.data.status !== true) {
2196
3675
  throw new Error("Visual validation failed");
2197
3676
  }
@@ -2211,13 +3690,15 @@ class StableBrowser {
2211
3690
  info.screenshotPath = screenshotPath;
2212
3691
  Object.assign(e, { info: info });
2213
3692
  error = e;
2214
- throw e;
3693
+ // throw e;
3694
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
2215
3695
  }
2216
3696
  finally {
2217
3697
  const endTime = Date.now();
2218
- this._reportToWorld(world, {
3698
+ _reportToWorld(world, {
2219
3699
  type: Types.VERIFY_VISUAL,
2220
3700
  text: "Visual verification",
3701
+ _text: "Visual verification of " + text,
2221
3702
  screenshotId,
2222
3703
  result: error
2223
3704
  ? {
@@ -2263,6 +3744,7 @@ class StableBrowser {
2263
3744
  let screenshotPath = null;
2264
3745
  const info = {};
2265
3746
  info.log = "";
3747
+ info.locatorLog = new LocatorLog(selectors);
2266
3748
  info.operation = "getTableData";
2267
3749
  info.selectors = selectors;
2268
3750
  try {
@@ -2278,11 +3760,12 @@ class StableBrowser {
2278
3760
  info.screenshotPath = screenshotPath;
2279
3761
  Object.assign(e, { info: info });
2280
3762
  error = e;
2281
- throw e;
3763
+ // throw e;
3764
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2282
3765
  }
2283
3766
  finally {
2284
3767
  const endTime = Date.now();
2285
- this._reportToWorld(world, {
3768
+ _reportToWorld(world, {
2286
3769
  element_name: selectors.element_name,
2287
3770
  type: Types.GET_TABLE_DATA,
2288
3771
  text: "Get table data",
@@ -2337,7 +3820,7 @@ class StableBrowser {
2337
3820
  info.operation = "analyzeTable";
2338
3821
  info.selectors = selectors;
2339
3822
  info.query = query;
2340
- query = this._fixUsingParams(query, _params);
3823
+ query = _fixUsingParams(query, _params);
2341
3824
  info.query_fixed = query;
2342
3825
  info.operator = operator;
2343
3826
  info.value = value;
@@ -2443,11 +3926,12 @@ class StableBrowser {
2443
3926
  info.screenshotPath = screenshotPath;
2444
3927
  Object.assign(e, { info: info });
2445
3928
  error = e;
2446
- throw e;
3929
+ // throw e;
3930
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2447
3931
  }
2448
3932
  finally {
2449
3933
  const endTime = Date.now();
2450
- this._reportToWorld(world, {
3934
+ _reportToWorld(world, {
2451
3935
  element_name: selectors.element_name,
2452
3936
  type: Types.ANALYZE_TABLE,
2453
3937
  text: "Analyze table",
@@ -2468,8 +3952,51 @@ class StableBrowser {
2468
3952
  });
2469
3953
  }
2470
3954
  }
3955
+ /**
3956
+ * Explicit wait/sleep function that pauses execution for a specified duration
3957
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3958
+ * @param options - Optional configuration object
3959
+ * @param world - Optional world context
3960
+ * @returns Promise that resolves after the specified duration
3961
+ */
3962
+ async sleep(duration = 1000, options = {}, world = null) {
3963
+ const state = {
3964
+ duration,
3965
+ options,
3966
+ world,
3967
+ locate: false,
3968
+ scroll: false,
3969
+ screenshot: false,
3970
+ highlight: false,
3971
+ type: Types.SLEEP,
3972
+ text: `Sleep for ${duration} ms`,
3973
+ _text: `Sleep for ${duration} ms`,
3974
+ operation: "sleep",
3975
+ log: `***** Sleep for ${duration} ms *****\n`,
3976
+ };
3977
+ try {
3978
+ await _preCommand(state, this);
3979
+ if (duration < 0) {
3980
+ throw new Error("Sleep duration cannot be negative");
3981
+ }
3982
+ await new Promise((resolve) => setTimeout(resolve, duration));
3983
+ return state.info;
3984
+ }
3985
+ catch (e) {
3986
+ await _commandError(state, e, this);
3987
+ }
3988
+ finally {
3989
+ await _commandFinally(state, this);
3990
+ }
3991
+ }
2471
3992
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2472
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3993
+ try {
3994
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3995
+ }
3996
+ catch (error) {
3997
+ this.logger.debug(error);
3998
+ throw error;
3999
+ }
2473
4000
  }
2474
4001
  _getLoadTimeout(options) {
2475
4002
  let timeout = 15000;
@@ -2481,7 +4008,54 @@ class StableBrowser {
2481
4008
  }
2482
4009
  return timeout;
2483
4010
  }
4011
+ _getFindElementTimeout(options) {
4012
+ if (options && options.timeout) {
4013
+ return options.timeout;
4014
+ }
4015
+ if (this.configuration.find_element_timeout) {
4016
+ return this.configuration.find_element_timeout;
4017
+ }
4018
+ return 30000;
4019
+ }
4020
+ async saveStoreState(path = null, world = null) {
4021
+ const storageState = await this.page.context().storageState();
4022
+ path = await this._replaceWithLocalData(path, this.world);
4023
+ //const testDataFile = _getDataFile(world, this.context, this);
4024
+ if (path) {
4025
+ // save { storageState: storageState } into the path
4026
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
4027
+ }
4028
+ else {
4029
+ await this.setTestData({ storageState: storageState }, world);
4030
+ }
4031
+ }
4032
+ async restoreSaveState(path = null, world = null) {
4033
+ path = await this._replaceWithLocalData(path, this.world);
4034
+ await refreshBrowser(this, path, world);
4035
+ this.registerEventListeners(this.context);
4036
+ registerNetworkEvents(this.world, this, this.context, this.page);
4037
+ registerDownloadEvent(this.page, this.world, this.context);
4038
+ if (this.onRestoreSaveState) {
4039
+ this.onRestoreSaveState(path);
4040
+ }
4041
+ }
2484
4042
  async waitForPageLoad(options = {}, world = null) {
4043
+ // try {
4044
+ // let currentPagePath = null;
4045
+ // currentPagePath = new URL(this.page.url()).pathname;
4046
+ // if (this.latestPagePath) {
4047
+ // // get the currect page path and compare with the latest page path
4048
+ // if (this.latestPagePath === currentPagePath) {
4049
+ // // if the page path is the same, do not wait for page load
4050
+ // console.log("No page change: " + currentPagePath);
4051
+ // return;
4052
+ // }
4053
+ // }
4054
+ // this.latestPagePath = currentPagePath;
4055
+ // } catch (e) {
4056
+ // console.debug("Error getting current page path: ", e);
4057
+ // }
4058
+ //console.log("Waiting for page load");
2485
4059
  let timeout = this._getLoadTimeout(options);
2486
4060
  const promiseArray = [];
2487
4061
  // let waitForNetworkIdle = true;
@@ -2514,13 +4088,15 @@ class StableBrowser {
2514
4088
  else if (e.label === "domcontentloaded") {
2515
4089
  console.log("waited for the domcontent loaded timeout");
2516
4090
  }
2517
- console.log(".");
2518
4091
  }
2519
4092
  finally {
2520
- await new Promise((resolve) => setTimeout(resolve, 2000));
4093
+ await new Promise((resolve) => setTimeout(resolve, 500));
4094
+ if (options && !options.noSleep) {
4095
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4096
+ }
2521
4097
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2522
4098
  const endTime = Date.now();
2523
- this._reportToWorld(world, {
4099
+ _reportToWorld(world, {
2524
4100
  type: Types.GET_PAGE_STATUS,
2525
4101
  text: "Wait for page load",
2526
4102
  screenshotId,
@@ -2540,41 +4116,139 @@ class StableBrowser {
2540
4116
  }
2541
4117
  }
2542
4118
  async closePage(options = {}, world = null) {
2543
- const startTime = Date.now();
2544
- let error = null;
2545
- let screenshotId = null;
2546
- let screenshotPath = null;
2547
- const info = {};
4119
+ const state = {
4120
+ options,
4121
+ world,
4122
+ locate: false,
4123
+ scroll: false,
4124
+ highlight: false,
4125
+ type: Types.CLOSE_PAGE,
4126
+ text: `Close page`,
4127
+ _text: `Close the page`,
4128
+ operation: "closePage",
4129
+ log: "***** close page *****\n",
4130
+ throwError: false,
4131
+ };
2548
4132
  try {
4133
+ await _preCommand(state, this);
2549
4134
  await this.page.close();
2550
4135
  }
2551
4136
  catch (e) {
2552
- console.log(".");
4137
+ await _commandError(state, e, this);
2553
4138
  }
2554
4139
  finally {
2555
- await new Promise((resolve) => setTimeout(resolve, 2000));
2556
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2557
- const endTime = Date.now();
2558
- this._reportToWorld(world, {
2559
- type: Types.CLOSE_PAGE,
2560
- text: "close page",
2561
- screenshotId,
2562
- result: error
2563
- ? {
2564
- status: "FAILED",
2565
- startTime,
2566
- endTime,
2567
- message: error?.message,
4140
+ await _commandFinally(state, this);
4141
+ }
4142
+ }
4143
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
4144
+ let operation = null;
4145
+ if (!options || !options.operation) {
4146
+ throw new Error("operation is not defined");
4147
+ }
4148
+ operation = options.operation;
4149
+ // validate operation is one of the supported operations
4150
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
4151
+ throw new Error("operation is not supported");
4152
+ }
4153
+ const state = {
4154
+ options,
4155
+ world,
4156
+ locate: false,
4157
+ scroll: false,
4158
+ highlight: false,
4159
+ type: Types.TABLE_OPERATION,
4160
+ text: `Table operation`,
4161
+ _text: `Table ${operation} operation`,
4162
+ operation: operation,
4163
+ log: "***** Table operation *****\n",
4164
+ };
4165
+ const timeout = this._getFindElementTimeout(options);
4166
+ try {
4167
+ await _preCommand(state, this);
4168
+ const start = Date.now();
4169
+ let cellArea = null;
4170
+ while (true) {
4171
+ try {
4172
+ cellArea = await _findCellArea(headerText, rowText, this, state);
4173
+ if (cellArea) {
4174
+ break;
2568
4175
  }
2569
- : {
2570
- status: "PASSED",
2571
- startTime,
2572
- endTime,
2573
- },
2574
- info: info,
2575
- });
4176
+ }
4177
+ catch (e) {
4178
+ // ignore
4179
+ }
4180
+ if (Date.now() - start > timeout) {
4181
+ throw new Error(`Cell not found in table`);
4182
+ }
4183
+ await new Promise((resolve) => setTimeout(resolve, 1000));
4184
+ }
4185
+ switch (operation) {
4186
+ case "click":
4187
+ if (!options.css) {
4188
+ // will click in the center of the cell
4189
+ let xOffset = 0;
4190
+ let yOffset = 0;
4191
+ if (options.xOffset) {
4192
+ xOffset = options.xOffset;
4193
+ }
4194
+ if (options.yOffset) {
4195
+ yOffset = options.yOffset;
4196
+ }
4197
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
4198
+ }
4199
+ else {
4200
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4201
+ if (results.length === 0) {
4202
+ throw new Error(`Element not found in cell area`);
4203
+ }
4204
+ state.element = results[0];
4205
+ await performAction("click", state.element, options, this, state, _params);
4206
+ }
4207
+ break;
4208
+ case "hover+click":
4209
+ if (!options.css) {
4210
+ throw new Error("css is not defined");
4211
+ }
4212
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4213
+ if (results.length === 0) {
4214
+ throw new Error(`Element not found in cell area`);
4215
+ }
4216
+ state.element = results[0];
4217
+ await performAction("hover+click", state.element, options, this, state, _params);
4218
+ break;
4219
+ case "hover":
4220
+ if (!options.css) {
4221
+ throw new Error("css is not defined");
4222
+ }
4223
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4224
+ if (result1.length === 0) {
4225
+ throw new Error(`Element not found in cell area`);
4226
+ }
4227
+ state.element = result1[0];
4228
+ await performAction("hover", state.element, options, this, state, _params);
4229
+ break;
4230
+ default:
4231
+ throw new Error("operation is not supported");
4232
+ }
4233
+ }
4234
+ catch (e) {
4235
+ await _commandError(state, e, this);
4236
+ }
4237
+ finally {
4238
+ await _commandFinally(state, this);
2576
4239
  }
2577
4240
  }
4241
+ saveTestDataAsGlobal(options, world) {
4242
+ const dataFile = _getDataFile(world, this.context, this);
4243
+ if (process.env.MODE === "executions") {
4244
+ const globalDataFile = path.join(this.project_path, "global_test_data.json");
4245
+ fs.copyFileSync(dataFile, globalDataFile);
4246
+ this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
4247
+ return;
4248
+ }
4249
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
4250
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
4251
+ }
2578
4252
  async setViewportSize(width, hight, options = {}, world = null) {
2579
4253
  const startTime = Date.now();
2580
4254
  let error = null;
@@ -2591,15 +4265,16 @@ class StableBrowser {
2591
4265
  await this.page.setViewportSize({ width: width, height: hight });
2592
4266
  }
2593
4267
  catch (e) {
2594
- console.log(".");
4268
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2595
4269
  }
2596
4270
  finally {
2597
4271
  await new Promise((resolve) => setTimeout(resolve, 2000));
2598
4272
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2599
4273
  const endTime = Date.now();
2600
- this._reportToWorld(world, {
4274
+ _reportToWorld(world, {
2601
4275
  type: Types.SET_VIEWPORT,
2602
4276
  text: "set viewport size to " + width + "x" + hight,
4277
+ _text: "Set the viewport size to " + width + "x" + hight,
2603
4278
  screenshotId,
2604
4279
  result: error
2605
4280
  ? {
@@ -2627,13 +4302,13 @@ class StableBrowser {
2627
4302
  await this.page.reload();
2628
4303
  }
2629
4304
  catch (e) {
2630
- console.log(".");
4305
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2631
4306
  }
2632
4307
  finally {
2633
4308
  await new Promise((resolve) => setTimeout(resolve, 2000));
2634
4309
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2635
4310
  const endTime = Date.now();
2636
- this._reportToWorld(world, {
4311
+ _reportToWorld(world, {
2637
4312
  type: Types.GET_PAGE_STATUS,
2638
4313
  text: "page relaod",
2639
4314
  screenshotId,
@@ -2669,11 +4344,246 @@ class StableBrowser {
2669
4344
  console.log("#-#");
2670
4345
  }
2671
4346
  }
2672
- _reportToWorld(world, properties) {
2673
- if (!world || !world.attach) {
2674
- return;
4347
+ async beforeScenario(world, scenario) {
4348
+ if (world && world.attach) {
4349
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4350
+ }
4351
+ this.context.loadedRoutes = null;
4352
+ this.beforeScenarioCalled = true;
4353
+ if (scenario && scenario.pickle && scenario.pickle.name) {
4354
+ this.scenarioName = scenario.pickle.name;
4355
+ }
4356
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
4357
+ this.featureName = scenario.gherkinDocument.feature.name;
4358
+ }
4359
+ if (this.context) {
4360
+ this.context.examplesRow = extractStepExampleParameters(scenario);
4361
+ }
4362
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
4363
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
4364
+ // check if @global_test_data tag is present
4365
+ if (this.tags.includes("@global_test_data")) {
4366
+ this.saveTestDataAsGlobal({}, world);
4367
+ }
4368
+ }
4369
+ // update test data based on feature/scenario
4370
+ let envName = null;
4371
+ if (this.context && this.context.environment) {
4372
+ envName = this.context.environment.name;
4373
+ }
4374
+ if (!process.env.TEMP_RUN) {
4375
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
4376
+ }
4377
+ await loadBrunoParams(this.context, this.context.environment.name);
4378
+ }
4379
+ async afterScenario(world, scenario) { }
4380
+ async beforeStep(world, step) {
4381
+ this.stepTags = [];
4382
+ if (!this.beforeScenarioCalled) {
4383
+ this.beforeScenario(world, step);
4384
+ this.context.loadedRoutes = null;
4385
+ }
4386
+ if (this.stepIndex === undefined) {
4387
+ this.stepIndex = 0;
4388
+ }
4389
+ else {
4390
+ this.stepIndex++;
4391
+ }
4392
+ if (step && step.pickleStep && step.pickleStep.text) {
4393
+ this.stepName = step.pickleStep.text;
4394
+ let printableStepName = this.stepName;
4395
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4396
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4397
+ return `\x1b[33m"${p1}"\x1b[0m`;
4398
+ });
4399
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4400
+ }
4401
+ else if (step && step.text) {
4402
+ this.stepName = step.text;
4403
+ }
4404
+ else {
4405
+ this.stepName = "step " + this.stepIndex;
4406
+ }
4407
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4408
+ if (this.context.browserObject.context) {
4409
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
4410
+ }
4411
+ }
4412
+ if (this.initSnapshotTaken === false) {
4413
+ this.initSnapshotTaken = true;
4414
+ if (world &&
4415
+ world.attach &&
4416
+ !process.env.DISABLE_SNAPSHOT &&
4417
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4418
+ const snapshot = await this.getAriaSnapshot();
4419
+ if (snapshot) {
4420
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
4421
+ }
4422
+ }
4423
+ }
4424
+ this.context.routeResults = null;
4425
+ this.context.loadedRoutes = null;
4426
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4427
+ networkBeforeStep(this.stepName, this.context);
4428
+ this.inStepReport = false;
4429
+ }
4430
+ setStepTags(tags) {
4431
+ this.stepTags = tags;
4432
+ }
4433
+ async getAriaSnapshot() {
4434
+ try {
4435
+ // find the page url
4436
+ const url = await this.page.url();
4437
+ // extract the path from the url
4438
+ const path = new URL(url).pathname;
4439
+ // get the page title
4440
+ const title = await this.page.title();
4441
+ // go over other frams
4442
+ const frames = this.page.frames();
4443
+ const snapshots = [];
4444
+ const content = [`- path: ${path}`, `- title: ${title}`];
4445
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
4446
+ for (let i = 0; i < frames.length; i++) {
4447
+ const frame = frames[i];
4448
+ try {
4449
+ // Ensure frame is attached and has body
4450
+ const body = frame.locator("body");
4451
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4452
+ const snapshot = await body.ariaSnapshot({ timeout });
4453
+ if (!snapshot) {
4454
+ continue;
4455
+ }
4456
+ content.push(`- frame: ${i}`);
4457
+ content.push(snapshot);
4458
+ }
4459
+ catch (innerErr) {
4460
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4461
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4462
+ }
4463
+ }
4464
+ return content.join("\n");
4465
+ }
4466
+ catch (e) {
4467
+ console.log("Error in getAriaSnapshot");
4468
+ //console.debug(e);
4469
+ }
4470
+ return null;
4471
+ }
4472
+ /**
4473
+ * Sends command with custom payload to report.
4474
+ * @param commandText - Title of the command to be shown in the report.
4475
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
4476
+ * @param content - Content of the command to be shown in the report.
4477
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
4478
+ * @param world - Optional world context.
4479
+ * @public
4480
+ */
4481
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
4482
+ const state = {
4483
+ options,
4484
+ world,
4485
+ locate: false,
4486
+ scroll: false,
4487
+ screenshot: options.screenshot ?? false,
4488
+ highlight: options.highlight ?? false,
4489
+ type: Types.REPORT_COMMAND,
4490
+ text: commandText,
4491
+ _text: commandText,
4492
+ operation: "report_command",
4493
+ log: "***** " + commandText + " *****\n",
4494
+ };
4495
+ try {
4496
+ await _preCommand(state, this);
4497
+ const payload = {
4498
+ type: options.type ?? "text",
4499
+ content: content,
4500
+ screenshotId: null,
4501
+ };
4502
+ state.payload = payload;
4503
+ if (commandStatus === "FAILED") {
4504
+ state.throwError = true;
4505
+ throw new Error(commandText);
4506
+ }
4507
+ }
4508
+ catch (e) {
4509
+ await _commandError(state, e, this);
4510
+ }
4511
+ finally {
4512
+ await _commandFinally(state, this);
4513
+ }
4514
+ }
4515
+ async afterStep(world, step, result) {
4516
+ this.stepName = null;
4517
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4518
+ if (this.context.browserObject.context) {
4519
+ await this.context.browserObject.context.tracing.stopChunk({
4520
+ path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
4521
+ });
4522
+ if (world && world.attach) {
4523
+ await world.attach(JSON.stringify({
4524
+ type: "trace",
4525
+ traceFilePath: `trace-${this.stepIndex}.zip`,
4526
+ }), "application/json+trace");
4527
+ }
4528
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
4529
+ }
4530
+ }
4531
+ if (this.context) {
4532
+ this.context.examplesRow = null;
4533
+ }
4534
+ if (!this.inStepReport) {
4535
+ // check the step result
4536
+ if (result && result.status === "FAILED" && world && world.attach) {
4537
+ await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
4538
+ }
4539
+ }
4540
+ if (world &&
4541
+ world.attach &&
4542
+ !process.env.DISABLE_SNAPSHOT &&
4543
+ !this.fastMode &&
4544
+ !this.stepTags.includes("fast-mode")) {
4545
+ const snapshot = await this.getAriaSnapshot();
4546
+ if (snapshot) {
4547
+ const obj = {};
4548
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4549
+ }
4550
+ }
4551
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4552
+ if (this.context.routeResults) {
4553
+ if (world && world.attach) {
4554
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4555
+ }
4556
+ }
4557
+ if (!process.env.TEMP_RUN) {
4558
+ const state = {
4559
+ world,
4560
+ locate: false,
4561
+ scroll: false,
4562
+ screenshot: true,
4563
+ highlight: true,
4564
+ type: Types.STEP_COMPLETE,
4565
+ text: "end of scenario",
4566
+ _text: "end of scenario",
4567
+ operation: "step_complete",
4568
+ log: "***** " + "end of scenario" + " *****\n",
4569
+ };
4570
+ try {
4571
+ await _preCommand(state, this);
4572
+ }
4573
+ catch (e) {
4574
+ await _commandError(state, e, this);
4575
+ }
4576
+ finally {
4577
+ await _commandFinally(state, this);
4578
+ }
4579
+ }
4580
+ networkAfterStep(this.stepName, this.context);
4581
+ if (process.env.TEMP_RUN === "true") {
4582
+ // Put a sleep for some time to allow the browser to finish processing
4583
+ if (!this.stepTags.includes("fast-mode")) {
4584
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4585
+ }
2675
4586
  }
2676
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2677
4587
  }
2678
4588
  }
2679
4589
  function createTimedPromise(promise, label) {
@@ -2681,156 +4591,5 @@ function createTimedPromise(promise, label) {
2681
4591
  .then((result) => ({ status: "fulfilled", label, result }))
2682
4592
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2683
4593
  }
2684
- const KEYBOARD_EVENTS = [
2685
- "ALT",
2686
- "AltGraph",
2687
- "CapsLock",
2688
- "Control",
2689
- "Fn",
2690
- "FnLock",
2691
- "Hyper",
2692
- "Meta",
2693
- "NumLock",
2694
- "ScrollLock",
2695
- "Shift",
2696
- "Super",
2697
- "Symbol",
2698
- "SymbolLock",
2699
- "Enter",
2700
- "Tab",
2701
- "ArrowDown",
2702
- "ArrowLeft",
2703
- "ArrowRight",
2704
- "ArrowUp",
2705
- "End",
2706
- "Home",
2707
- "PageDown",
2708
- "PageUp",
2709
- "Backspace",
2710
- "Clear",
2711
- "Copy",
2712
- "CrSel",
2713
- "Cut",
2714
- "Delete",
2715
- "EraseEof",
2716
- "ExSel",
2717
- "Insert",
2718
- "Paste",
2719
- "Redo",
2720
- "Undo",
2721
- "Accept",
2722
- "Again",
2723
- "Attn",
2724
- "Cancel",
2725
- "ContextMenu",
2726
- "Escape",
2727
- "Execute",
2728
- "Find",
2729
- "Finish",
2730
- "Help",
2731
- "Pause",
2732
- "Play",
2733
- "Props",
2734
- "Select",
2735
- "ZoomIn",
2736
- "ZoomOut",
2737
- "BrightnessDown",
2738
- "BrightnessUp",
2739
- "Eject",
2740
- "LogOff",
2741
- "Power",
2742
- "PowerOff",
2743
- "PrintScreen",
2744
- "Hibernate",
2745
- "Standby",
2746
- "WakeUp",
2747
- "AllCandidates",
2748
- "Alphanumeric",
2749
- "CodeInput",
2750
- "Compose",
2751
- "Convert",
2752
- "Dead",
2753
- "FinalMode",
2754
- "GroupFirst",
2755
- "GroupLast",
2756
- "GroupNext",
2757
- "GroupPrevious",
2758
- "ModeChange",
2759
- "NextCandidate",
2760
- "NonConvert",
2761
- "PreviousCandidate",
2762
- "Process",
2763
- "SingleCandidate",
2764
- "HangulMode",
2765
- "HanjaMode",
2766
- "JunjaMode",
2767
- "Eisu",
2768
- "Hankaku",
2769
- "Hiragana",
2770
- "HiraganaKatakana",
2771
- "KanaMode",
2772
- "KanjiMode",
2773
- "Katakana",
2774
- "Romaji",
2775
- "Zenkaku",
2776
- "ZenkakuHanaku",
2777
- "F1",
2778
- "F2",
2779
- "F3",
2780
- "F4",
2781
- "F5",
2782
- "F6",
2783
- "F7",
2784
- "F8",
2785
- "F9",
2786
- "F10",
2787
- "F11",
2788
- "F12",
2789
- "Soft1",
2790
- "Soft2",
2791
- "Soft3",
2792
- "Soft4",
2793
- "ChannelDown",
2794
- "ChannelUp",
2795
- "Close",
2796
- "MailForward",
2797
- "MailReply",
2798
- "MailSend",
2799
- "MediaFastForward",
2800
- "MediaPause",
2801
- "MediaPlay",
2802
- "MediaPlayPause",
2803
- "MediaRecord",
2804
- "MediaRewind",
2805
- "MediaStop",
2806
- "MediaTrackNext",
2807
- "MediaTrackPrevious",
2808
- "AudioBalanceLeft",
2809
- "AudioBalanceRight",
2810
- "AudioBassBoostDown",
2811
- "AudioBassBoostToggle",
2812
- "AudioBassBoostUp",
2813
- "AudioFaderFront",
2814
- "AudioFaderRear",
2815
- "AudioSurroundModeNext",
2816
- "AudioTrebleDown",
2817
- "AudioTrebleUp",
2818
- "AudioVolumeDown",
2819
- "AudioVolumeMute",
2820
- "AudioVolumeUp",
2821
- "MicrophoneToggle",
2822
- "MicrophoneVolumeDown",
2823
- "MicrophoneVolumeMute",
2824
- "MicrophoneVolumeUp",
2825
- "TV",
2826
- "TV3DMode",
2827
- "TVAntennaCable",
2828
- "TVAudioDescription",
2829
- ];
2830
- function unEscapeString(str) {
2831
- const placeholder = "__NEWLINE__";
2832
- str = str.replace(new RegExp(placeholder, "g"), "\n");
2833
- return str;
2834
- }
2835
4594
  export { StableBrowser };
2836
4595
  //# sourceMappingURL=stable_browser.js.map