automation_model 1.0.492-dev → 1.0.492

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 +292 -17
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.d.ts +6 -3
  10. package/lib/browser_manager.js +202 -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 -2
  51. package/lib/network.js +439 -68
  52. package/lib/network.js.map +1 -1
  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 -823
  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 +747 -35
  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,59 +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
- import { registerNetworkEvents } from "./network.js";
21
- 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 = {
22
33
  CLICK: "click_element",
34
+ WAIT_ELEMENT: "wait_element",
23
35
  NAVIGATE: "navigate",
36
+ GO_BACK: "go_back",
37
+ GO_FORWARD: "go_forward",
24
38
  FILL: "fill_element",
25
- EXECUTE: "execute_page_method",
26
- OPEN: "open_environment",
39
+ EXECUTE: "execute_page_method", //
40
+ OPEN: "open_environment", //
27
41
  COMPLETE: "step_complete",
28
42
  ASK: "information_needed",
29
- GET_PAGE_STATUS: "get_page_status",
30
- CLICK_ROW_ACTION: "click_row_action",
43
+ GET_PAGE_STATUS: "get_page_status", ///
44
+ CLICK_ROW_ACTION: "click_row_action", //
31
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",
32
48
  ANALYZE_TABLE: "analyze_table",
33
- SELECT: "select_combobox",
49
+ SELECT: "select_combobox", //
50
+ VERIFY_PROPERTY: "verify_element_property",
34
51
  VERIFY_PAGE_PATH: "verify_page_path",
52
+ VERIFY_PAGE_TITLE: "verify_page_title",
35
53
  TYPE_PRESS: "type_press",
36
54
  PRESS: "press_key",
37
55
  HOVER: "hover_element",
38
56
  CHECK: "check_element",
39
57
  UNCHECK: "uncheck_element",
40
58
  EXTRACT: "extract_attribute",
59
+ EXTRACT_PROPERTY: "extract_property",
41
60
  CLOSE_PAGE: "close_page",
61
+ TABLE_OPERATION: "table_operation",
42
62
  SET_DATE_TIME: "set_date_time",
43
63
  SET_VIEWPORT: "set_viewport",
44
64
  VERIFY_VISUAL: "verify_visual",
45
65
  LOAD_DATA: "load_data",
46
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",
47
78
  };
48
79
  export const apps = {};
80
+ const formatElementName = (elementName) => {
81
+ return elementName ? JSON.stringify(elementName) : "element";
82
+ };
49
83
  class StableBrowser {
50
84
  browser;
51
85
  page;
52
86
  logger;
53
87
  context;
54
88
  world;
89
+ fastMode;
90
+ stepTags;
55
91
  project_path = null;
56
92
  webLogFile = null;
57
93
  networkLogger = null;
58
94
  configuration = null;
59
95
  appName = "main";
60
- 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 = []) {
61
103
  this.browser = browser;
62
104
  this.page = page;
63
105
  this.logger = logger;
64
106
  this.context = context;
65
107
  this.world = world;
108
+ this.fastMode = fastMode;
109
+ this.stepTags = stepTags;
66
110
  if (!this.logger) {
67
111
  this.logger = console;
68
112
  }
@@ -91,22 +135,54 @@ class StableBrowser {
91
135
  context.pages = [this.page];
92
136
  const logFolder = path.join(this.project_path, "logs", "web");
93
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
+ }
94
151
  this.registerEventListeners(this.context);
95
152
  registerNetworkEvents(this.world, this, this.context, this.page);
153
+ registerDownloadEvent(this.page, this.world, this.context);
96
154
  }
97
155
  registerEventListeners(context) {
98
156
  this.registerConsoleLogListener(this.page, context);
99
- this.registerRequestListener(this.page, context, this.webLogFile);
157
+ // this.registerRequestListener(this.page, context, this.webLogFile);
100
158
  if (!context.pageLoading) {
101
159
  context.pageLoading = { status: false };
102
160
  }
161
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
162
+ this.page.on("dialog", (dialog) => dialog.accept());
163
+ }
103
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
+ }
104
170
  context.pageLoading.status = true;
105
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
+ }
106
180
  context.page = page;
107
181
  context.pages.push(page);
108
182
  registerNetworkEvents(this.world, this, context, this.page);
183
+ registerDownloadEvent(this.page, this.world, context);
109
184
  page.on("close", async () => {
185
+ // return if browser context is already closed
110
186
  if (this.context && this.context.pages && this.context.pages.length > 1) {
111
187
  this.context.pages.pop();
112
188
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -116,7 +192,12 @@ class StableBrowser {
116
192
  console.log("Switched to page " + title);
117
193
  }
118
194
  catch (error) {
119
- 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
+ }
120
201
  }
121
202
  }
122
203
  });
@@ -125,7 +206,12 @@ class StableBrowser {
125
206
  console.log("Switch page: " + (await page.title()));
126
207
  }
127
208
  catch (e) {
128
- 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
+ }
129
215
  }
130
216
  context.pageLoading.status = false;
131
217
  }.bind(this));
@@ -137,7 +223,7 @@ class StableBrowser {
137
223
  }
138
224
  let newContextCreated = false;
139
225
  if (!apps[appName]) {
140
- 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);
141
227
  newContextCreated = true;
142
228
  apps[appName] = {
143
229
  context: newContext,
@@ -146,31 +232,41 @@ class StableBrowser {
146
232
  };
147
233
  }
148
234
  const tempContext = {};
149
- this._copyContext(this, tempContext);
150
- this._copyContext(apps[appName], this);
235
+ _copyContext(this, tempContext);
236
+ _copyContext(apps[appName], this);
151
237
  apps[this.appName] = tempContext;
152
238
  this.appName = appName;
153
239
  if (newContextCreated) {
154
240
  this.registerEventListeners(this.context);
155
241
  await this.goto(this.context.environment.baseUrl);
156
- await this.waitForPageLoad();
242
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
243
+ await this.waitForPageLoad();
244
+ }
157
245
  }
158
246
  }
159
- _copyContext(from, to) {
160
- to.browser = from.browser;
161
- to.page = from.page;
162
- to.context = from.context;
163
- }
164
- getWebLogFile(logFolder) {
165
- if (!fs.existsSync(logFolder)) {
166
- 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
+ }
167
257
  }
168
- let nextIndex = 1;
169
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
170
- 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
+ }
171
268
  }
172
- const fileName = nextIndex + ".json";
173
- return path.join(logFolder, fileName);
269
+ throw new Error("Tab not found: " + tabTitleOrIndex);
174
270
  }
175
271
  registerConsoleLogListener(page, context) {
176
272
  if (!this.context.webLogger) {
@@ -221,7 +317,7 @@ class StableBrowser {
221
317
  this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
222
318
  }
223
319
  catch (error) {
224
- console.error("Error in request listener", error);
320
+ // console.error("Error in request listener", error);
225
321
  context.networkLogger.push({
226
322
  error: "not able to listen",
227
323
  message: error.message,
@@ -235,55 +331,114 @@ class StableBrowser {
235
331
  // async closeUnexpectedPopups() {
236
332
  // await closeUnexpectedPopups(this.page);
237
333
  // }
238
- 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);
239
339
  if (!url.startsWith("http")) {
240
340
  url = "https://" + url;
241
341
  }
242
- await this.page.goto(url, {
243
- timeout: 60000,
244
- });
245
- }
246
- _fixUsingParams(text, _params) {
247
- if (!_params || typeof text !== "string") {
248
- 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"];
249
358
  }
250
- for (let key in _params) {
251
- let regValue = key;
252
- if (key.startsWith("_")) {
253
- // remove the _ prefix
254
- regValue = key.substring(1);
255
- }
256
- 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);
257
372
  }
258
- return text;
259
373
  }
260
- _fixLocatorUsingParams(locator, _params) {
261
- // check if not null
262
- if (!locator) {
263
- 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);
264
401
  }
265
- // clone the locator
266
- locator = JSON.parse(JSON.stringify(locator));
267
- this.scanAndManipulate(locator, _params);
268
- return locator;
269
402
  }
270
- _isObject(value) {
271
- 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
+ }
272
431
  }
273
- scanAndManipulate(currentObj, _params) {
274
- for (const key in currentObj) {
275
- if (typeof currentObj[key] === "string") {
276
- // Perform string manipulation
277
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
278
- }
279
- else if (this._isObject(currentObj[key])) {
280
- // Recursively scan nested objects
281
- 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);
282
440
  }
283
441
  }
284
- }
285
- _getLocator(locator, scope, _params) {
286
- locator = this._fixLocatorUsingParams(locator, _params);
287
442
  let locatorReturn;
288
443
  if (locator.role) {
289
444
  if (locator.role[1].nameReg) {
@@ -291,7 +446,7 @@ class StableBrowser {
291
446
  delete locator.role[1].nameReg;
292
447
  }
293
448
  // if (locator.role[1].name) {
294
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
449
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
295
450
  // }
296
451
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
297
452
  }
@@ -334,213 +489,171 @@ class StableBrowser {
334
489
  if (css && css.locator) {
335
490
  css = css.locator;
336
491
  }
337
- 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);
338
493
  if (result.elementCount === 0) {
339
494
  return;
340
495
  }
341
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
496
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
342
497
  // css climb to parent element
343
498
  const climbArray = [];
344
499
  for (let i = 0; i < climb; i++) {
345
500
  climbArray.push("..");
346
501
  }
347
502
  let climbXpath = "xpath=" + climbArray.join("/");
348
- return textElementCss + " >> " + climbXpath + " >> " + css;
349
- }
350
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
351
- //const stringifyText = JSON.stringify(text);
352
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
353
- function isParent(parent, child) {
354
- let currentNode = child.parentNode;
355
- while (currentNode !== null) {
356
- if (currentNode === parent) {
357
- return true;
358
- }
359
- currentNode = currentNode.parentNode;
360
- }
361
- return false;
362
- }
363
- document.isParent = isParent;
364
- function getRegex(str) {
365
- const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
366
- if (!match) {
367
- return null;
368
- }
369
- let [_, pattern, flags] = match;
370
- return new RegExp(pattern, flags);
371
- }
372
- document.getRegex = getRegex;
373
- function collectAllShadowDomElements(element, result = []) {
374
- // Check and add the element if it has a shadow root
375
- if (element.shadowRoot) {
376
- result.push(element);
377
- // Also search within the shadow root
378
- document.collectAllShadowDomElements(element.shadowRoot, result);
379
- }
380
- // Iterate over child nodes
381
- element.childNodes.forEach((child) => {
382
- // Recursively call the function for each child node
383
- document.collectAllShadowDomElements(child, result);
384
- });
385
- return result;
386
- }
387
- document.collectAllShadowDomElements = collectAllShadowDomElements;
388
- if (!tag) {
389
- tag = "*";
390
- }
391
- let regexpSearch = document.getRegex(text);
392
- if (regexpSearch) {
393
- regex = true;
394
- }
395
- let elements = Array.from(document.querySelectorAll(tag));
396
- let shadowHosts = [];
397
- document.collectAllShadowDomElements(document, shadowHosts);
398
- for (let i = 0; i < shadowHosts.length; i++) {
399
- let shadowElement = shadowHosts[i].shadowRoot;
400
- if (!shadowElement) {
401
- console.log("shadowElement is null, for host " + shadowHosts[i]);
402
- continue;
403
- }
404
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
405
- elements = elements.concat(shadowElements);
406
- }
407
- let randomToken = null;
408
- const foundElements = [];
409
- if (regex) {
410
- if (!regexpSearch) {
411
- regexpSearch = new RegExp(text, "im");
412
- }
413
- for (let i = 0; i < elements.length; i++) {
414
- const element = elements[i];
415
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
416
- (element.value && regexpSearch.test(element.value))) {
417
- foundElements.push(element);
418
- }
419
- }
420
- }
421
- else {
422
- text = text.trim();
423
- for (let i = 0; i < elements.length; i++) {
424
- const element = elements[i];
425
- if (partial) {
426
- if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
427
- (element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
428
- foundElements.push(element);
429
- }
430
- }
431
- else {
432
- if ((element.innerText && element.innerText.trim() === text) ||
433
- (element.value && element.value === text)) {
434
- foundElements.push(element);
435
- }
436
- }
437
- }
438
- }
439
- let noChildElements = [];
440
- for (let i = 0; i < foundElements.length; i++) {
441
- let element = foundElements[i];
442
- let hasChild = false;
443
- for (let j = 0; j < foundElements.length; j++) {
444
- if (i === j) {
445
- continue;
446
- }
447
- if (isParent(element, foundElements[j])) {
448
- hasChild = true;
449
- 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;
450
525
  }
451
526
  }
452
- if (!hasChild) {
453
- noChildElements.push(element);
527
+ if (!el.setAttribute) {
528
+ el = el.parentElement;
454
529
  }
530
+ el.setAttribute("data-blinq-id-" + randomToken, "");
531
+ return true;
532
+ }, [tag1, randomToken]))) {
533
+ continue;
455
534
  }
456
- let elementCount = 0;
457
- if (noChildElements.length > 0) {
458
- for (let i = 0; i < noChildElements.length; i++) {
459
- if (randomToken === null) {
460
- randomToken = Math.random().toString(36).substring(7);
461
- }
462
- let element = noChildElements[i];
463
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
464
- elementCount++;
465
- }
466
- }
467
- return { elementCount: elementCount, randomToken: randomToken };
468
- }, [text1, tag1, regex1, partial1]);
535
+ tagCount++;
536
+ }
537
+ return { elementCount: tagCount, randomToken };
469
538
  }
470
- 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
+ }
471
550
  let locatorSearch = selectorHierarchy[index];
472
551
  try {
473
- locatorSearch = JSON.parse(this._fixUsingParams(JSON.stringify(locatorSearch), _params));
552
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
474
553
  }
475
554
  catch (e) {
476
555
  console.error(e);
477
556
  }
557
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
478
558
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
479
559
  let locator = null;
480
560
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
481
- 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);
482
563
  if (!locatorString) {
564
+ info.failCause.textNotFound = true;
565
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
483
566
  return;
484
567
  }
485
- locator = this._getLocator({ css: locatorString }, scope, _params);
568
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
486
569
  }
487
570
  else if (locatorSearch.text) {
488
- 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);
489
573
  if (result.elementCount === 0) {
574
+ info.failCause.textNotFound = true;
575
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
490
576
  return;
491
577
  }
492
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
578
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
493
579
  if (locatorSearch.childCss) {
494
580
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
495
581
  }
496
- locator = this._getLocator(locatorSearch, scope, _params);
582
+ locator = await this._getLocator(locatorSearch, scope, _params);
497
583
  }
498
584
  else {
499
- locator = this._getLocator(locatorSearch, scope, _params);
585
+ locator = await this._getLocator(locatorSearch, scope, _params);
500
586
  }
501
587
  // let cssHref = false;
502
588
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
503
589
  // cssHref = true;
504
590
  // }
505
591
  let count = await locator.count();
592
+ if (count > 0 && !info.failCause.count) {
593
+ info.failCause.count = count;
594
+ }
506
595
  //info.log += "total elements found " + count + "\n";
507
596
  //let visibleCount = 0;
508
597
  let visibleLocator = null;
509
- if (locatorSearch.index && locatorSearch.index < count) {
598
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
510
599
  foundLocators.push(locator.nth(locatorSearch.index));
600
+ if (info.locatorLog) {
601
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
602
+ }
511
603
  return;
512
604
  }
605
+ if (info.locatorLog && count === 0 && logErrors) {
606
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
607
+ }
513
608
  for (let j = 0; j < count; j++) {
514
609
  let visible = await locator.nth(j).isVisible();
515
610
  const enabled = await locator.nth(j).isEnabled();
516
611
  if (!visibleOnly) {
517
612
  visible = true;
518
613
  }
519
- if (visible && enabled) {
614
+ if (visible && (allowDisabled || enabled)) {
520
615
  foundLocators.push(locator.nth(j));
616
+ if (info.locatorLog) {
617
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
618
+ }
521
619
  }
522
- else {
620
+ else if (logErrors) {
621
+ info.failCause.visible = visible;
622
+ info.failCause.enabled = enabled;
523
623
  if (!info.printMessages) {
524
624
  info.printMessages = {};
525
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
+ }
526
634
  if (!info.printMessages[j.toString()]) {
527
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
635
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
528
636
  info.printMessages[j.toString()] = true;
529
637
  }
530
638
  }
531
639
  }
532
640
  }
533
641
  async closeUnexpectedPopups(info, _params) {
642
+ if (!info) {
643
+ info = {};
644
+ info.failCause = {};
645
+ info.log = "";
646
+ }
534
647
  if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
535
648
  if (!info) {
536
649
  info = {};
537
650
  }
538
- info.log += "scan for popup handlers" + "\n";
651
+ //info.log += "scan for popup handlers" + "\n";
539
652
  const handlerGroup = [];
540
653
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
541
654
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
542
655
  }
543
- const scopes = [this.page, ...this.page.frames()];
656
+ const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
544
657
  let result = null;
545
658
  let scope = null;
546
659
  for (let i = 0; i < scopes.length; i++) {
@@ -562,33 +675,218 @@ class StableBrowser {
562
675
  }
563
676
  if (result.foundElements.length > 0) {
564
677
  let dialogCloseLocator = result.foundElements[0].locator;
565
- await dialogCloseLocator.click();
566
- // wait for the dialog to close
567
- 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
+ }
568
693
  return { rerun: true };
569
694
  }
570
695
  }
571
696
  }
572
697
  return { rerun: false };
573
698
  }
574
- async _locate(selectors, info, _params, timeout) {
699
+ getFilePath() {
700
+ const stackFrames = errorStackParser.parse(new Error());
701
+ const mjsFrames = stackFrames.filter((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
702
+ const stackFrame = mjsFrames[mjsFrames.length - 2];
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) {
575
764
  if (!timeout) {
576
765
  timeout = 30000;
577
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
+ }
578
774
  for (let i = 0; i < 3; i++) {
579
775
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
580
776
  for (let j = 0; j < selectors.locators.length; j++) {
581
777
  let selector = selectors.locators[j];
582
778
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
583
779
  }
584
- 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
+ }
585
823
  if (!element.rerun) {
586
- 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();
587
879
  }
588
880
  }
589
881
  throw new Error("unable to locate element " + JSON.stringify(selectors));
590
882
  }
591
- 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();
592
890
  let scope = this.page;
593
891
  if (selectors.frame) {
594
892
  return selectors.frame;
@@ -598,7 +896,7 @@ class StableBrowser {
598
896
  for (let i = 0; i < frame.selectors.length; i++) {
599
897
  let frameLocator = frame.selectors[i];
600
898
  if (frameLocator.css) {
601
- let testframescope = framescope.frameLocator(frameLocator.css);
899
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
602
900
  if (frameLocator.index) {
603
901
  testframescope = framescope.nth(frameLocator.index);
604
902
  }
@@ -610,7 +908,7 @@ class StableBrowser {
610
908
  break;
611
909
  }
612
910
  catch (error) {
613
- console.error("frame not found " + frameLocator.css);
911
+ // console.error("frame not found " + frameLocator.css);
614
912
  }
615
913
  }
616
914
  }
@@ -619,9 +917,11 @@ class StableBrowser {
619
917
  }
620
918
  return framescope;
621
919
  };
920
+ let fLocator = null;
622
921
  while (true) {
623
922
  let frameFound = false;
624
923
  if (selectors.nestFrmLoc) {
924
+ fLocator = selectors.nestFrmLoc;
625
925
  scope = await findFrame(selectors.nestFrmLoc, scope);
626
926
  frameFound = true;
627
927
  break;
@@ -630,6 +930,7 @@ class StableBrowser {
630
930
  for (let i = 0; i < selectors.frameLocators.length; i++) {
631
931
  let frameLocator = selectors.frameLocators[i];
632
932
  if (frameLocator.css) {
933
+ fLocator = frameLocator.css;
633
934
  scope = scope.frameLocator(frameLocator.css);
634
935
  frameFound = true;
635
936
  break;
@@ -637,16 +938,25 @@ class StableBrowser {
637
938
  }
638
939
  }
639
940
  if (!frameFound && selectors.iframe_src) {
941
+ fLocator = selectors.iframe_src;
640
942
  scope = this.page.frame({ url: selectors.iframe_src });
641
943
  }
642
944
  if (!scope) {
643
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
644
- 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}"`;
645
952
  throw new Error("unable to locate iframe " + selectors.iframe_src);
646
953
  }
647
954
  await new Promise((resolve) => setTimeout(resolve, 1000));
648
955
  }
649
956
  else {
957
+ if (info && info.locatorLog) {
958
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
959
+ }
650
960
  break;
651
961
  }
652
962
  }
@@ -656,20 +966,35 @@ class StableBrowser {
656
966
  }
657
967
  return scope;
658
968
  }
659
- async _getDocumentBody(selectors, timeout = 30000) {
660
- let scope = await this._findFrameScope(selectors, timeout);
969
+ async _getDocumentBody(selectors, timeout = 30000, info) {
970
+ let scope = await this._findFrameScope(selectors, timeout, info);
661
971
  return scope.evaluate(() => {
662
972
  var bodyContent = document.body.innerHTML;
663
973
  return bodyContent;
664
974
  });
665
975
  }
666
- 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
+ }
667
992
  let highPriorityTimeout = 5000;
668
993
  let visibleOnlyTimeout = 6000;
669
- let startTime = performance.now();
994
+ let startTime = Date.now();
670
995
  let locatorsCount = 0;
996
+ let lazy_scroll = false;
671
997
  //let arrayMode = Array.isArray(selectors);
672
- let scope = await this._findFrameScope(selectors, timeout);
673
998
  let selectorsLocators = null;
674
999
  selectorsLocators = selectors.locators;
675
1000
  // group selectors by priority
@@ -697,6 +1022,7 @@ class StableBrowser {
697
1022
  let highPriorityOnly = true;
698
1023
  let visibleOnly = true;
699
1024
  while (true) {
1025
+ let scope = await this._findFrameScope(selectors, timeout, info);
700
1026
  locatorsCount = 0;
701
1027
  let result = [];
702
1028
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -705,18 +1031,13 @@ class StableBrowser {
705
1031
  }
706
1032
  // info.log += "scanning locators in priority 1" + "\n";
707
1033
  let onlyPriority3 = selectorsLocators[0].priority === 3;
708
- 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);
709
1035
  if (result.foundElements.length === 0) {
710
1036
  // info.log += "scanning locators in priority 2" + "\n";
711
- 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);
712
1038
  }
713
- if (result.foundElements.length === 0 && onlyPriority3) {
714
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
715
- }
716
- else {
717
- if (result.foundElements.length === 0 && !highPriorityOnly) {
718
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
719
- }
1039
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
1040
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
720
1041
  }
721
1042
  let foundElements = result.foundElements;
722
1043
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -756,24 +1077,43 @@ class StableBrowser {
756
1077
  return maxCountElement.locator;
757
1078
  }
758
1079
  }
759
- if (performance.now() - startTime > timeout) {
1080
+ if (Date.now() - startTime > timeout) {
760
1081
  break;
761
1082
  }
762
- if (performance.now() - startTime > highPriorityTimeout) {
763
- 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";
764
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
+ }
765
1090
  }
766
- if (performance.now() - startTime > visibleOnlyTimeout) {
767
- 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";
768
1093
  visibleOnly = false;
769
1094
  }
770
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
+ }
771
1101
  }
772
1102
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
773
- 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
+ }
774
1114
  throw new Error("failed to locate first element no elements found, " + info.log);
775
1115
  }
776
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
1116
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
777
1117
  let foundElements = [];
778
1118
  const result = {
779
1119
  foundElements: foundElements,
@@ -781,31 +1121,88 @@ class StableBrowser {
781
1121
  for (let i = 0; i < locatorsGroup.length; i++) {
782
1122
  let foundLocators = [];
783
1123
  try {
784
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
1124
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
785
1125
  }
786
1126
  catch (e) {
787
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
788
- 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);
789
1130
  foundLocators = [];
790
1131
  try {
791
- 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);
792
1133
  }
793
1134
  catch (e) {
794
- 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
+ }
795
1138
  }
796
1139
  }
797
1140
  if (foundLocators.length === 1) {
1141
+ let box = null;
1142
+ if (!this.onlyFailuresScreenshot) {
1143
+ box = await foundLocators[0].boundingBox();
1144
+ }
798
1145
  result.foundElements.push({
799
1146
  locator: foundLocators[0],
800
- box: await foundLocators[0].boundingBox(),
1147
+ box: box,
801
1148
  unique: true,
802
1149
  });
803
1150
  result.locatorIndex = i;
804
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
+ }
805
1189
  }
806
1190
  return result;
807
1191
  }
808
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);
809
1206
  const startTime = Date.now();
810
1207
  let timeout = 30000;
811
1208
  if (options && options.timeout) {
@@ -829,13 +1226,32 @@ class StableBrowser {
829
1226
  }
830
1227
  catch (e) {
831
1228
  if (performance.now() - startTime > timeout) {
832
- 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
+ }
833
1236
  }
834
1237
  }
835
1238
  await new Promise((resolve) => setTimeout(resolve, 3000));
836
1239
  }
837
1240
  }
838
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);
839
1255
  const startTime = Date.now();
840
1256
  let timeout = 30000;
841
1257
  if (options && options.timeout) {
@@ -859,7 +1275,13 @@ class StableBrowser {
859
1275
  }
860
1276
  catch (e) {
861
1277
  if (performance.now() - startTime > timeout) {
862
- 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
+ }
863
1285
  }
864
1286
  }
865
1287
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -872,34 +1294,74 @@ class StableBrowser {
872
1294
  options,
873
1295
  world,
874
1296
  text: "Click element",
1297
+ _text: "Click on " + selectors.element_name,
875
1298
  type: Types.CLICK,
876
1299
  operation: "click",
877
1300
  log: "***** click on " + selectors.element_name + " *****\n",
878
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
+ }
879
1309
  try {
1310
+ check_performance("click_preCommand", this.context, true);
880
1311
  await _preCommand(state, this);
881
- if (state.options && state.options.context) {
882
- state.selectors.locators[0].text = state.options.context;
883
- }
884
- try {
885
- await state.element.click();
886
- await new Promise((resolve) => setTimeout(resolve, 1000));
887
- }
888
- catch (e) {
889
- // await this.closeUnexpectedPopups();
890
- state.element = await this._locate(selectors, state.info, _params);
891
- await state.element.dispatchEvent("click");
892
- 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);
893
1318
  }
894
- await this.waitForPageLoad();
895
1319
  return state.info;
896
1320
  }
897
1321
  catch (e) {
898
1322
  await _commandError(state, e, this);
899
1323
  }
900
1324
  finally {
901
- _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);
902
1363
  }
1364
+ return found;
903
1365
  }
904
1366
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
905
1367
  const state = {
@@ -909,6 +1371,7 @@ class StableBrowser {
909
1371
  world,
910
1372
  type: checked ? Types.CHECK : Types.UNCHECK,
911
1373
  text: checked ? `Check element` : `Uncheck element`,
1374
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
912
1375
  operation: "setCheck",
913
1376
  log: "***** check " + selectors.element_name + " *****\n",
914
1377
  };
@@ -918,30 +1381,53 @@ class StableBrowser {
918
1381
  // let element = await this._locate(selectors, info, _params);
919
1382
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
920
1383
  try {
921
- // await this._highlightElements(element);
922
- 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 });
923
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);
924
1393
  }
925
1394
  catch (e) {
926
1395
  if (e.message && e.message.includes("did not change its state")) {
927
1396
  this.logger.info("element did not change its state, ignoring...");
928
1397
  }
929
1398
  else {
1399
+ await new Promise((resolve) => setTimeout(resolve, 1000));
930
1400
  //await this.closeUnexpectedPopups();
931
1401
  state.info.log += "setCheck failed, will try again" + "\n";
932
- state.element = await this._locate(selectors, state.info, _params);
933
- await state.element.setChecked(checked, { timeout: 5000, force: true });
934
- 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
+ }
935
1421
  }
936
1422
  }
937
- await this.waitForPageLoad();
1423
+ //await this.waitForPageLoad();
938
1424
  return state.info;
939
1425
  }
940
1426
  catch (e) {
941
1427
  await _commandError(state, e, this);
942
1428
  }
943
1429
  finally {
944
- _commandFinally(state, this);
1430
+ await _commandFinally(state, this);
945
1431
  }
946
1432
  }
947
1433
  async hover(selectors, _params, options = {}, world = null) {
@@ -952,31 +1438,22 @@ class StableBrowser {
952
1438
  world,
953
1439
  type: Types.HOVER,
954
1440
  text: `Hover element`,
1441
+ _text: `Hover on ${selectors.element_name}`,
955
1442
  operation: "hover",
956
1443
  log: "***** hover " + selectors.element_name + " *****\n",
957
1444
  };
958
1445
  try {
959
1446
  await _preCommand(state, this);
960
- try {
961
- await state.element.hover();
962
- await new Promise((resolve) => setTimeout(resolve, 1000));
963
- }
964
- catch (e) {
965
- //await this.closeUnexpectedPopups();
966
- state.info.log += "hover failed, will try again" + "\n";
967
- state.element = await this._locate(selectors, state.info, _params);
968
- await state.element.hover({ timeout: 10000 });
969
- await new Promise((resolve) => setTimeout(resolve, 1000));
970
- }
1447
+ await performAction("hover", state.element, options, this, state, _params);
971
1448
  await _screenshot(state, this);
972
- await this.waitForPageLoad();
1449
+ //await this.waitForPageLoad();
973
1450
  return state.info;
974
1451
  }
975
1452
  catch (e) {
976
1453
  await _commandError(state, e, this);
977
1454
  }
978
1455
  finally {
979
- _commandFinally(state, this);
1456
+ await _commandFinally(state, this);
980
1457
  }
981
1458
  }
982
1459
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -991,6 +1468,7 @@ class StableBrowser {
991
1468
  value: values.toString(),
992
1469
  type: Types.SELECT,
993
1470
  text: `Select option: ${values}`,
1471
+ _text: `Select option: ${values} on ${selectors.element_name}`,
994
1472
  operation: "selectOption",
995
1473
  log: "***** select option " + selectors.element_name + " *****\n",
996
1474
  };
@@ -1004,14 +1482,14 @@ class StableBrowser {
1004
1482
  state.info.log += "selectOption failed, will try force" + "\n";
1005
1483
  await state.element.selectOption(values, { timeout: 10000, force: true });
1006
1484
  }
1007
- await this.waitForPageLoad();
1485
+ //await this.waitForPageLoad();
1008
1486
  return state.info;
1009
1487
  }
1010
1488
  catch (e) {
1011
1489
  await _commandError(state, e, this);
1012
1490
  }
1013
1491
  finally {
1014
- _commandFinally(state, this);
1492
+ await _commandFinally(state, this);
1015
1493
  }
1016
1494
  }
1017
1495
  async type(_value, _params = null, options = {}, world = null) {
@@ -1025,6 +1503,7 @@ class StableBrowser {
1025
1503
  highlight: false,
1026
1504
  type: Types.TYPE_PRESS,
1027
1505
  text: `Type value: ${_value}`,
1506
+ _text: `Type value: ${_value}`,
1028
1507
  operation: "type",
1029
1508
  log: "",
1030
1509
  };
@@ -1056,7 +1535,7 @@ class StableBrowser {
1056
1535
  await _commandError(state, e, this);
1057
1536
  }
1058
1537
  finally {
1059
- _commandFinally(state, this);
1538
+ await _commandFinally(state, this);
1060
1539
  }
1061
1540
  }
1062
1541
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1092,37 +1571,35 @@ class StableBrowser {
1092
1571
  await _commandError(state, e, this);
1093
1572
  }
1094
1573
  finally {
1095
- _commandFinally(state, this);
1574
+ await _commandFinally(state, this);
1096
1575
  }
1097
1576
  }
1098
1577
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1099
- _validateSelectors(selectors);
1100
- const startTime = Date.now();
1101
- let error = null;
1102
- let screenshotId = null;
1103
- let screenshotPath = null;
1104
- const info = {};
1105
- info.log = "";
1106
- info.operation = Types.SET_DATE_TIME;
1107
- info.selectors = selectors;
1108
- 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
+ };
1109
1591
  try {
1110
- value = await this._replaceWithLocalData(value, this);
1111
- let element = await this._locate(selectors, info, _params);
1112
- //insert red border around the element
1113
- await this.scrollIfNeeded(element, info);
1114
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1115
- await this._highlightElements(element);
1592
+ await _preCommand(state, this);
1116
1593
  try {
1117
- await element.click();
1594
+ await performAction("click", state.element, options, this, state, _params);
1118
1595
  await new Promise((resolve) => setTimeout(resolve, 500));
1119
1596
  if (format) {
1120
- value = dayjs(value).format(format);
1121
- await element.fill(value);
1597
+ state.value = dayjs(state.value).format(format);
1598
+ await state.element.fill(state.value);
1122
1599
  }
1123
1600
  else {
1124
- const dateTimeValue = await getDateTimeValue({ value, element });
1125
- await element.evaluateHandle((el, dateTimeValue) => {
1601
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1602
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1126
1603
  el.value = ""; // clear input
1127
1604
  el.value = dateTimeValue;
1128
1605
  }, dateTimeValue);
@@ -1135,20 +1612,19 @@ class StableBrowser {
1135
1612
  }
1136
1613
  catch (err) {
1137
1614
  //await this.closeUnexpectedPopups();
1138
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1615
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1139
1616
  this.logger.info("Trying again");
1140
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1141
- info.screenshotPath = screenshotPath;
1142
- Object.assign(err, { info: info });
1617
+ await _screenshot(state, this);
1618
+ Object.assign(err, { info: state.info });
1143
1619
  await element.click();
1144
1620
  await new Promise((resolve) => setTimeout(resolve, 500));
1145
1621
  if (format) {
1146
- value = dayjs(value).format(format);
1147
- await element.fill(value);
1622
+ state.value = dayjs(state.value).format(format);
1623
+ await state.element.fill(state.value);
1148
1624
  }
1149
1625
  else {
1150
- const dateTimeValue = await getDateTimeValue({ value, element });
1151
- await element.evaluateHandle((el, dateTimeValue) => {
1626
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1627
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1152
1628
  el.value = ""; // clear input
1153
1629
  el.value = dateTimeValue;
1154
1630
  }, dateTimeValue);
@@ -1161,31 +1637,10 @@ class StableBrowser {
1161
1637
  }
1162
1638
  }
1163
1639
  catch (e) {
1164
- error = e;
1165
- throw e;
1640
+ await _commandError(state, e, this);
1166
1641
  }
1167
1642
  finally {
1168
- const endTime = Date.now();
1169
- this._reportToWorld(world, {
1170
- element_name: selectors.element_name,
1171
- type: Types.SET_DATE_TIME,
1172
- screenshotId,
1173
- value: value,
1174
- text: `setDateTime input with value: ${value}`,
1175
- result: error
1176
- ? {
1177
- status: "FAILED",
1178
- startTime,
1179
- endTime,
1180
- message: error === null || error === void 0 ? void 0 : error.message,
1181
- }
1182
- : {
1183
- status: "PASSED",
1184
- startTime,
1185
- endTime,
1186
- },
1187
- info: info,
1188
- });
1643
+ await _commandFinally(state, this);
1189
1644
  }
1190
1645
  }
1191
1646
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1200,17 +1655,29 @@ class StableBrowser {
1200
1655
  world,
1201
1656
  type: Types.FILL,
1202
1657
  text: `Click type input with value: ${_value}`,
1658
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1203
1659
  operation: "clickType",
1204
1660
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1205
1661
  };
1662
+ if (!options) {
1663
+ options = {};
1664
+ }
1206
1665
  if (newValue !== _value) {
1207
1666
  //this.logger.info(_value + "=" + newValue);
1208
1667
  _value = newValue;
1209
1668
  }
1210
1669
  try {
1211
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);
1212
1679
  state.info.value = _value;
1213
- if (options === null || options === undefined || !options.press) {
1680
+ if (!options.press) {
1214
1681
  try {
1215
1682
  let currentValue = await state.element.inputValue();
1216
1683
  if (currentValue) {
@@ -1221,13 +1688,9 @@ class StableBrowser {
1221
1688
  this.logger.info("unable to clear input value");
1222
1689
  }
1223
1690
  }
1224
- if (options === null || options === undefined || options.press) {
1225
- try {
1226
- await state.element.click({ timeout: 5000 });
1227
- }
1228
- catch (e) {
1229
- await state.element.dispatchEvent("click");
1230
- }
1691
+ if (options.press) {
1692
+ options.timeout = 5000;
1693
+ await performAction("click", state.element, options, this, state, _params);
1231
1694
  }
1232
1695
  else {
1233
1696
  try {
@@ -1238,6 +1701,25 @@ class StableBrowser {
1238
1701
  }
1239
1702
  }
1240
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
+ }
1241
1723
  const valueSegment = state.value.split("&&");
1242
1724
  for (let i = 0; i < valueSegment.length; i++) {
1243
1725
  if (i > 0) {
@@ -1258,14 +1740,21 @@ class StableBrowser {
1258
1740
  await new Promise((resolve) => setTimeout(resolve, 500));
1259
1741
  }
1260
1742
  }
1743
+ //if (!this.fastMode) {
1261
1744
  await _screenshot(state, this);
1745
+ //}
1262
1746
  if (enter === true) {
1263
1747
  await new Promise((resolve) => setTimeout(resolve, 2000));
1264
1748
  await this.page.keyboard.press("Enter");
1265
1749
  await this.waitForPageLoad();
1266
1750
  }
1267
1751
  else if (enter === false) {
1268
- await state.element.dispatchEvent("change");
1752
+ try {
1753
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1754
+ }
1755
+ catch (e) {
1756
+ // ignore
1757
+ }
1269
1758
  //await this.page.keyboard.press("Tab");
1270
1759
  }
1271
1760
  else {
@@ -1280,7 +1769,7 @@ class StableBrowser {
1280
1769
  await _commandError(state, e, this);
1281
1770
  }
1282
1771
  finally {
1283
- _commandFinally(state, this);
1772
+ await _commandFinally(state, this);
1284
1773
  }
1285
1774
  }
1286
1775
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1302,30 +1791,67 @@ class StableBrowser {
1302
1791
  if (enter) {
1303
1792
  await new Promise((resolve) => setTimeout(resolve, 2000));
1304
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;
1305
1829
  }
1306
- await this.waitForPageLoad();
1830
+ await state.element.setInputFiles(files);
1307
1831
  return state.info;
1308
1832
  }
1309
1833
  catch (e) {
1310
1834
  await _commandError(state, e, this);
1311
1835
  }
1312
1836
  finally {
1313
- _commandFinally(state, this);
1837
+ await _commandFinally(state, this);
1314
1838
  }
1315
1839
  }
1316
1840
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1317
1841
  return await this._getText(selectors, 0, _params, options, info, world);
1318
1842
  }
1319
1843
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1844
+ const timeout = this._getFindElementTimeout(options);
1320
1845
  _validateSelectors(selectors);
1321
1846
  let screenshotId = null;
1322
1847
  let screenshotPath = null;
1323
1848
  if (!info.log) {
1324
1849
  info.log = "";
1850
+ info.locatorLog = new LocatorLog(selectors);
1325
1851
  }
1326
1852
  info.operation = "getText";
1327
1853
  info.selectors = selectors;
1328
- let element = await this._locate(selectors, info, _params);
1854
+ let element = await this._locate(selectors, info, _params, timeout);
1329
1855
  if (climb > 0) {
1330
1856
  const climbArray = [];
1331
1857
  for (let i = 0; i < climb; i++) {
@@ -1344,6 +1870,18 @@ class StableBrowser {
1344
1870
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1345
1871
  try {
1346
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
+ // }
1347
1885
  const elementText = await element.innerText();
1348
1886
  return {
1349
1887
  text: elementText,
@@ -1355,7 +1893,7 @@ class StableBrowser {
1355
1893
  }
1356
1894
  catch (e) {
1357
1895
  //await this.closeUnexpectedPopups();
1358
- this.logger.info("no innerText will use textContent");
1896
+ this.logger.info("no innerText, will use textContent");
1359
1897
  const elementText = await element.textContent();
1360
1898
  return { text: elementText, screenshotId, screenshotPath, value: value };
1361
1899
  }
@@ -1380,6 +1918,7 @@ class StableBrowser {
1380
1918
  highlight: false,
1381
1919
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1382
1920
  text: `Verify element contains pattern: ${pattern}`,
1921
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1383
1922
  operation: "containsPattern",
1384
1923
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1385
1924
  };
@@ -1411,10 +1950,12 @@ class StableBrowser {
1411
1950
  await _commandError(state, e, this);
1412
1951
  }
1413
1952
  finally {
1414
- _commandFinally(state, this);
1953
+ await _commandFinally(state, this);
1415
1954
  }
1416
1955
  }
1417
1956
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1957
+ const timeout = this._getFindElementTimeout(options);
1958
+ const startTime = Date.now();
1418
1959
  const state = {
1419
1960
  selectors,
1420
1961
  _params,
@@ -1441,61 +1982,137 @@ class StableBrowser {
1441
1982
  }
1442
1983
  let foundObj = null;
1443
1984
  try {
1444
- await _preCommand(state, this);
1445
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1446
- if (foundObj && foundObj.element) {
1447
- await this.scrollIfNeeded(foundObj.element, state.info);
1448
- }
1449
- await _screenshot(state, this);
1450
- const dateAlternatives = findDateAlternatives(text);
1451
- const numberAlternatives = findNumberAlternatives(text);
1452
- if (dateAlternatives.date) {
1453
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1454
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1455
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1456
- 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);
1457
1991
  }
1458
- }
1459
- throw new Error("element doesn't contain text " + text);
1460
- }
1461
- else if (numberAlternatives.number) {
1462
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1463
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1464
- 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)) {
1465
2012
  return state.info;
1466
2013
  }
1467
2014
  }
1468
- throw new Error("element doesn't contain text " + text);
1469
- }
1470
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1471
- state.info.foundText = foundObj?.text;
1472
- state.info.value = foundObj?.value;
1473
- 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
1474
2020
  }
1475
- 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);
1476
2024
  }
1477
2025
  catch (e) {
1478
2026
  await _commandError(state, e, this);
2027
+ throw e;
1479
2028
  }
1480
2029
  finally {
1481
- _commandFinally(state, this);
2030
+ await _commandFinally(state, this);
1482
2031
  }
1483
2032
  }
1484
- _getDataFile(world = null) {
1485
- let dataFile = null;
1486
- if (world && world.reportFolder) {
1487
- 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");
1488
2057
  }
1489
- else if (this.reportFolder) {
1490
- 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");
1491
2060
  }
1492
- else if (this.context && this.context.reportFolder) {
1493
- dataFile = path.join(this.context.reportFolder, "data.json");
2061
+ else if (referanceSnapshot.startsWith("yaml:")) {
2062
+ text = referanceSnapshot.substring(5);
1494
2063
  }
1495
2064
  else {
1496
- 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);
1497
2115
  }
1498
- return dataFile;
1499
2116
  }
1500
2117
  async waitForUserInput(message, world = null) {
1501
2118
  if (!message) {
@@ -1525,13 +2142,22 @@ class StableBrowser {
1525
2142
  return;
1526
2143
  }
1527
2144
  // if data file exists, load it
1528
- const dataFile = this._getDataFile(world);
2145
+ const dataFile = _getDataFile(world, this.context, this);
1529
2146
  let data = this.getTestData(world);
1530
2147
  // merge the testData with the existing data
1531
2148
  Object.assign(data, testData);
1532
2149
  // save the data to the file
1533
2150
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1534
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
+ }
1535
2161
  _getDataFilePath(fileName) {
1536
2162
  let dataFile = path.join(this.project_path, "data", fileName);
1537
2163
  if (fs.existsSync(dataFile)) {
@@ -1628,14 +2254,12 @@ class StableBrowser {
1628
2254
  }
1629
2255
  }
1630
2256
  getTestData(world = null) {
1631
- const dataFile = this._getDataFile(world);
1632
- let data = {};
1633
- if (fs.existsSync(dataFile)) {
1634
- data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1635
- }
1636
- return data;
2257
+ return _getTestData(world, this.context, this);
1637
2258
  }
1638
2259
  async _screenShot(options = {}, world = null, info = null) {
2260
+ if (!options) {
2261
+ options = {};
2262
+ }
1639
2263
  // collect url/path/title
1640
2264
  if (info) {
1641
2265
  if (!info.title) {
@@ -1660,13 +2284,11 @@ class StableBrowser {
1660
2284
  if (!fs.existsSync(world.screenshotPath)) {
1661
2285
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1662
2286
  }
1663
- let nextIndex = 1;
1664
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1665
- nextIndex++;
1666
- }
1667
- 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");
1668
2290
  try {
1669
- await this.takeScreenshot(screenshotPath);
2291
+ await this.takeScreenshot(screenshotPath, options.fullPage === true);
1670
2292
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1671
2293
  // // save the buffer to the screenshot path asynchrously
1672
2294
  // fs.writeFile(screenshotPath, buffer, (err) => {
@@ -1674,20 +2296,20 @@ class StableBrowser {
1674
2296
  // this.logger.info("unable to save screenshot " + screenshotPath);
1675
2297
  // }
1676
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
+ }
1677
2304
  }
1678
2305
  catch (e) {
1679
2306
  this.logger.info("unable to take screenshot, ignored");
1680
2307
  }
1681
- result.screenshotId = nextIndex;
1682
- result.screenshotPath = screenshotPath;
1683
- if (info && info.box) {
1684
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1685
- }
1686
2308
  }
1687
2309
  else if (options && options.screenshot) {
1688
2310
  result.screenshotPath = options.screenshotPath;
1689
2311
  try {
1690
- await this.takeScreenshot(options.screenshotPath);
2312
+ await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
1691
2313
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1692
2314
  // // save the buffer to the screenshot path asynchrously
1693
2315
  // fs.writeFile(options.screenshotPath, buffer, (err) => {
@@ -1705,7 +2327,7 @@ class StableBrowser {
1705
2327
  }
1706
2328
  return result;
1707
2329
  }
1708
- async takeScreenshot(screenshotPath) {
2330
+ async takeScreenshot(screenshotPath, fullPage = false) {
1709
2331
  const playContext = this.context.playContext;
1710
2332
  // Using CDP to capture the screenshot
1711
2333
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
@@ -1717,17 +2339,20 @@ class StableBrowser {
1717
2339
  document.documentElement.clientWidth,
1718
2340
  ])));
1719
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
+ // }
1720
2351
  if (this.context.browserName === "chromium") {
1721
2352
  const client = await playContext.newCDPSession(this.page);
1722
2353
  const { data } = await client.send("Page.captureScreenshot", {
1723
2354
  format: "png",
1724
- // clip: {
1725
- // x: 0,
1726
- // y: 0,
1727
- // width: viewportWidth,
1728
- // height: viewportHeight,
1729
- // scale: 1,
1730
- // },
2355
+ captureBeyondViewport: fullPage,
1731
2356
  });
1732
2357
  await client.detach();
1733
2358
  if (!screenshotPath) {
@@ -1736,8 +2361,12 @@ class StableBrowser {
1736
2361
  screenshotBuffer = Buffer.from(data, "base64");
1737
2362
  }
1738
2363
  else {
1739
- screenshotBuffer = await this.page.screenshot();
2364
+ screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
1740
2365
  }
2366
+ // if (focusedElement) {
2367
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2368
+ // await this._unhighlightElements(focusedElement);
2369
+ // }
1741
2370
  let image = await Jimp.read(screenshotBuffer);
1742
2371
  // Get the image dimensions
1743
2372
  const { width, height } = image.bitmap;
@@ -1750,6 +2379,7 @@ class StableBrowser {
1750
2379
  else {
1751
2380
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1752
2381
  }
2382
+ return screenshotBuffer;
1753
2383
  }
1754
2384
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1755
2385
  const state = {
@@ -1772,116 +2402,535 @@ class StableBrowser {
1772
2402
  await _commandError(state, e, this);
1773
2403
  }
1774
2404
  finally {
1775
- _commandFinally(state, this);
2405
+ await _commandFinally(state, this);
1776
2406
  }
1777
2407
  }
1778
2408
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1779
- _validateSelectors(selectors);
1780
- const startTime = Date.now();
1781
- let error = null;
1782
- let screenshotId = null;
1783
- 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
+ };
1784
2423
  await new Promise((resolve) => setTimeout(resolve, 2000));
1785
- const info = {};
1786
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1787
- info.operation = "extract";
1788
- info.selectors = selectors;
1789
2424
  try {
1790
- const element = await this._locate(selectors, info, _params);
1791
- await this._highlightElements(element);
1792
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2425
+ await _preCommand(state, this);
1793
2426
  switch (attribute) {
1794
2427
  case "inner_text":
1795
- info.value = await element.innerText();
2428
+ state.value = await state.element.innerText();
1796
2429
  break;
1797
2430
  case "href":
1798
- info.value = await element.getAttribute("href");
2431
+ state.value = await state.element.getAttribute("href");
1799
2432
  break;
1800
2433
  case "value":
1801
- info.value = await element.inputValue();
2434
+ state.value = await state.element.inputValue();
2435
+ break;
2436
+ case "text":
2437
+ state.value = await state.element.textContent();
1802
2438
  break;
1803
2439
  default:
1804
- info.value = await element.getAttribute(attribute);
2440
+ state.value = await state.element.getAttribute(attribute);
1805
2441
  break;
1806
2442
  }
1807
- this[variable] = info.value;
1808
- if (world) {
1809
- 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
+ }
1810
2460
  }
1811
- this.setTestData({ [variable]: info.value }, world);
1812
- this.logger.info("set test data: " + variable + "=" + info.value);
1813
- 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;
1814
2466
  }
1815
2467
  catch (e) {
1816
- //await this.closeUnexpectedPopups();
1817
- this.logger.error("extract failed " + info.log);
1818
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1819
- info.screenshotPath = screenshotPath;
1820
- Object.assign(e, { info: info });
1821
- error = e;
1822
- throw e;
2468
+ await _commandError(state, e, this);
1823
2469
  }
1824
2470
  finally {
1825
- const endTime = Date.now();
1826
- this._reportToWorld(world, {
1827
- element_name: selectors.element_name,
1828
- type: Types.EXTRACT_ATTRIBUTE,
1829
- variable: variable,
1830
- value: info.value,
1831
- text: "Extract attribute from element",
1832
- screenshotId,
1833
- result: error
1834
- ? {
1835
- status: "FAILED",
1836
- startTime,
1837
- endTime,
1838
- message: error?.message,
1839
- }
1840
- : {
1841
- status: "PASSED",
1842
- startTime,
1843
- endTime,
1844
- },
1845
- info: info,
1846
- });
2471
+ await _commandFinally(state, this);
1847
2472
  }
1848
2473
  }
1849
- async extractEmailData(emailAddress, options, world) {
1850
- if (!emailAddress) {
1851
- throw new Error("email address is null");
1852
- }
1853
- // check if address contain @
1854
- if (emailAddress.indexOf("@") === -1) {
1855
- emailAddress = emailAddress + "@blinq-mail.io";
1856
- }
1857
- else {
1858
- if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1859
- 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
+ }
1860
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;
1861
2537
  }
1862
- const startTime = Date.now();
1863
- let timeout = 60000;
1864
- if (options && options.timeout) {
1865
- timeout = options.timeout;
2538
+ catch (e) {
2539
+ await _commandError(state, e, this);
1866
2540
  }
1867
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1868
- const request = {
1869
- method: "POST",
1870
- url: serviceUrl,
1871
- headers: {
1872
- "Content-Type": "application/json",
1873
- Authorization: `Bearer ${process.env.TOKEN}`,
1874
- },
1875
- data: JSON.stringify({
1876
- email: emailAddress,
1877
- }),
1878
- };
1879
- let errorCount = 0;
1880
- while (true) {
1881
- try {
1882
- let result = await this.context.api.request(request);
1883
- // the response body expected to be the following:
1884
- // {
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
+ // {
1885
2934
  // "status": true,
1886
2935
  // "content": {
1887
2936
  // "url": "",
@@ -1920,7 +2969,8 @@ class StableBrowser {
1920
2969
  catch (e) {
1921
2970
  errorCount++;
1922
2971
  if (errorCount > 3) {
1923
- throw e;
2972
+ // throw e;
2973
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
1924
2974
  }
1925
2975
  // ignore
1926
2976
  }
@@ -1934,27 +2984,32 @@ class StableBrowser {
1934
2984
  async _highlightElements(scope, css) {
1935
2985
  try {
1936
2986
  if (!scope) {
2987
+ // console.log(`Scope is not defined`);
1937
2988
  return;
1938
2989
  }
1939
2990
  if (!css) {
1940
2991
  scope
1941
2992
  .evaluate((node) => {
1942
2993
  if (node && node.style) {
1943
- let originalBorder = node.style.border;
1944
- 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}`);
1945
2999
  if (window) {
1946
3000
  window.addEventListener("beforeunload", function (e) {
1947
- node.style.border = originalBorder;
3001
+ node.style.outline = originalOutline;
1948
3002
  });
1949
3003
  }
1950
3004
  setTimeout(function () {
1951
- node.style.border = originalBorder;
3005
+ node.style.outline = originalOutline;
1952
3006
  }, 2000);
1953
3007
  }
1954
3008
  })
1955
3009
  .then(() => { })
1956
3010
  .catch((e) => {
1957
3011
  // ignore
3012
+ // console.error(`Could not highlight node : ${e}`);
1958
3013
  });
1959
3014
  }
1960
3015
  else {
@@ -1970,17 +3025,18 @@ class StableBrowser {
1970
3025
  if (!element.style) {
1971
3026
  return;
1972
3027
  }
1973
- var originalBorder = element.style.border;
3028
+ let originalOutline = element.style.outline;
3029
+ element.__previousOutline = originalOutline;
1974
3030
  // Set the new border to be red and 2px solid
1975
- element.style.border = "2px solid red";
3031
+ element.style.outline = "2px solid red";
1976
3032
  if (window) {
1977
3033
  window.addEventListener("beforeunload", function (e) {
1978
- element.style.border = originalBorder;
3034
+ element.style.outline = originalOutline;
1979
3035
  });
1980
3036
  }
1981
3037
  // Set a timeout to revert to the original border after 2 seconds
1982
3038
  setTimeout(function () {
1983
- element.style.border = originalBorder;
3039
+ element.style.outline = originalOutline;
1984
3040
  }, 2000);
1985
3041
  }
1986
3042
  return;
@@ -1988,6 +3044,7 @@ class StableBrowser {
1988
3044
  .then(() => { })
1989
3045
  .catch((e) => {
1990
3046
  // ignore
3047
+ // console.error(`Could not highlight css: ${e}`);
1991
3048
  });
1992
3049
  }
1993
3050
  }
@@ -1995,8 +3052,49 @@ class StableBrowser {
1995
3052
  console.debug(error);
1996
3053
  }
1997
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
+ */
1998
3097
  async verifyPagePath(pathPart, options = {}, world = null) {
1999
- const startTime = Date.now();
2000
3098
  let error = null;
2001
3099
  let screenshotId = null;
2002
3100
  let screenshotPath = null;
@@ -2010,159 +3108,534 @@ class StableBrowser {
2010
3108
  pathPart = newValue;
2011
3109
  }
2012
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
+ };
2013
3125
  try {
3126
+ await _preCommand(state, this);
3127
+ state.info.text = queryText;
2014
3128
  for (let i = 0; i < 30; i++) {
2015
3129
  const url = await this.page.url();
2016
- if (!url.includes(pathPart)) {
2017
- if (i === 29) {
2018
- 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`);
2019
3405
  }
2020
3406
  await new Promise((resolve) => setTimeout(resolve, 1000));
2021
3407
  continue;
2022
3408
  }
2023
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2024
- 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
+ }
2025
3426
  }
2026
3427
  }
2027
3428
  catch (e) {
2028
- //await this.closeUnexpectedPopups();
2029
- this.logger.error("verify page path failed " + info.log);
2030
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2031
- info.screenshotPath = screenshotPath;
2032
- Object.assign(e, { info: info });
2033
- error = e;
2034
- throw e;
3429
+ await _commandError(state, e, this);
2035
3430
  }
2036
3431
  finally {
2037
- const endTime = Date.now();
2038
- this._reportToWorld(world, {
2039
- type: Types.VERIFY_PAGE_PATH,
2040
- text: "Verify page path",
2041
- screenshotId,
2042
- result: error
2043
- ? {
2044
- status: "FAILED",
2045
- startTime,
2046
- endTime,
2047
- message: error?.message,
2048
- }
2049
- : {
2050
- status: "PASSED",
2051
- startTime,
2052
- endTime,
2053
- },
2054
- info: info,
2055
- });
3432
+ await _commandFinally(state, this);
2056
3433
  }
2057
3434
  }
2058
- async verifyTextExistInPage(text, options = {}, world = null) {
3435
+ async waitForTextToDisappear(text, options = {}, world = null) {
2059
3436
  text = unEscapeString(text);
2060
- const startTime = Date.now();
2061
- const timeout = this._getLoadTimeout(options);
2062
- let error = null;
2063
- let screenshotId = null;
2064
- let screenshotPath = null;
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);
2065
3454
  await new Promise((resolve) => setTimeout(resolve, 2000));
2066
- const info = {};
2067
- info.log = "***** verify text " + text + " exists in page *****\n";
2068
- info.operation = "verifyTextExistInPage";
2069
3455
  const newValue = await this._replaceWithLocalData(text, world);
2070
3456
  if (newValue !== text) {
2071
3457
  this.logger.info(text + "=" + newValue);
2072
3458
  text = newValue;
2073
3459
  }
2074
- info.text = text;
2075
- let dateAlternatives = findDateAlternatives(text);
2076
- let numberAlternatives = findNumberAlternatives(text);
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);
3490
+ }
3491
+ }
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);
3511
+ await new Promise((resolve) => setTimeout(resolve, 2000));
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;
2077
3525
  try {
3526
+ await _preCommand(state, this);
3527
+ state.info.text = textToVerify;
3528
+ let resultWithElementsFound = {
3529
+ length: 0,
3530
+ };
2078
3531
  while (true) {
2079
- const frames = this.page.frames();
2080
- let results = [];
2081
- for (let i = 0; i < frames.length; i++) {
2082
- if (dateAlternatives.date) {
2083
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2084
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
2085
- result.frame = frames[i];
2086
- results.push(result);
2087
- }
2088
- }
2089
- else if (numberAlternatives.number) {
2090
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2091
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
2092
- result.frame = frames[i];
2093
- results.push(result);
2094
- }
2095
- }
2096
- else {
2097
- const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
2098
- result.frame = frames[i];
2099
- results.push(result);
2100
- }
3532
+ try {
3533
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
3534
+ }
3535
+ catch (error) {
3536
+ // ignore
2101
3537
  }
2102
- info.results = results;
2103
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2104
3538
  if (resultWithElementsFound.length === 0) {
2105
- if (Date.now() - startTime > timeout) {
2106
- 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`);
2107
3541
  }
2108
3542
  await new Promise((resolve) => setTimeout(resolve, 1000));
2109
3543
  continue;
2110
3544
  }
2111
- if (resultWithElementsFound[0].randomToken) {
2112
- const frame = resultWithElementsFound[0].frame;
2113
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2114
- await this._highlightElements(frame, dataAttribute);
2115
- const element = await frame.$(dataAttribute);
2116
- if (element) {
2117
- await this.scrollIfNeeded(element, info);
2118
- 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
+ }
2119
3601
  }
2120
3602
  }
2121
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2122
- return info;
3603
+ catch (error) {
3604
+ console.error(error);
3605
+ }
2123
3606
  }
2124
3607
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2125
3608
  }
2126
3609
  catch (e) {
2127
- //await this.closeUnexpectedPopups();
2128
- this.logger.error("verify text exist in page failed " + info.log);
2129
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2130
- info.screenshotPath = screenshotPath;
2131
- Object.assign(e, { info: info });
2132
- error = e;
2133
- throw e;
3610
+ await _commandError(state, e, this);
2134
3611
  }
2135
3612
  finally {
2136
- const endTime = Date.now();
2137
- this._reportToWorld(world, {
2138
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2139
- text: "Verify text exists in page",
2140
- screenshotId,
2141
- result: error
2142
- ? {
2143
- status: "FAILED",
2144
- startTime,
2145
- endTime,
2146
- message: error?.message,
2147
- }
2148
- : {
2149
- status: "PASSED",
2150
- startTime,
2151
- endTime,
2152
- },
2153
- info: info,
2154
- });
3613
+ await _commandFinally(state, this);
2155
3614
  }
2156
3615
  }
2157
- _getServerUrl() {
2158
- let serviceUrl = "https://api.blinq.io";
2159
- if (process.env.NODE_ENV_BLINQ === "dev") {
2160
- serviceUrl = "https://dev.api.blinq.io";
2161
- }
2162
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2163
- 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
+ }
2164
3635
  }
2165
- return serviceUrl;
3636
+ // state.info.results = results;
3637
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3638
+ return resultWithElementsFound;
2166
3639
  }
2167
3640
  async visualVerification(text, options = {}, world = null) {
2168
3641
  const startTime = Date.now();
@@ -2178,14 +3651,17 @@ class StableBrowser {
2178
3651
  throw new Error("TOKEN is not set");
2179
3652
  }
2180
3653
  try {
2181
- let serviceUrl = this._getServerUrl();
3654
+ let serviceUrl = _getServerUrl();
2182
3655
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2183
3656
  info.screenshotPath = screenshotPath;
2184
3657
  const screenshot = await this.takeScreenshot();
2185
- const request = {
2186
- method: "POST",
3658
+ let request = {
3659
+ method: "post",
3660
+ maxBodyLength: Infinity,
2187
3661
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2188
3662
  headers: {
3663
+ "x-bvt-project-id": path.basename(this.project_path),
3664
+ "x-source": "aaa",
2189
3665
  "Content-Type": "application/json",
2190
3666
  Authorization: `Bearer ${process.env.TOKEN}`,
2191
3667
  },
@@ -2194,7 +3670,7 @@ class StableBrowser {
2194
3670
  screenshot: screenshot,
2195
3671
  }),
2196
3672
  };
2197
- let result = await this.context.api.request(request);
3673
+ const result = await axios.request(request);
2198
3674
  if (result.data.status !== true) {
2199
3675
  throw new Error("Visual validation failed");
2200
3676
  }
@@ -2214,13 +3690,15 @@ class StableBrowser {
2214
3690
  info.screenshotPath = screenshotPath;
2215
3691
  Object.assign(e, { info: info });
2216
3692
  error = e;
2217
- throw e;
3693
+ // throw e;
3694
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
2218
3695
  }
2219
3696
  finally {
2220
3697
  const endTime = Date.now();
2221
- this._reportToWorld(world, {
3698
+ _reportToWorld(world, {
2222
3699
  type: Types.VERIFY_VISUAL,
2223
3700
  text: "Visual verification",
3701
+ _text: "Visual verification of " + text,
2224
3702
  screenshotId,
2225
3703
  result: error
2226
3704
  ? {
@@ -2266,6 +3744,7 @@ class StableBrowser {
2266
3744
  let screenshotPath = null;
2267
3745
  const info = {};
2268
3746
  info.log = "";
3747
+ info.locatorLog = new LocatorLog(selectors);
2269
3748
  info.operation = "getTableData";
2270
3749
  info.selectors = selectors;
2271
3750
  try {
@@ -2281,11 +3760,12 @@ class StableBrowser {
2281
3760
  info.screenshotPath = screenshotPath;
2282
3761
  Object.assign(e, { info: info });
2283
3762
  error = e;
2284
- throw e;
3763
+ // throw e;
3764
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2285
3765
  }
2286
3766
  finally {
2287
3767
  const endTime = Date.now();
2288
- this._reportToWorld(world, {
3768
+ _reportToWorld(world, {
2289
3769
  element_name: selectors.element_name,
2290
3770
  type: Types.GET_TABLE_DATA,
2291
3771
  text: "Get table data",
@@ -2340,7 +3820,7 @@ class StableBrowser {
2340
3820
  info.operation = "analyzeTable";
2341
3821
  info.selectors = selectors;
2342
3822
  info.query = query;
2343
- query = this._fixUsingParams(query, _params);
3823
+ query = _fixUsingParams(query, _params);
2344
3824
  info.query_fixed = query;
2345
3825
  info.operator = operator;
2346
3826
  info.value = value;
@@ -2446,11 +3926,12 @@ class StableBrowser {
2446
3926
  info.screenshotPath = screenshotPath;
2447
3927
  Object.assign(e, { info: info });
2448
3928
  error = e;
2449
- throw e;
3929
+ // throw e;
3930
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2450
3931
  }
2451
3932
  finally {
2452
3933
  const endTime = Date.now();
2453
- this._reportToWorld(world, {
3934
+ _reportToWorld(world, {
2454
3935
  element_name: selectors.element_name,
2455
3936
  type: Types.ANALYZE_TABLE,
2456
3937
  text: "Analyze table",
@@ -2471,8 +3952,51 @@ class StableBrowser {
2471
3952
  });
2472
3953
  }
2473
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
+ }
2474
3992
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2475
- 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
+ }
2476
4000
  }
2477
4001
  _getLoadTimeout(options) {
2478
4002
  let timeout = 15000;
@@ -2484,7 +4008,54 @@ class StableBrowser {
2484
4008
  }
2485
4009
  return timeout;
2486
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
+ }
2487
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");
2488
4059
  let timeout = this._getLoadTimeout(options);
2489
4060
  const promiseArray = [];
2490
4061
  // let waitForNetworkIdle = true;
@@ -2517,13 +4088,15 @@ class StableBrowser {
2517
4088
  else if (e.label === "domcontentloaded") {
2518
4089
  console.log("waited for the domcontent loaded timeout");
2519
4090
  }
2520
- console.log(".");
2521
4091
  }
2522
4092
  finally {
2523
- 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
+ }
2524
4097
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2525
4098
  const endTime = Date.now();
2526
- this._reportToWorld(world, {
4099
+ _reportToWorld(world, {
2527
4100
  type: Types.GET_PAGE_STATUS,
2528
4101
  text: "Wait for page load",
2529
4102
  screenshotId,
@@ -2543,40 +4116,138 @@ class StableBrowser {
2543
4116
  }
2544
4117
  }
2545
4118
  async closePage(options = {}, world = null) {
2546
- const startTime = Date.now();
2547
- let error = null;
2548
- let screenshotId = null;
2549
- let screenshotPath = null;
2550
- 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
+ };
2551
4132
  try {
4133
+ await _preCommand(state, this);
2552
4134
  await this.page.close();
2553
4135
  }
2554
4136
  catch (e) {
2555
- console.log(".");
4137
+ await _commandError(state, e, this);
2556
4138
  }
2557
4139
  finally {
2558
- await new Promise((resolve) => setTimeout(resolve, 2000));
2559
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2560
- const endTime = Date.now();
2561
- this._reportToWorld(world, {
2562
- type: Types.CLOSE_PAGE,
2563
- text: "close page",
2564
- screenshotId,
2565
- result: error
2566
- ? {
2567
- status: "FAILED",
2568
- startTime,
2569
- endTime,
2570
- 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;
2571
4175
  }
2572
- : {
2573
- status: "PASSED",
2574
- startTime,
2575
- endTime,
2576
- },
2577
- info: info,
2578
- });
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);
4239
+ }
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;
2579
4248
  }
4249
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
4250
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
2580
4251
  }
2581
4252
  async setViewportSize(width, hight, options = {}, world = null) {
2582
4253
  const startTime = Date.now();
@@ -2594,15 +4265,16 @@ class StableBrowser {
2594
4265
  await this.page.setViewportSize({ width: width, height: hight });
2595
4266
  }
2596
4267
  catch (e) {
2597
- console.log(".");
4268
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2598
4269
  }
2599
4270
  finally {
2600
4271
  await new Promise((resolve) => setTimeout(resolve, 2000));
2601
4272
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2602
4273
  const endTime = Date.now();
2603
- this._reportToWorld(world, {
4274
+ _reportToWorld(world, {
2604
4275
  type: Types.SET_VIEWPORT,
2605
4276
  text: "set viewport size to " + width + "x" + hight,
4277
+ _text: "Set the viewport size to " + width + "x" + hight,
2606
4278
  screenshotId,
2607
4279
  result: error
2608
4280
  ? {
@@ -2630,13 +4302,13 @@ class StableBrowser {
2630
4302
  await this.page.reload();
2631
4303
  }
2632
4304
  catch (e) {
2633
- console.log(".");
4305
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2634
4306
  }
2635
4307
  finally {
2636
4308
  await new Promise((resolve) => setTimeout(resolve, 2000));
2637
4309
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2638
4310
  const endTime = Date.now();
2639
- this._reportToWorld(world, {
4311
+ _reportToWorld(world, {
2640
4312
  type: Types.GET_PAGE_STATUS,
2641
4313
  text: "page relaod",
2642
4314
  screenshotId,
@@ -2672,11 +4344,254 @@ class StableBrowser {
2672
4344
  console.log("#-#");
2673
4345
  }
2674
4346
  }
2675
- _reportToWorld(world, properties) {
2676
- if (!world || !world.attach) {
2677
- 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
+ if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
4379
+ this.trace = true;
4380
+ const traceFolder = path.join(this.context.reportFolder, "trace");
4381
+ if (!fs.existsSync(traceFolder)) {
4382
+ fs.mkdirSync(traceFolder, { recursive: true });
4383
+ }
4384
+ this.traceFolder = traceFolder;
4385
+ await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
4386
+ }
4387
+ }
4388
+ async afterScenario(world, scenario) {
4389
+ const id = scenario.testCaseStartedId;
4390
+ if (this.trace) {
4391
+ await this.context.playContext.tracing.stop({
4392
+ path: path.join(this.traceFolder, `trace-${id}.zip`),
4393
+ });
4394
+ }
4395
+ }
4396
+ async beforeStep(world, step) {
4397
+ if (step?.pickleStep && this.trace) {
4398
+ await this.context.playContext.tracing.group(`Step: ${step.pickleStep.text}`);
4399
+ }
4400
+ this.stepTags = [];
4401
+ if (!this.beforeScenarioCalled) {
4402
+ this.beforeScenario(world, step);
4403
+ this.context.loadedRoutes = null;
4404
+ }
4405
+ if (this.stepIndex === undefined) {
4406
+ this.stepIndex = 0;
4407
+ }
4408
+ else {
4409
+ this.stepIndex++;
4410
+ }
4411
+ if (step && step.pickleStep && step.pickleStep.text) {
4412
+ this.stepName = step.pickleStep.text;
4413
+ let printableStepName = this.stepName;
4414
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4415
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4416
+ return `\x1b[33m"${p1}"\x1b[0m`;
4417
+ });
4418
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4419
+ }
4420
+ else if (step && step.text) {
4421
+ this.stepName = step.text;
4422
+ }
4423
+ else {
4424
+ this.stepName = "step " + this.stepIndex;
4425
+ }
4426
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4427
+ if (this.context.browserObject.context) {
4428
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
4429
+ }
4430
+ }
4431
+ if (this.initSnapshotTaken === false) {
4432
+ this.initSnapshotTaken = true;
4433
+ if (world &&
4434
+ world.attach &&
4435
+ !process.env.DISABLE_SNAPSHOT &&
4436
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4437
+ const snapshot = await this.getAriaSnapshot();
4438
+ if (snapshot) {
4439
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
4440
+ }
4441
+ }
4442
+ }
4443
+ this.context.routeResults = null;
4444
+ this.context.loadedRoutes = null;
4445
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4446
+ networkBeforeStep(this.stepName, this.context);
4447
+ this.inStepReport = false;
4448
+ }
4449
+ setStepTags(tags) {
4450
+ this.stepTags = tags;
4451
+ }
4452
+ async getAriaSnapshot() {
4453
+ try {
4454
+ // find the page url
4455
+ const url = await this.page.url();
4456
+ // extract the path from the url
4457
+ const path = new URL(url).pathname;
4458
+ // get the page title
4459
+ const title = await this.page.title();
4460
+ // go over other frams
4461
+ const frames = this.page.frames();
4462
+ const snapshots = [];
4463
+ const content = [`- path: ${path}`, `- title: ${title}`];
4464
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
4465
+ for (let i = 0; i < frames.length; i++) {
4466
+ const frame = frames[i];
4467
+ try {
4468
+ // Ensure frame is attached and has body
4469
+ const body = frame.locator("body");
4470
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4471
+ const snapshot = await body.ariaSnapshot({ timeout });
4472
+ if (!snapshot) {
4473
+ continue;
4474
+ }
4475
+ content.push(`- frame: ${i}`);
4476
+ content.push(snapshot);
4477
+ }
4478
+ catch (innerErr) {
4479
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4480
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4481
+ }
4482
+ }
4483
+ return content.join("\n");
4484
+ }
4485
+ catch (e) {
4486
+ console.log("Error in getAriaSnapshot");
4487
+ //console.debug(e);
4488
+ }
4489
+ return null;
4490
+ }
4491
+ /**
4492
+ * Sends command with custom payload to report.
4493
+ * @param commandText - Title of the command to be shown in the report.
4494
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
4495
+ * @param content - Content of the command to be shown in the report.
4496
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
4497
+ * @param world - Optional world context.
4498
+ * @public
4499
+ */
4500
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
4501
+ const state = {
4502
+ options,
4503
+ world,
4504
+ locate: false,
4505
+ scroll: false,
4506
+ screenshot: options.screenshot ?? false,
4507
+ highlight: options.highlight ?? false,
4508
+ type: Types.REPORT_COMMAND,
4509
+ text: commandText,
4510
+ _text: commandText,
4511
+ operation: "report_command",
4512
+ log: "***** " + commandText + " *****\n",
4513
+ };
4514
+ try {
4515
+ await _preCommand(state, this);
4516
+ const payload = {
4517
+ type: options.type ?? "text",
4518
+ content: content,
4519
+ screenshotId: null,
4520
+ };
4521
+ state.payload = payload;
4522
+ if (commandStatus === "FAILED") {
4523
+ state.throwError = true;
4524
+ throw new Error(commandText);
4525
+ }
4526
+ }
4527
+ catch (e) {
4528
+ await _commandError(state, e, this);
4529
+ }
4530
+ finally {
4531
+ await _commandFinally(state, this);
4532
+ }
4533
+ }
4534
+ async afterStep(world, step, result) {
4535
+ this.stepName = null;
4536
+ if (this.context) {
4537
+ this.context.examplesRow = null;
4538
+ }
4539
+ if (!this.inStepReport) {
4540
+ // check the step result
4541
+ if (result && result.status === "FAILED" && world && world.attach) {
4542
+ await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
4543
+ }
4544
+ }
4545
+ if (world &&
4546
+ world.attach &&
4547
+ !process.env.DISABLE_SNAPSHOT &&
4548
+ !this.fastMode &&
4549
+ !this.stepTags.includes("fast-mode")) {
4550
+ const snapshot = await this.getAriaSnapshot();
4551
+ if (snapshot) {
4552
+ const obj = {};
4553
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4554
+ }
4555
+ }
4556
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4557
+ if (this.context.routeResults) {
4558
+ if (world && world.attach) {
4559
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4560
+ }
4561
+ }
4562
+ if (!process.env.TEMP_RUN) {
4563
+ const state = {
4564
+ world,
4565
+ locate: false,
4566
+ scroll: false,
4567
+ screenshot: true,
4568
+ highlight: true,
4569
+ type: Types.STEP_COMPLETE,
4570
+ text: "end of scenario",
4571
+ _text: "end of scenario",
4572
+ operation: "step_complete",
4573
+ log: "***** " + "end of scenario" + " *****\n",
4574
+ };
4575
+ try {
4576
+ await _preCommand(state, this);
4577
+ }
4578
+ catch (e) {
4579
+ await _commandError(state, e, this);
4580
+ }
4581
+ finally {
4582
+ await _commandFinally(state, this);
4583
+ }
4584
+ }
4585
+ networkAfterStep(this.stepName, this.context);
4586
+ if (process.env.TEMP_RUN === "true") {
4587
+ // Put a sleep for some time to allow the browser to finish processing
4588
+ if (!this.stepTags.includes("fast-mode")) {
4589
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4590
+ }
4591
+ }
4592
+ if (this.trace) {
4593
+ await this.context.playContext.tracing.groupEnd();
2678
4594
  }
2679
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2680
4595
  }
2681
4596
  }
2682
4597
  function createTimedPromise(promise, label) {
@@ -2684,156 +4599,5 @@ function createTimedPromise(promise, label) {
2684
4599
  .then((result) => ({ status: "fulfilled", label, result }))
2685
4600
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2686
4601
  }
2687
- const KEYBOARD_EVENTS = [
2688
- "ALT",
2689
- "AltGraph",
2690
- "CapsLock",
2691
- "Control",
2692
- "Fn",
2693
- "FnLock",
2694
- "Hyper",
2695
- "Meta",
2696
- "NumLock",
2697
- "ScrollLock",
2698
- "Shift",
2699
- "Super",
2700
- "Symbol",
2701
- "SymbolLock",
2702
- "Enter",
2703
- "Tab",
2704
- "ArrowDown",
2705
- "ArrowLeft",
2706
- "ArrowRight",
2707
- "ArrowUp",
2708
- "End",
2709
- "Home",
2710
- "PageDown",
2711
- "PageUp",
2712
- "Backspace",
2713
- "Clear",
2714
- "Copy",
2715
- "CrSel",
2716
- "Cut",
2717
- "Delete",
2718
- "EraseEof",
2719
- "ExSel",
2720
- "Insert",
2721
- "Paste",
2722
- "Redo",
2723
- "Undo",
2724
- "Accept",
2725
- "Again",
2726
- "Attn",
2727
- "Cancel",
2728
- "ContextMenu",
2729
- "Escape",
2730
- "Execute",
2731
- "Find",
2732
- "Finish",
2733
- "Help",
2734
- "Pause",
2735
- "Play",
2736
- "Props",
2737
- "Select",
2738
- "ZoomIn",
2739
- "ZoomOut",
2740
- "BrightnessDown",
2741
- "BrightnessUp",
2742
- "Eject",
2743
- "LogOff",
2744
- "Power",
2745
- "PowerOff",
2746
- "PrintScreen",
2747
- "Hibernate",
2748
- "Standby",
2749
- "WakeUp",
2750
- "AllCandidates",
2751
- "Alphanumeric",
2752
- "CodeInput",
2753
- "Compose",
2754
- "Convert",
2755
- "Dead",
2756
- "FinalMode",
2757
- "GroupFirst",
2758
- "GroupLast",
2759
- "GroupNext",
2760
- "GroupPrevious",
2761
- "ModeChange",
2762
- "NextCandidate",
2763
- "NonConvert",
2764
- "PreviousCandidate",
2765
- "Process",
2766
- "SingleCandidate",
2767
- "HangulMode",
2768
- "HanjaMode",
2769
- "JunjaMode",
2770
- "Eisu",
2771
- "Hankaku",
2772
- "Hiragana",
2773
- "HiraganaKatakana",
2774
- "KanaMode",
2775
- "KanjiMode",
2776
- "Katakana",
2777
- "Romaji",
2778
- "Zenkaku",
2779
- "ZenkakuHanaku",
2780
- "F1",
2781
- "F2",
2782
- "F3",
2783
- "F4",
2784
- "F5",
2785
- "F6",
2786
- "F7",
2787
- "F8",
2788
- "F9",
2789
- "F10",
2790
- "F11",
2791
- "F12",
2792
- "Soft1",
2793
- "Soft2",
2794
- "Soft3",
2795
- "Soft4",
2796
- "ChannelDown",
2797
- "ChannelUp",
2798
- "Close",
2799
- "MailForward",
2800
- "MailReply",
2801
- "MailSend",
2802
- "MediaFastForward",
2803
- "MediaPause",
2804
- "MediaPlay",
2805
- "MediaPlayPause",
2806
- "MediaRecord",
2807
- "MediaRewind",
2808
- "MediaStop",
2809
- "MediaTrackNext",
2810
- "MediaTrackPrevious",
2811
- "AudioBalanceLeft",
2812
- "AudioBalanceRight",
2813
- "AudioBassBoostDown",
2814
- "AudioBassBoostToggle",
2815
- "AudioBassBoostUp",
2816
- "AudioFaderFront",
2817
- "AudioFaderRear",
2818
- "AudioSurroundModeNext",
2819
- "AudioTrebleDown",
2820
- "AudioTrebleUp",
2821
- "AudioVolumeDown",
2822
- "AudioVolumeMute",
2823
- "AudioVolumeUp",
2824
- "MicrophoneToggle",
2825
- "MicrophoneVolumeDown",
2826
- "MicrophoneVolumeMute",
2827
- "MicrophoneVolumeUp",
2828
- "TV",
2829
- "TV3DMode",
2830
- "TVAntennaCable",
2831
- "TVAudioDescription",
2832
- ];
2833
- function unEscapeString(str) {
2834
- const placeholder = "__NEWLINE__";
2835
- str = str.replace(new RegExp(placeholder, "g"), "\n");
2836
- return str;
2837
- }
2838
4602
  export { StableBrowser };
2839
4603
  //# sourceMappingURL=stable_browser.js.map