automation_model 1.0.494-dev → 1.0.494

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 +205 -52
  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 -22
  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 -90
  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 +2601 -819
  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,112 +2402,543 @@ 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
+ }
2460
+ }
2461
+ state.info.value = state.value;
2462
+ this.setTestData({ [variable]: state.value }, world);
2463
+ this.logger.info("set test data: " + variable + "=" + state.value);
2464
+ if (process.env.MODE === "executions") {
2465
+ const globalDataFile = "global_test_data.json";
2466
+ if (existsSync(globalDataFile)) {
2467
+ this.saveTestDataAsGlobal({}, world);
2468
+ }
1810
2469
  }
1811
- this.setTestData({ [variable]: info.value }, world);
1812
- this.logger.info("set test data: " + variable + "=" + info.value);
1813
- return info;
2470
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2471
+ return state.info;
1814
2472
  }
1815
2473
  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;
2474
+ await _commandError(state, e, this);
1823
2475
  }
1824
2476
  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
- });
2477
+ await _commandFinally(state, this);
1847
2478
  }
1848
2479
  }
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");
2480
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2481
+ const state = {
2482
+ selectors,
2483
+ _params,
2484
+ property,
2485
+ variable,
2486
+ options,
2487
+ world,
2488
+ type: Types.EXTRACT_PROPERTY,
2489
+ text: `Extract property from element`,
2490
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2491
+ operation: "extractProperty",
2492
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2493
+ allowDisabled: true,
2494
+ };
2495
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2496
+ try {
2497
+ await _preCommand(state, this);
2498
+ switch (property) {
2499
+ case "inner_text":
2500
+ state.value = await state.element.innerText();
2501
+ break;
2502
+ case "href":
2503
+ state.value = await state.element.getAttribute("href");
2504
+ break;
2505
+ case "value":
2506
+ state.value = await state.element.inputValue();
2507
+ break;
2508
+ case "text":
2509
+ state.value = await state.element.textContent();
2510
+ break;
2511
+ default:
2512
+ if (property.startsWith("dataset.")) {
2513
+ const dataAttribute = property.substring(8);
2514
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2515
+ }
2516
+ else {
2517
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2518
+ }
2519
+ }
2520
+ if (options !== null) {
2521
+ if (options.regex && options.regex !== "") {
2522
+ // Construct a regex pattern from the provided string
2523
+ const regex = options.regex.slice(1, -1);
2524
+ const regexPattern = new RegExp(regex, "g");
2525
+ const matches = state.value.match(regexPattern);
2526
+ if (matches) {
2527
+ let newValue = "";
2528
+ for (const match of matches) {
2529
+ newValue += match;
2530
+ }
2531
+ state.value = newValue;
2532
+ }
2533
+ }
2534
+ if (options.trimSpaces && options.trimSpaces === true) {
2535
+ state.value = state.value.trim();
2536
+ }
2537
+ }
2538
+ state.info.value = state.value;
2539
+ this.setTestData({ [variable]: state.value }, world);
2540
+ this.logger.info("set test data: " + variable + "=" + state.value);
2541
+ if (process.env.MODE === "executions") {
2542
+ const globalDataFile = "global_test_data.json";
2543
+ if (existsSync(globalDataFile)) {
2544
+ this.saveTestDataAsGlobal({}, world);
2545
+ }
1860
2546
  }
2547
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2548
+ return state.info;
1861
2549
  }
1862
- const startTime = Date.now();
1863
- let timeout = 60000;
1864
- if (options && options.timeout) {
1865
- timeout = options.timeout;
2550
+ catch (e) {
2551
+ await _commandError(state, e, this);
1866
2552
  }
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) {
2553
+ finally {
2554
+ await _commandFinally(state, this);
2555
+ }
2556
+ }
2557
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2558
+ const state = {
2559
+ selectors,
2560
+ _params,
2561
+ attribute,
2562
+ value,
2563
+ options,
2564
+ world,
2565
+ type: Types.VERIFY_ATTRIBUTE,
2566
+ highlight: true,
2567
+ screenshot: true,
2568
+ text: `Verify element attribute`,
2569
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2570
+ operation: "verifyAttribute",
2571
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2572
+ allowDisabled: true,
2573
+ };
2574
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2575
+ let val;
2576
+ let expectedValue;
2577
+ try {
2578
+ await _preCommand(state, this);
2579
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2580
+ state.info.expectedValue = expectedValue;
2581
+ switch (attribute) {
2582
+ case "innerText":
2583
+ val = String(await state.element.innerText());
2584
+ break;
2585
+ case "text":
2586
+ val = String(await state.element.textContent());
2587
+ break;
2588
+ case "value":
2589
+ val = String(await state.element.inputValue());
2590
+ break;
2591
+ case "checked":
2592
+ val = String(await state.element.isChecked());
2593
+ break;
2594
+ case "disabled":
2595
+ val = String(await state.element.isDisabled());
2596
+ break;
2597
+ case "readOnly":
2598
+ const isEditable = await state.element.isEditable();
2599
+ val = String(!isEditable);
2600
+ break;
2601
+ default:
2602
+ val = String(await state.element.getAttribute(attribute));
2603
+ break;
2604
+ }
2605
+ state.info.value = val;
2606
+ let regex;
2607
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2608
+ const patternBody = expectedValue.slice(1, -1);
2609
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2610
+ regex = new RegExp(processedPattern, "gs");
2611
+ state.info.regex = true;
2612
+ }
2613
+ else {
2614
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2615
+ regex = new RegExp(escapedPattern, "g");
2616
+ }
2617
+ if (attribute === "innerText") {
2618
+ if (state.info.regex) {
2619
+ if (!regex.test(val)) {
2620
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2621
+ state.info.failCause.assertionFailed = true;
2622
+ state.info.failCause.lastError = errorMessage;
2623
+ throw new Error(errorMessage);
2624
+ }
2625
+ }
2626
+ else {
2627
+ const valLines = val.split("\n");
2628
+ const expectedLines = expectedValue.split("\n");
2629
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2630
+ if (!isPart) {
2631
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2632
+ state.info.failCause.assertionFailed = true;
2633
+ state.info.failCause.lastError = errorMessage;
2634
+ throw new Error(errorMessage);
2635
+ }
2636
+ }
2637
+ }
2638
+ else {
2639
+ if (!val.match(regex)) {
2640
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2641
+ state.info.failCause.assertionFailed = true;
2642
+ state.info.failCause.lastError = errorMessage;
2643
+ throw new Error(errorMessage);
2644
+ }
2645
+ }
2646
+ return state.info;
2647
+ }
2648
+ catch (e) {
2649
+ await _commandError(state, e, this);
2650
+ }
2651
+ finally {
2652
+ await _commandFinally(state, this);
2653
+ }
2654
+ }
2655
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2656
+ const state = {
2657
+ selectors,
2658
+ _params,
2659
+ property,
2660
+ value,
2661
+ options,
2662
+ world,
2663
+ type: Types.VERIFY_PROPERTY,
2664
+ highlight: true,
2665
+ screenshot: true,
2666
+ text: `Verify element property`,
2667
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2668
+ operation: "verifyProperty",
2669
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2670
+ allowDisabled: true,
2671
+ };
2672
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2673
+ let val;
2674
+ let expectedValue;
2675
+ try {
2676
+ await _preCommand(state, this);
2677
+ expectedValue = await this._replaceWithLocalData(value, world);
2678
+ state.info.expectedValue = expectedValue;
2679
+ switch (property) {
2680
+ case "innerText":
2681
+ val = String(await state.element.innerText());
2682
+ break;
2683
+ case "text":
2684
+ val = String(await state.element.textContent());
2685
+ break;
2686
+ case "value":
2687
+ val = String(await state.element.inputValue());
2688
+ break;
2689
+ case "checked":
2690
+ val = String(await state.element.isChecked());
2691
+ break;
2692
+ case "disabled":
2693
+ val = String(await state.element.isDisabled());
2694
+ break;
2695
+ case "readOnly":
2696
+ const isEditable = await state.element.isEditable();
2697
+ val = String(!isEditable);
2698
+ break;
2699
+ case "innerHTML":
2700
+ val = String(await state.element.innerHTML());
2701
+ break;
2702
+ case "outerHTML":
2703
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2704
+ break;
2705
+ default:
2706
+ if (property.startsWith("dataset.")) {
2707
+ const dataAttribute = property.substring(8);
2708
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2709
+ }
2710
+ else {
2711
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2712
+ }
2713
+ }
2714
+ // Helper function to remove all style="" attributes
2715
+ const removeStyleAttributes = (htmlString) => {
2716
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2717
+ };
2718
+ // Remove style attributes for innerHTML and outerHTML properties
2719
+ if (property === "innerHTML" || property === "outerHTML") {
2720
+ val = removeStyleAttributes(val);
2721
+ expectedValue = removeStyleAttributes(expectedValue);
2722
+ }
2723
+ state.info.value = val;
2724
+ let regex;
2725
+ state.info.value = val;
2726
+ const isRegex = expectedValue.startsWith("regex:");
2727
+ const isContains = expectedValue.startsWith("contains:");
2728
+ const isExact = expectedValue.startsWith("exact:");
2729
+ let matchPassed = false;
2730
+ if (isRegex) {
2731
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2732
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2733
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2734
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2735
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2736
+ const regex = new RegExp(patternBody, flags);
2737
+ state.info.regex = true;
2738
+ matchPassed = regex.test(val);
2739
+ }
2740
+ else {
2741
+ // Fallback: treat as literal
2742
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2743
+ const regex = new RegExp(escapedPattern, "g");
2744
+ matchPassed = regex.test(val);
2745
+ }
2746
+ }
2747
+ else if (isContains) {
2748
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2749
+ matchPassed = val.includes(containsValue);
2750
+ }
2751
+ else if (isExact) {
2752
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2753
+ matchPassed = val === exactValue;
2754
+ }
2755
+ else if (property === "innerText") {
2756
+ // Default innerText logic
2757
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2758
+ const valLines = val.split("\n");
2759
+ const expectedLines = normalizedExpectedValue.split("\n");
2760
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2761
+ }
2762
+ else {
2763
+ // Fallback exact or loose match
2764
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2765
+ const regex = new RegExp(escapedPattern, "g");
2766
+ matchPassed = regex.test(val);
2767
+ }
2768
+ if (!matchPassed) {
2769
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2770
+ state.info.failCause.assertionFailed = true;
2771
+ state.info.failCause.lastError = errorMessage;
2772
+ throw new Error(errorMessage);
2773
+ }
2774
+ return state.info;
2775
+ }
2776
+ catch (e) {
2777
+ await _commandError(state, e, this);
2778
+ }
2779
+ finally {
2780
+ await _commandFinally(state, this);
2781
+ }
2782
+ }
2783
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2784
+ // Convert timeout from seconds to milliseconds
2785
+ const timeoutMs = timeout * 1000;
2786
+ const state = {
2787
+ selectors,
2788
+ _params,
2789
+ condition,
2790
+ timeout: timeoutMs, // Store as milliseconds for internal use
2791
+ options,
2792
+ world,
2793
+ type: Types.CONDITIONAL_WAIT,
2794
+ highlight: true,
2795
+ screenshot: true,
2796
+ text: `Conditional wait for element`,
2797
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2798
+ operation: "conditionalWait",
2799
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2800
+ allowDisabled: true,
2801
+ info: {},
2802
+ };
2803
+ state.options ??= { timeout: timeoutMs };
2804
+ // Initialize startTime outside try block to ensure it's always accessible
2805
+ const startTime = Date.now();
2806
+ let conditionMet = false;
2807
+ let currentValue = null;
2808
+ let lastError = null;
2809
+ // Main retry loop - continues until timeout or condition is met
2810
+ while (Date.now() - startTime < timeoutMs) {
2811
+ const elapsedTime = Date.now() - startTime;
2812
+ const remainingTime = timeoutMs - elapsedTime;
2813
+ try {
2814
+ // Try to execute _preCommand (element location)
2815
+ await _preCommand(state, this);
2816
+ // If _preCommand succeeds, start condition checking
2817
+ const checkCondition = async () => {
2818
+ try {
2819
+ switch (condition.toLowerCase()) {
2820
+ case "checked":
2821
+ currentValue = await state.element.isChecked();
2822
+ return currentValue === true;
2823
+ case "unchecked":
2824
+ currentValue = await state.element.isChecked();
2825
+ return currentValue === false;
2826
+ case "visible":
2827
+ currentValue = await state.element.isVisible();
2828
+ return currentValue === true;
2829
+ case "hidden":
2830
+ currentValue = await state.element.isVisible();
2831
+ return currentValue === false;
2832
+ case "enabled":
2833
+ currentValue = await state.element.isDisabled();
2834
+ return currentValue === false;
2835
+ case "disabled":
2836
+ currentValue = await state.element.isDisabled();
2837
+ return currentValue === true;
2838
+ case "editable":
2839
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2840
+ currentValue = await state.element.isContentEditable();
2841
+ return currentValue === true;
2842
+ default:
2843
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2844
+ state.info.success = false;
2845
+ return false;
2846
+ }
2847
+ }
2848
+ catch (error) {
2849
+ // Don't throw here, just return false to continue retrying
2850
+ return false;
2851
+ }
2852
+ };
2853
+ // Inner loop for condition checking (once element is located)
2854
+ while (Date.now() - startTime < timeoutMs) {
2855
+ const currentElapsedTime = Date.now() - startTime;
2856
+ conditionMet = await checkCondition();
2857
+ if (conditionMet) {
2858
+ break;
2859
+ }
2860
+ // Check if we still have time for another attempt
2861
+ if (Date.now() - startTime + 50 < timeoutMs) {
2862
+ await new Promise((res) => setTimeout(res, 50));
2863
+ }
2864
+ else {
2865
+ break;
2866
+ }
2867
+ }
2868
+ // If we got here and condition is met, break out of main loop
2869
+ if (conditionMet) {
2870
+ break;
2871
+ }
2872
+ // If condition not met but no exception, we've timed out
2873
+ break;
2874
+ }
2875
+ catch (e) {
2876
+ lastError = e;
2877
+ const currentElapsedTime = Date.now() - startTime;
2878
+ const timeLeft = timeoutMs - currentElapsedTime;
2879
+ // Check if we have enough time left to retry
2880
+ if (timeLeft > 100) {
2881
+ await new Promise((resolve) => setTimeout(resolve, 50));
2882
+ }
2883
+ else {
2884
+ break;
2885
+ }
2886
+ }
2887
+ }
2888
+ const actualWaitTime = Date.now() - startTime;
2889
+ state.info = {
2890
+ success: conditionMet,
2891
+ conditionMet,
2892
+ actualWaitTime,
2893
+ currentValue,
2894
+ lastError: lastError?.message || null,
2895
+ message: conditionMet
2896
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2897
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2898
+ };
2899
+ if (lastError) {
2900
+ state.log += `Last error: ${lastError.message}\n`;
2901
+ }
2902
+ try {
2903
+ await _commandFinally(state, this);
2904
+ }
2905
+ catch (finallyError) {
2906
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2907
+ }
2908
+ return state.info;
2909
+ }
2910
+ async extractEmailData(emailAddress, options, world) {
2911
+ if (!emailAddress) {
2912
+ throw new Error("email address is null");
2913
+ }
2914
+ // check if address contain @
2915
+ if (emailAddress.indexOf("@") === -1) {
2916
+ emailAddress = emailAddress + "@blinq-mail.io";
2917
+ }
2918
+ else {
2919
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
2920
+ throw new Error("email address should end with @blinq-mail.io");
2921
+ }
2922
+ }
2923
+ const startTime = Date.now();
2924
+ let timeout = 60000;
2925
+ if (options && options.timeout) {
2926
+ timeout = options.timeout;
2927
+ }
2928
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2929
+ const request = {
2930
+ method: "POST",
2931
+ url: serviceUrl,
2932
+ headers: {
2933
+ "Content-Type": "application/json",
2934
+ Authorization: `Bearer ${process.env.TOKEN}`,
2935
+ },
2936
+ data: JSON.stringify({
2937
+ email: emailAddress,
2938
+ }),
2939
+ };
2940
+ let errorCount = 0;
2941
+ while (true) {
1881
2942
  try {
1882
2943
  let result = await this.context.api.request(request);
1883
2944
  // the response body expected to be the following:
@@ -1909,6 +2970,12 @@ class StableBrowser {
1909
2970
  emailUrl = url;
1910
2971
  codeOrUrlFound = true;
1911
2972
  }
2973
+ if (process.env.MODE === "executions") {
2974
+ const globalDataFile = "global_test_data.json";
2975
+ if (existsSync(globalDataFile)) {
2976
+ this.saveTestDataAsGlobal({}, world);
2977
+ }
2978
+ }
1912
2979
  if (codeOrUrlFound) {
1913
2980
  return { emailUrl, emailCode };
1914
2981
  }
@@ -1920,7 +2987,8 @@ class StableBrowser {
1920
2987
  catch (e) {
1921
2988
  errorCount++;
1922
2989
  if (errorCount > 3) {
1923
- throw e;
2990
+ // throw e;
2991
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
1924
2992
  }
1925
2993
  // ignore
1926
2994
  }
@@ -1934,27 +3002,32 @@ class StableBrowser {
1934
3002
  async _highlightElements(scope, css) {
1935
3003
  try {
1936
3004
  if (!scope) {
3005
+ // console.log(`Scope is not defined`);
1937
3006
  return;
1938
3007
  }
1939
3008
  if (!css) {
1940
3009
  scope
1941
3010
  .evaluate((node) => {
1942
3011
  if (node && node.style) {
1943
- let originalBorder = node.style.border;
1944
- node.style.border = "2px solid red";
3012
+ let originalOutline = node.style.outline;
3013
+ // console.log(`Original outline was: ${originalOutline}`);
3014
+ // node.__previousOutline = originalOutline;
3015
+ node.style.outline = "2px solid red";
3016
+ // console.log(`New outline is: ${node.style.outline}`);
1945
3017
  if (window) {
1946
3018
  window.addEventListener("beforeunload", function (e) {
1947
- node.style.border = originalBorder;
3019
+ node.style.outline = originalOutline;
1948
3020
  });
1949
3021
  }
1950
3022
  setTimeout(function () {
1951
- node.style.border = originalBorder;
3023
+ node.style.outline = originalOutline;
1952
3024
  }, 2000);
1953
3025
  }
1954
3026
  })
1955
3027
  .then(() => { })
1956
3028
  .catch((e) => {
1957
3029
  // ignore
3030
+ // console.error(`Could not highlight node : ${e}`);
1958
3031
  });
1959
3032
  }
1960
3033
  else {
@@ -1970,17 +3043,18 @@ class StableBrowser {
1970
3043
  if (!element.style) {
1971
3044
  return;
1972
3045
  }
1973
- var originalBorder = element.style.border;
3046
+ let originalOutline = element.style.outline;
3047
+ element.__previousOutline = originalOutline;
1974
3048
  // Set the new border to be red and 2px solid
1975
- element.style.border = "2px solid red";
3049
+ element.style.outline = "2px solid red";
1976
3050
  if (window) {
1977
3051
  window.addEventListener("beforeunload", function (e) {
1978
- element.style.border = originalBorder;
3052
+ element.style.outline = originalOutline;
1979
3053
  });
1980
3054
  }
1981
3055
  // Set a timeout to revert to the original border after 2 seconds
1982
3056
  setTimeout(function () {
1983
- element.style.border = originalBorder;
3057
+ element.style.outline = originalOutline;
1984
3058
  }, 2000);
1985
3059
  }
1986
3060
  return;
@@ -1988,6 +3062,7 @@ class StableBrowser {
1988
3062
  .then(() => { })
1989
3063
  .catch((e) => {
1990
3064
  // ignore
3065
+ // console.error(`Could not highlight css: ${e}`);
1991
3066
  });
1992
3067
  }
1993
3068
  }
@@ -1995,8 +3070,49 @@ class StableBrowser {
1995
3070
  console.debug(error);
1996
3071
  }
1997
3072
  }
3073
+ _matcher(text) {
3074
+ if (!text) {
3075
+ return { matcher: "contains", queryText: "" };
3076
+ }
3077
+ if (text.length < 2) {
3078
+ return { matcher: "contains", queryText: text };
3079
+ }
3080
+ const split = text.split(":");
3081
+ const matcher = split[0].toLowerCase();
3082
+ const queryText = split.slice(1).join(":").trim();
3083
+ return { matcher, queryText };
3084
+ }
3085
+ _getDomain(url) {
3086
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
3087
+ return "";
3088
+ }
3089
+ let hostnameFragments = url.split("/")[2].split(".");
3090
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
3091
+ return hostnameFragments.join("-").split(":").join("-");
3092
+ }
3093
+ let n = hostnameFragments.length;
3094
+ let fragments = [...hostnameFragments];
3095
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
3096
+ hostnameFragments.pop();
3097
+ n = hostnameFragments.length;
3098
+ }
3099
+ if (n == 0) {
3100
+ if (fragments[0] === "www")
3101
+ fragments = fragments.slice(1);
3102
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
3103
+ }
3104
+ if (hostnameFragments[0] === "www")
3105
+ hostnameFragments = hostnameFragments.slice(1);
3106
+ return hostnameFragments.join(".");
3107
+ }
3108
+ /**
3109
+ * Verify the page path matches the given path.
3110
+ * @param {string} pathPart - The path to verify.
3111
+ * @param {object} options - Options for verification.
3112
+ * @param {object} world - The world context.
3113
+ * @returns {Promise<object>} - The state info after verification.
3114
+ */
1998
3115
  async verifyPagePath(pathPart, options = {}, world = null) {
1999
- const startTime = Date.now();
2000
3116
  let error = null;
2001
3117
  let screenshotId = null;
2002
3118
  let screenshotPath = null;
@@ -2010,159 +3126,534 @@ class StableBrowser {
2010
3126
  pathPart = newValue;
2011
3127
  }
2012
3128
  info.pathPart = pathPart;
3129
+ const { matcher, queryText } = this._matcher(pathPart);
3130
+ const state = {
3131
+ text_search: queryText,
3132
+ options,
3133
+ world,
3134
+ locate: false,
3135
+ scroll: false,
3136
+ highlight: false,
3137
+ type: Types.VERIFY_PAGE_PATH,
3138
+ text: `Verify the page url is ${queryText}`,
3139
+ _text: `Verify the page url is ${queryText}`,
3140
+ operation: "verifyPagePath",
3141
+ log: "***** verify page url is " + queryText + " *****\n",
3142
+ };
2013
3143
  try {
3144
+ await _preCommand(state, this);
3145
+ state.info.text = queryText;
2014
3146
  for (let i = 0; i < 30; i++) {
2015
3147
  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}`);
3148
+ switch (matcher) {
3149
+ case "exact":
3150
+ if (url !== queryText) {
3151
+ if (i === 29) {
3152
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
3153
+ }
3154
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3155
+ continue;
3156
+ }
3157
+ break;
3158
+ case "contains":
3159
+ if (!url.includes(queryText)) {
3160
+ if (i === 29) {
3161
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
3162
+ }
3163
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3164
+ continue;
3165
+ }
3166
+ break;
3167
+ case "starts-with":
3168
+ {
3169
+ const domain = this._getDomain(url);
3170
+ if (domain.length > 0 && domain !== queryText) {
3171
+ if (i === 29) {
3172
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
3173
+ }
3174
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3175
+ continue;
3176
+ }
3177
+ }
3178
+ break;
3179
+ case "ends-with":
3180
+ {
3181
+ const urlObj = new URL(url);
3182
+ let route = "/";
3183
+ if (urlObj.pathname !== "/") {
3184
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
3185
+ }
3186
+ else {
3187
+ route = "/";
3188
+ }
3189
+ if (route !== queryText) {
3190
+ if (i === 29) {
3191
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
3192
+ }
3193
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3194
+ continue;
3195
+ }
3196
+ }
3197
+ break;
3198
+ case "regex":
3199
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3200
+ if (!regex.test(url)) {
3201
+ if (i === 29) {
3202
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
3203
+ }
3204
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3205
+ continue;
3206
+ }
3207
+ break;
3208
+ default:
3209
+ console.log("Unknown matching type, defaulting to contains matching");
3210
+ if (!url.includes(pathPart)) {
3211
+ if (i === 29) {
3212
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
3213
+ }
3214
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3215
+ continue;
3216
+ }
3217
+ }
3218
+ await _screenshot(state, this);
3219
+ return state.info;
3220
+ }
3221
+ }
3222
+ catch (e) {
3223
+ state.info.failCause.lastError = e.message;
3224
+ state.info.failCause.assertionFailed = true;
3225
+ await _commandError(state, e, this);
3226
+ }
3227
+ finally {
3228
+ await _commandFinally(state, this);
3229
+ }
3230
+ }
3231
+ /**
3232
+ * Verify the page title matches the given title.
3233
+ * @param {string} title - The title to verify.
3234
+ * @param {object} options - Options for verification.
3235
+ * @param {object} world - The world context.
3236
+ * @returns {Promise<object>} - The state info after verification.
3237
+ */
3238
+ async verifyPageTitle(title, options = {}, world = null) {
3239
+ let error = null;
3240
+ let screenshotId = null;
3241
+ let screenshotPath = null;
3242
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3243
+ const newValue = await this._replaceWithLocalData(title, world);
3244
+ if (newValue !== title) {
3245
+ this.logger.info(title + "=" + newValue);
3246
+ title = newValue;
3247
+ }
3248
+ const { matcher, queryText } = this._matcher(title);
3249
+ const state = {
3250
+ text_search: queryText,
3251
+ options,
3252
+ world,
3253
+ locate: false,
3254
+ scroll: false,
3255
+ highlight: false,
3256
+ type: Types.VERIFY_PAGE_TITLE,
3257
+ text: `Verify the page title is ${queryText}`,
3258
+ _text: `Verify the page title is ${queryText}`,
3259
+ operation: "verifyPageTitle",
3260
+ log: "***** verify page title is " + queryText + " *****\n",
3261
+ };
3262
+ try {
3263
+ await _preCommand(state, this);
3264
+ state.info.text = queryText;
3265
+ for (let i = 0; i < 30; i++) {
3266
+ const foundTitle = await this.page.title();
3267
+ switch (matcher) {
3268
+ case "exact":
3269
+ if (foundTitle !== queryText) {
3270
+ if (i === 29) {
3271
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
3272
+ }
3273
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3274
+ continue;
3275
+ }
3276
+ break;
3277
+ case "contains":
3278
+ if (!foundTitle.includes(queryText)) {
3279
+ if (i === 29) {
3280
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
3281
+ }
3282
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3283
+ continue;
3284
+ }
3285
+ break;
3286
+ case "starts-with":
3287
+ if (!foundTitle.startsWith(queryText)) {
3288
+ if (i === 29) {
3289
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
3290
+ }
3291
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3292
+ continue;
3293
+ }
3294
+ break;
3295
+ case "ends-with":
3296
+ if (!foundTitle.endsWith(queryText)) {
3297
+ if (i === 29) {
3298
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
3299
+ }
3300
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3301
+ continue;
3302
+ }
3303
+ break;
3304
+ case "regex":
3305
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3306
+ if (!regex.test(foundTitle)) {
3307
+ if (i === 29) {
3308
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
3309
+ }
3310
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3311
+ continue;
3312
+ }
3313
+ break;
3314
+ default:
3315
+ console.log("Unknown matching type, defaulting to contains matching");
3316
+ if (!foundTitle.includes(title)) {
3317
+ if (i === 29) {
3318
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
3319
+ }
3320
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3321
+ continue;
3322
+ }
3323
+ }
3324
+ await _screenshot(state, this);
3325
+ return state.info;
3326
+ }
3327
+ }
3328
+ catch (e) {
3329
+ state.info.failCause.lastError = e.message;
3330
+ state.info.failCause.assertionFailed = true;
3331
+ await _commandError(state, e, this);
3332
+ }
3333
+ finally {
3334
+ await _commandFinally(state, this);
3335
+ }
3336
+ }
3337
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
3338
+ const frames = this.page.frames();
3339
+ let results = [];
3340
+ // let ignoreCase = false;
3341
+ for (let i = 0; i < frames.length; i++) {
3342
+ if (dateAlternatives.date) {
3343
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
3344
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3345
+ result.frame = frames[i];
3346
+ results.push(result);
3347
+ }
3348
+ }
3349
+ else if (numberAlternatives.number) {
3350
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
3351
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3352
+ result.frame = frames[i];
3353
+ results.push(result);
3354
+ }
3355
+ }
3356
+ else {
3357
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
3358
+ result.frame = frames[i];
3359
+ results.push(result);
3360
+ }
3361
+ }
3362
+ state.info.results = results;
3363
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3364
+ return resultWithElementsFound;
3365
+ }
3366
+ async verifyTextExistInPage(text, options = {}, world = null) {
3367
+ text = unEscapeString(text);
3368
+ const state = {
3369
+ text_search: text,
3370
+ options,
3371
+ world,
3372
+ locate: false,
3373
+ scroll: false,
3374
+ highlight: false,
3375
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
3376
+ text: `Verify the text '${maskValue(text)}' exists in page`,
3377
+ _text: `Verify the text '${text}' exists in page`,
3378
+ operation: "verifyTextExistInPage",
3379
+ log: "***** verify text " + text + " exists in page *****\n",
3380
+ };
3381
+ if (testForRegex(text)) {
3382
+ text = text.replace(/\\"/g, '"');
3383
+ }
3384
+ const timeout = this._getFindElementTimeout(options);
3385
+ //if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
3386
+ let stepFastMode = this.stepTags.includes("fast-mode");
3387
+ if (!stepFastMode) {
3388
+ if (!this.fastMode) {
3389
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3390
+ }
3391
+ else {
3392
+ await new Promise((resolve) => setTimeout(resolve, 500));
3393
+ }
3394
+ }
3395
+ const newValue = await this._replaceWithLocalData(text, world);
3396
+ if (newValue !== text) {
3397
+ this.logger.info(text + "=" + newValue);
3398
+ text = newValue;
3399
+ }
3400
+ let dateAlternatives = findDateAlternatives(text);
3401
+ let numberAlternatives = findNumberAlternatives(text);
3402
+ if (stepFastMode) {
3403
+ state.onlyFailuresScreenshot = true;
3404
+ state.scroll = false;
3405
+ state.highlight = false;
3406
+ }
3407
+ try {
3408
+ await _preCommand(state, this);
3409
+ state.info.text = text;
3410
+ while (true) {
3411
+ let resultWithElementsFound = {
3412
+ length: 0,
3413
+ };
3414
+ try {
3415
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3416
+ }
3417
+ catch (error) {
3418
+ // ignore
3419
+ }
3420
+ if (resultWithElementsFound.length === 0) {
3421
+ if (Date.now() - state.startTime > timeout) {
3422
+ throw new Error(`Text ${text} not found in page`);
2019
3423
  }
2020
3424
  await new Promise((resolve) => setTimeout(resolve, 1000));
2021
3425
  continue;
2022
3426
  }
2023
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2024
- return info;
3427
+ try {
3428
+ if (resultWithElementsFound[0].randomToken) {
3429
+ const frame = resultWithElementsFound[0].frame;
3430
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
3431
+ await this._highlightElements(frame, dataAttribute);
3432
+ const element = await frame.locator(dataAttribute).first();
3433
+ if (element) {
3434
+ await this.scrollIfNeeded(element, state.info);
3435
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3436
+ }
3437
+ }
3438
+ await _screenshot(state, this);
3439
+ return state.info;
3440
+ }
3441
+ catch (error) {
3442
+ console.error(error);
3443
+ }
2025
3444
  }
2026
3445
  }
2027
3446
  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;
3447
+ await _commandError(state, e, this);
2035
3448
  }
2036
3449
  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
- });
3450
+ await _commandFinally(state, this);
2056
3451
  }
2057
3452
  }
2058
- async verifyTextExistInPage(text, options = {}, world = null) {
3453
+ async waitForTextToDisappear(text, options = {}, world = null) {
2059
3454
  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;
3455
+ const state = {
3456
+ text_search: text,
3457
+ options,
3458
+ world,
3459
+ locate: false,
3460
+ scroll: false,
3461
+ highlight: false,
3462
+ type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
3463
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
3464
+ _text: `Verify the text '${text}' does not exist in page`,
3465
+ operation: "verifyTextNotExistInPage",
3466
+ log: "***** verify text " + text + " does not exist in page *****\n",
3467
+ };
3468
+ if (testForRegex(text)) {
3469
+ text = text.replace(/\\"/g, '"');
3470
+ }
3471
+ const timeout = this._getFindElementTimeout(options);
2065
3472
  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
3473
  const newValue = await this._replaceWithLocalData(text, world);
2070
3474
  if (newValue !== text) {
2071
3475
  this.logger.info(text + "=" + newValue);
2072
3476
  text = newValue;
2073
3477
  }
2074
- info.text = text;
2075
- let dateAlternatives = findDateAlternatives(text);
2076
- let numberAlternatives = findNumberAlternatives(text);
3478
+ let dateAlternatives = findDateAlternatives(text);
3479
+ let numberAlternatives = findNumberAlternatives(text);
3480
+ try {
3481
+ await _preCommand(state, this);
3482
+ state.info.text = text;
3483
+ let resultWithElementsFound = {
3484
+ length: null, // initial cannot be 0
3485
+ };
3486
+ while (true) {
3487
+ try {
3488
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3489
+ }
3490
+ catch (error) {
3491
+ // ignore
3492
+ }
3493
+ if (resultWithElementsFound.length === 0) {
3494
+ await _screenshot(state, this);
3495
+ return state.info;
3496
+ }
3497
+ if (Date.now() - state.startTime > timeout) {
3498
+ throw new Error(`Text ${text} found in page`);
3499
+ }
3500
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3501
+ }
3502
+ }
3503
+ catch (e) {
3504
+ await _commandError(state, e, this);
3505
+ }
3506
+ finally {
3507
+ await _commandFinally(state, this);
3508
+ }
3509
+ }
3510
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
3511
+ textAnchor = unEscapeString(textAnchor);
3512
+ textToVerify = unEscapeString(textToVerify);
3513
+ const state = {
3514
+ text_search: textToVerify,
3515
+ options,
3516
+ world,
3517
+ locate: false,
3518
+ scroll: false,
3519
+ highlight: false,
3520
+ type: Types.VERIFY_TEXT_WITH_RELATION,
3521
+ text: `Verify text with relation to another text`,
3522
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
3523
+ operation: "verify_text_with_relation",
3524
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3525
+ };
3526
+ const cmdStartTime = Date.now();
3527
+ let cmdEndTime = null;
3528
+ const timeout = this._getFindElementTimeout(options);
3529
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3530
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
3531
+ if (newValue !== textAnchor) {
3532
+ this.logger.info(textAnchor + "=" + newValue);
3533
+ textAnchor = newValue;
3534
+ }
3535
+ newValue = await this._replaceWithLocalData(textToVerify, world);
3536
+ if (newValue !== textToVerify) {
3537
+ this.logger.info(textToVerify + "=" + newValue);
3538
+ textToVerify = newValue;
3539
+ }
3540
+ let dateAlternatives = findDateAlternatives(textToVerify);
3541
+ let numberAlternatives = findNumberAlternatives(textToVerify);
3542
+ let foundAncore = false;
2077
3543
  try {
3544
+ await _preCommand(state, this);
3545
+ state.info.text = textToVerify;
3546
+ let resultWithElementsFound = {
3547
+ length: 0,
3548
+ };
2078
3549
  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
- }
3550
+ try {
3551
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
3552
+ }
3553
+ catch (error) {
3554
+ // ignore
2101
3555
  }
2102
- info.results = results;
2103
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2104
3556
  if (resultWithElementsFound.length === 0) {
2105
- if (Date.now() - startTime > timeout) {
2106
- throw new Error(`Text ${text} not found in page`);
3557
+ if (Date.now() - state.startTime > timeout) {
3558
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2107
3559
  }
2108
3560
  await new Promise((resolve) => setTimeout(resolve, 1000));
2109
3561
  continue;
2110
3562
  }
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");
3563
+ else {
3564
+ cmdEndTime = Date.now();
3565
+ if (cmdEndTime - cmdStartTime > 55000) {
3566
+ if (foundAncore) {
3567
+ throw new Error(`Text ${textToVerify} not found in page`);
3568
+ }
3569
+ else {
3570
+ throw new Error(`Text ${textAnchor} not found in page`);
3571
+ }
3572
+ }
3573
+ }
3574
+ try {
3575
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
3576
+ foundAncore = true;
3577
+ const result = resultWithElementsFound[i];
3578
+ const token = result.randomToken;
3579
+ const frame = result.frame;
3580
+ let css = `[data-blinq-id-${token}]`;
3581
+ const climbArray1 = [];
3582
+ for (let i = 0; i < climb; i++) {
3583
+ climbArray1.push("..");
3584
+ }
3585
+ let climbXpath = "xpath=" + climbArray1.join("/");
3586
+ css = css + " >> " + climbXpath;
3587
+ const count = await frame.locator(css).count();
3588
+ for (let j = 0; j < count; j++) {
3589
+ const continer = await frame.locator(css).nth(j);
3590
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
3591
+ if (result.elementCount > 0) {
3592
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
3593
+ await this._highlightElements(frame, dataAttribute);
3594
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
3595
+ // if (world && world.screenshot && !world.screenshotPath) {
3596
+ // console.log(`Highlighting for vtrt while running from recorder`);
3597
+ // this._highlightElements(frame, dataAttribute)
3598
+ // .then(async () => {
3599
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
3600
+ // this._unhighlightElements(frame, dataAttribute).then(
3601
+ // () => {}
3602
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
3603
+ // );
3604
+ // })
3605
+ // .catch(e);
3606
+ // }
3607
+ //await this._highlightElements(frame, cssAnchor);
3608
+ const element = await frame.locator(dataAttribute).first();
3609
+ // await new Promise((resolve) => setTimeout(resolve, 100));
3610
+ // await this._unhighlightElements(frame, dataAttribute);
3611
+ if (element) {
3612
+ await this.scrollIfNeeded(element, state.info);
3613
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3614
+ }
3615
+ await _screenshot(state, this);
3616
+ return state.info;
3617
+ }
3618
+ }
2119
3619
  }
2120
3620
  }
2121
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2122
- return info;
3621
+ catch (error) {
3622
+ console.error(error);
3623
+ }
2123
3624
  }
2124
3625
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2125
3626
  }
2126
3627
  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;
3628
+ await _commandError(state, e, this);
2134
3629
  }
2135
3630
  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
- });
3631
+ await _commandFinally(state, this);
2155
3632
  }
2156
3633
  }
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";
3634
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
3635
+ const frames = this.page.frames();
3636
+ let results = [];
3637
+ let ignoreCase = false;
3638
+ for (let i = 0; i < frames.length; i++) {
3639
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
3640
+ result.frame = frames[i];
3641
+ const climbArray = [];
3642
+ for (let i = 0; i < climb; i++) {
3643
+ climbArray.push("..");
3644
+ }
3645
+ let climbXpath = "xpath=" + climbArray.join("/");
3646
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
3647
+ const count = await frames[i].locator(newLocator).count();
3648
+ if (count > 0) {
3649
+ result.elementCount = count;
3650
+ result.locator = newLocator;
3651
+ results.push(result);
3652
+ }
2164
3653
  }
2165
- return serviceUrl;
3654
+ // state.info.results = results;
3655
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3656
+ return resultWithElementsFound;
2166
3657
  }
2167
3658
  async visualVerification(text, options = {}, world = null) {
2168
3659
  const startTime = Date.now();
@@ -2178,14 +3669,17 @@ class StableBrowser {
2178
3669
  throw new Error("TOKEN is not set");
2179
3670
  }
2180
3671
  try {
2181
- let serviceUrl = this._getServerUrl();
3672
+ let serviceUrl = _getServerUrl();
2182
3673
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2183
3674
  info.screenshotPath = screenshotPath;
2184
3675
  const screenshot = await this.takeScreenshot();
2185
- const request = {
2186
- method: "POST",
3676
+ let request = {
3677
+ method: "post",
3678
+ maxBodyLength: Infinity,
2187
3679
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2188
3680
  headers: {
3681
+ "x-bvt-project-id": path.basename(this.project_path),
3682
+ "x-source": "aaa",
2189
3683
  "Content-Type": "application/json",
2190
3684
  Authorization: `Bearer ${process.env.TOKEN}`,
2191
3685
  },
@@ -2194,7 +3688,7 @@ class StableBrowser {
2194
3688
  screenshot: screenshot,
2195
3689
  }),
2196
3690
  };
2197
- let result = await this.context.api.request(request);
3691
+ const result = await axios.request(request);
2198
3692
  if (result.data.status !== true) {
2199
3693
  throw new Error("Visual validation failed");
2200
3694
  }
@@ -2214,13 +3708,15 @@ class StableBrowser {
2214
3708
  info.screenshotPath = screenshotPath;
2215
3709
  Object.assign(e, { info: info });
2216
3710
  error = e;
2217
- throw e;
3711
+ // throw e;
3712
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
2218
3713
  }
2219
3714
  finally {
2220
3715
  const endTime = Date.now();
2221
- this._reportToWorld(world, {
3716
+ _reportToWorld(world, {
2222
3717
  type: Types.VERIFY_VISUAL,
2223
3718
  text: "Visual verification",
3719
+ _text: "Visual verification of " + text,
2224
3720
  screenshotId,
2225
3721
  result: error
2226
3722
  ? {
@@ -2266,6 +3762,7 @@ class StableBrowser {
2266
3762
  let screenshotPath = null;
2267
3763
  const info = {};
2268
3764
  info.log = "";
3765
+ info.locatorLog = new LocatorLog(selectors);
2269
3766
  info.operation = "getTableData";
2270
3767
  info.selectors = selectors;
2271
3768
  try {
@@ -2281,11 +3778,12 @@ class StableBrowser {
2281
3778
  info.screenshotPath = screenshotPath;
2282
3779
  Object.assign(e, { info: info });
2283
3780
  error = e;
2284
- throw e;
3781
+ // throw e;
3782
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2285
3783
  }
2286
3784
  finally {
2287
3785
  const endTime = Date.now();
2288
- this._reportToWorld(world, {
3786
+ _reportToWorld(world, {
2289
3787
  element_name: selectors.element_name,
2290
3788
  type: Types.GET_TABLE_DATA,
2291
3789
  text: "Get table data",
@@ -2340,7 +3838,7 @@ class StableBrowser {
2340
3838
  info.operation = "analyzeTable";
2341
3839
  info.selectors = selectors;
2342
3840
  info.query = query;
2343
- query = this._fixUsingParams(query, _params);
3841
+ query = _fixUsingParams(query, _params);
2344
3842
  info.query_fixed = query;
2345
3843
  info.operator = operator;
2346
3844
  info.value = value;
@@ -2446,11 +3944,12 @@ class StableBrowser {
2446
3944
  info.screenshotPath = screenshotPath;
2447
3945
  Object.assign(e, { info: info });
2448
3946
  error = e;
2449
- throw e;
3947
+ // throw e;
3948
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2450
3949
  }
2451
3950
  finally {
2452
3951
  const endTime = Date.now();
2453
- this._reportToWorld(world, {
3952
+ _reportToWorld(world, {
2454
3953
  element_name: selectors.element_name,
2455
3954
  type: Types.ANALYZE_TABLE,
2456
3955
  text: "Analyze table",
@@ -2471,8 +3970,51 @@ class StableBrowser {
2471
3970
  });
2472
3971
  }
2473
3972
  }
3973
+ /**
3974
+ * Explicit wait/sleep function that pauses execution for a specified duration
3975
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3976
+ * @param options - Optional configuration object
3977
+ * @param world - Optional world context
3978
+ * @returns Promise that resolves after the specified duration
3979
+ */
3980
+ async sleep(duration = 1000, options = {}, world = null) {
3981
+ const state = {
3982
+ duration,
3983
+ options,
3984
+ world,
3985
+ locate: false,
3986
+ scroll: false,
3987
+ screenshot: false,
3988
+ highlight: false,
3989
+ type: Types.SLEEP,
3990
+ text: `Sleep for ${duration} ms`,
3991
+ _text: `Sleep for ${duration} ms`,
3992
+ operation: "sleep",
3993
+ log: `***** Sleep for ${duration} ms *****\n`,
3994
+ };
3995
+ try {
3996
+ await _preCommand(state, this);
3997
+ if (duration < 0) {
3998
+ throw new Error("Sleep duration cannot be negative");
3999
+ }
4000
+ await new Promise((resolve) => setTimeout(resolve, duration));
4001
+ return state.info;
4002
+ }
4003
+ catch (e) {
4004
+ await _commandError(state, e, this);
4005
+ }
4006
+ finally {
4007
+ await _commandFinally(state, this);
4008
+ }
4009
+ }
2474
4010
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2475
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
4011
+ try {
4012
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
4013
+ }
4014
+ catch (error) {
4015
+ this.logger.debug(error);
4016
+ throw error;
4017
+ }
2476
4018
  }
2477
4019
  _getLoadTimeout(options) {
2478
4020
  let timeout = 15000;
@@ -2484,7 +4026,54 @@ class StableBrowser {
2484
4026
  }
2485
4027
  return timeout;
2486
4028
  }
4029
+ _getFindElementTimeout(options) {
4030
+ if (options && options.timeout) {
4031
+ return options.timeout;
4032
+ }
4033
+ if (this.configuration.find_element_timeout) {
4034
+ return this.configuration.find_element_timeout;
4035
+ }
4036
+ return 30000;
4037
+ }
4038
+ async saveStoreState(path = null, world = null) {
4039
+ const storageState = await this.page.context().storageState();
4040
+ path = await this._replaceWithLocalData(path, this.world);
4041
+ //const testDataFile = _getDataFile(world, this.context, this);
4042
+ if (path) {
4043
+ // save { storageState: storageState } into the path
4044
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
4045
+ }
4046
+ else {
4047
+ await this.setTestData({ storageState: storageState }, world);
4048
+ }
4049
+ }
4050
+ async restoreSaveState(path = null, world = null) {
4051
+ path = await this._replaceWithLocalData(path, this.world);
4052
+ await refreshBrowser(this, path, world);
4053
+ this.registerEventListeners(this.context);
4054
+ registerNetworkEvents(this.world, this, this.context, this.page);
4055
+ registerDownloadEvent(this.page, this.world, this.context);
4056
+ if (this.onRestoreSaveState) {
4057
+ this.onRestoreSaveState(path);
4058
+ }
4059
+ }
2487
4060
  async waitForPageLoad(options = {}, world = null) {
4061
+ // try {
4062
+ // let currentPagePath = null;
4063
+ // currentPagePath = new URL(this.page.url()).pathname;
4064
+ // if (this.latestPagePath) {
4065
+ // // get the currect page path and compare with the latest page path
4066
+ // if (this.latestPagePath === currentPagePath) {
4067
+ // // if the page path is the same, do not wait for page load
4068
+ // console.log("No page change: " + currentPagePath);
4069
+ // return;
4070
+ // }
4071
+ // }
4072
+ // this.latestPagePath = currentPagePath;
4073
+ // } catch (e) {
4074
+ // console.debug("Error getting current page path: ", e);
4075
+ // }
4076
+ //console.log("Waiting for page load");
2488
4077
  let timeout = this._getLoadTimeout(options);
2489
4078
  const promiseArray = [];
2490
4079
  // let waitForNetworkIdle = true;
@@ -2517,13 +4106,15 @@ class StableBrowser {
2517
4106
  else if (e.label === "domcontentloaded") {
2518
4107
  console.log("waited for the domcontent loaded timeout");
2519
4108
  }
2520
- console.log(".");
2521
4109
  }
2522
4110
  finally {
2523
- await new Promise((resolve) => setTimeout(resolve, 2000));
4111
+ await new Promise((resolve) => setTimeout(resolve, 500));
4112
+ if (options && !options.noSleep) {
4113
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4114
+ }
2524
4115
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2525
4116
  const endTime = Date.now();
2526
- this._reportToWorld(world, {
4117
+ _reportToWorld(world, {
2527
4118
  type: Types.GET_PAGE_STATUS,
2528
4119
  text: "Wait for page load",
2529
4120
  screenshotId,
@@ -2543,40 +4134,138 @@ class StableBrowser {
2543
4134
  }
2544
4135
  }
2545
4136
  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 = {};
4137
+ const state = {
4138
+ options,
4139
+ world,
4140
+ locate: false,
4141
+ scroll: false,
4142
+ highlight: false,
4143
+ type: Types.CLOSE_PAGE,
4144
+ text: `Close page`,
4145
+ _text: `Close the page`,
4146
+ operation: "closePage",
4147
+ log: "***** close page *****\n",
4148
+ throwError: false,
4149
+ };
2551
4150
  try {
4151
+ await _preCommand(state, this);
2552
4152
  await this.page.close();
2553
4153
  }
2554
4154
  catch (e) {
2555
- console.log(".");
4155
+ await _commandError(state, e, this);
2556
4156
  }
2557
4157
  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,
4158
+ await _commandFinally(state, this);
4159
+ }
4160
+ }
4161
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
4162
+ let operation = null;
4163
+ if (!options || !options.operation) {
4164
+ throw new Error("operation is not defined");
4165
+ }
4166
+ operation = options.operation;
4167
+ // validate operation is one of the supported operations
4168
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
4169
+ throw new Error("operation is not supported");
4170
+ }
4171
+ const state = {
4172
+ options,
4173
+ world,
4174
+ locate: false,
4175
+ scroll: false,
4176
+ highlight: false,
4177
+ type: Types.TABLE_OPERATION,
4178
+ text: `Table operation`,
4179
+ _text: `Table ${operation} operation`,
4180
+ operation: operation,
4181
+ log: "***** Table operation *****\n",
4182
+ };
4183
+ const timeout = this._getFindElementTimeout(options);
4184
+ try {
4185
+ await _preCommand(state, this);
4186
+ const start = Date.now();
4187
+ let cellArea = null;
4188
+ while (true) {
4189
+ try {
4190
+ cellArea = await _findCellArea(headerText, rowText, this, state);
4191
+ if (cellArea) {
4192
+ break;
2571
4193
  }
2572
- : {
2573
- status: "PASSED",
2574
- startTime,
2575
- endTime,
2576
- },
2577
- info: info,
2578
- });
4194
+ }
4195
+ catch (e) {
4196
+ // ignore
4197
+ }
4198
+ if (Date.now() - start > timeout) {
4199
+ throw new Error(`Cell not found in table`);
4200
+ }
4201
+ await new Promise((resolve) => setTimeout(resolve, 1000));
4202
+ }
4203
+ switch (operation) {
4204
+ case "click":
4205
+ if (!options.css) {
4206
+ // will click in the center of the cell
4207
+ let xOffset = 0;
4208
+ let yOffset = 0;
4209
+ if (options.xOffset) {
4210
+ xOffset = options.xOffset;
4211
+ }
4212
+ if (options.yOffset) {
4213
+ yOffset = options.yOffset;
4214
+ }
4215
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
4216
+ }
4217
+ else {
4218
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4219
+ if (results.length === 0) {
4220
+ throw new Error(`Element not found in cell area`);
4221
+ }
4222
+ state.element = results[0];
4223
+ await performAction("click", state.element, options, this, state, _params);
4224
+ }
4225
+ break;
4226
+ case "hover+click":
4227
+ if (!options.css) {
4228
+ throw new Error("css is not defined");
4229
+ }
4230
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4231
+ if (results.length === 0) {
4232
+ throw new Error(`Element not found in cell area`);
4233
+ }
4234
+ state.element = results[0];
4235
+ await performAction("hover+click", state.element, options, this, state, _params);
4236
+ break;
4237
+ case "hover":
4238
+ if (!options.css) {
4239
+ throw new Error("css is not defined");
4240
+ }
4241
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4242
+ if (result1.length === 0) {
4243
+ throw new Error(`Element not found in cell area`);
4244
+ }
4245
+ state.element = result1[0];
4246
+ await performAction("hover", state.element, options, this, state, _params);
4247
+ break;
4248
+ default:
4249
+ throw new Error("operation is not supported");
4250
+ }
4251
+ }
4252
+ catch (e) {
4253
+ await _commandError(state, e, this);
4254
+ }
4255
+ finally {
4256
+ await _commandFinally(state, this);
4257
+ }
4258
+ }
4259
+ saveTestDataAsGlobal(options, world) {
4260
+ const dataFile = _getDataFile(world, this.context, this);
4261
+ if (process.env.MODE === "executions") {
4262
+ const globalDataFile = path.join(this.project_path, "global_test_data.json");
4263
+ fs.copyFileSync(dataFile, globalDataFile);
4264
+ this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
4265
+ return;
2579
4266
  }
4267
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
4268
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
2580
4269
  }
2581
4270
  async setViewportSize(width, hight, options = {}, world = null) {
2582
4271
  const startTime = Date.now();
@@ -2594,15 +4283,16 @@ class StableBrowser {
2594
4283
  await this.page.setViewportSize({ width: width, height: hight });
2595
4284
  }
2596
4285
  catch (e) {
2597
- console.log(".");
4286
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2598
4287
  }
2599
4288
  finally {
2600
4289
  await new Promise((resolve) => setTimeout(resolve, 2000));
2601
4290
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2602
4291
  const endTime = Date.now();
2603
- this._reportToWorld(world, {
4292
+ _reportToWorld(world, {
2604
4293
  type: Types.SET_VIEWPORT,
2605
4294
  text: "set viewport size to " + width + "x" + hight,
4295
+ _text: "Set the viewport size to " + width + "x" + hight,
2606
4296
  screenshotId,
2607
4297
  result: error
2608
4298
  ? {
@@ -2630,13 +4320,13 @@ class StableBrowser {
2630
4320
  await this.page.reload();
2631
4321
  }
2632
4322
  catch (e) {
2633
- console.log(".");
4323
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2634
4324
  }
2635
4325
  finally {
2636
4326
  await new Promise((resolve) => setTimeout(resolve, 2000));
2637
4327
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2638
4328
  const endTime = Date.now();
2639
- this._reportToWorld(world, {
4329
+ _reportToWorld(world, {
2640
4330
  type: Types.GET_PAGE_STATUS,
2641
4331
  text: "page relaod",
2642
4332
  screenshotId,
@@ -2672,11 +4362,254 @@ class StableBrowser {
2672
4362
  console.log("#-#");
2673
4363
  }
2674
4364
  }
2675
- _reportToWorld(world, properties) {
2676
- if (!world || !world.attach) {
2677
- return;
4365
+ async beforeScenario(world, scenario) {
4366
+ if (world && world.attach) {
4367
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4368
+ }
4369
+ this.context.loadedRoutes = null;
4370
+ this.beforeScenarioCalled = true;
4371
+ if (scenario && scenario.pickle && scenario.pickle.name) {
4372
+ this.scenarioName = scenario.pickle.name;
4373
+ }
4374
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
4375
+ this.featureName = scenario.gherkinDocument.feature.name;
4376
+ }
4377
+ if (this.context) {
4378
+ this.context.examplesRow = extractStepExampleParameters(scenario);
4379
+ }
4380
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
4381
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
4382
+ // check if @global_test_data tag is present
4383
+ if (this.tags.includes("@global_test_data")) {
4384
+ this.saveTestDataAsGlobal({}, world);
4385
+ }
4386
+ }
4387
+ // update test data based on feature/scenario
4388
+ let envName = null;
4389
+ if (this.context && this.context.environment) {
4390
+ envName = this.context.environment.name;
4391
+ }
4392
+ if (!process.env.TEMP_RUN) {
4393
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
4394
+ }
4395
+ await loadBrunoParams(this.context, this.context.environment.name);
4396
+ if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
4397
+ this.trace = true;
4398
+ const traceFolder = path.join(this.context.reportFolder, "trace");
4399
+ if (!fs.existsSync(traceFolder)) {
4400
+ fs.mkdirSync(traceFolder, { recursive: true });
4401
+ }
4402
+ this.traceFolder = traceFolder;
4403
+ await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
4404
+ }
4405
+ }
4406
+ async afterScenario(world, scenario) {
4407
+ const id = scenario.testCaseStartedId;
4408
+ if (this.trace) {
4409
+ await this.context.playContext.tracing.stop({
4410
+ path: path.join(this.traceFolder, `trace-${id}.zip`),
4411
+ });
4412
+ }
4413
+ }
4414
+ async beforeStep(world, step) {
4415
+ if (step?.pickleStep && this.trace) {
4416
+ await this.context.playContext.tracing.group(`Step: ${step.pickleStep.text}`);
4417
+ }
4418
+ this.stepTags = [];
4419
+ if (!this.beforeScenarioCalled) {
4420
+ this.beforeScenario(world, step);
4421
+ this.context.loadedRoutes = null;
4422
+ }
4423
+ if (this.stepIndex === undefined) {
4424
+ this.stepIndex = 0;
4425
+ }
4426
+ else {
4427
+ this.stepIndex++;
4428
+ }
4429
+ if (step && step.pickleStep && step.pickleStep.text) {
4430
+ this.stepName = step.pickleStep.text;
4431
+ let printableStepName = this.stepName;
4432
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4433
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4434
+ return `\x1b[33m"${p1}"\x1b[0m`;
4435
+ });
4436
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4437
+ }
4438
+ else if (step && step.text) {
4439
+ this.stepName = step.text;
4440
+ }
4441
+ else {
4442
+ this.stepName = "step " + this.stepIndex;
4443
+ }
4444
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4445
+ if (this.context.browserObject.context) {
4446
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
4447
+ }
4448
+ }
4449
+ if (this.initSnapshotTaken === false) {
4450
+ this.initSnapshotTaken = true;
4451
+ if (world &&
4452
+ world.attach &&
4453
+ !process.env.DISABLE_SNAPSHOT &&
4454
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4455
+ const snapshot = await this.getAriaSnapshot();
4456
+ if (snapshot) {
4457
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
4458
+ }
4459
+ }
4460
+ }
4461
+ this.context.routeResults = null;
4462
+ this.context.loadedRoutes = null;
4463
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4464
+ networkBeforeStep(this.stepName, this.context);
4465
+ this.inStepReport = false;
4466
+ }
4467
+ setStepTags(tags) {
4468
+ this.stepTags = tags;
4469
+ }
4470
+ async getAriaSnapshot() {
4471
+ try {
4472
+ // find the page url
4473
+ const url = await this.page.url();
4474
+ // extract the path from the url
4475
+ const path = new URL(url).pathname;
4476
+ // get the page title
4477
+ const title = await this.page.title();
4478
+ // go over other frams
4479
+ const frames = this.page.frames();
4480
+ const snapshots = [];
4481
+ const content = [`- path: ${path}`, `- title: ${title}`];
4482
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
4483
+ for (let i = 0; i < frames.length; i++) {
4484
+ const frame = frames[i];
4485
+ try {
4486
+ // Ensure frame is attached and has body
4487
+ const body = frame.locator("body");
4488
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4489
+ const snapshot = await body.ariaSnapshot({ timeout });
4490
+ if (!snapshot) {
4491
+ continue;
4492
+ }
4493
+ content.push(`- frame: ${i}`);
4494
+ content.push(snapshot);
4495
+ }
4496
+ catch (innerErr) {
4497
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4498
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4499
+ }
4500
+ }
4501
+ return content.join("\n");
4502
+ }
4503
+ catch (e) {
4504
+ console.log("Error in getAriaSnapshot");
4505
+ //console.debug(e);
4506
+ }
4507
+ return null;
4508
+ }
4509
+ /**
4510
+ * Sends command with custom payload to report.
4511
+ * @param commandText - Title of the command to be shown in the report.
4512
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
4513
+ * @param content - Content of the command to be shown in the report.
4514
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
4515
+ * @param world - Optional world context.
4516
+ * @public
4517
+ */
4518
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
4519
+ const state = {
4520
+ options,
4521
+ world,
4522
+ locate: false,
4523
+ scroll: false,
4524
+ screenshot: options.screenshot ?? false,
4525
+ highlight: options.highlight ?? false,
4526
+ type: Types.REPORT_COMMAND,
4527
+ text: commandText,
4528
+ _text: commandText,
4529
+ operation: "report_command",
4530
+ log: "***** " + commandText + " *****\n",
4531
+ };
4532
+ try {
4533
+ await _preCommand(state, this);
4534
+ const payload = {
4535
+ type: options.type ?? "text",
4536
+ content: content,
4537
+ screenshotId: null,
4538
+ };
4539
+ state.payload = payload;
4540
+ if (commandStatus === "FAILED") {
4541
+ state.throwError = true;
4542
+ throw new Error(commandText);
4543
+ }
4544
+ }
4545
+ catch (e) {
4546
+ await _commandError(state, e, this);
4547
+ }
4548
+ finally {
4549
+ await _commandFinally(state, this);
4550
+ }
4551
+ }
4552
+ async afterStep(world, step, result) {
4553
+ this.stepName = null;
4554
+ if (this.context) {
4555
+ this.context.examplesRow = null;
4556
+ }
4557
+ if (!this.inStepReport) {
4558
+ // check the step result
4559
+ if (result && result.status === "FAILED" && world && world.attach) {
4560
+ await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
4561
+ }
4562
+ }
4563
+ if (world &&
4564
+ world.attach &&
4565
+ !process.env.DISABLE_SNAPSHOT &&
4566
+ !this.fastMode &&
4567
+ !this.stepTags.includes("fast-mode")) {
4568
+ const snapshot = await this.getAriaSnapshot();
4569
+ if (snapshot) {
4570
+ const obj = {};
4571
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4572
+ }
4573
+ }
4574
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4575
+ if (this.context.routeResults) {
4576
+ if (world && world.attach) {
4577
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4578
+ }
4579
+ }
4580
+ if (!process.env.TEMP_RUN) {
4581
+ const state = {
4582
+ world,
4583
+ locate: false,
4584
+ scroll: false,
4585
+ screenshot: true,
4586
+ highlight: true,
4587
+ type: Types.STEP_COMPLETE,
4588
+ text: "end of scenario",
4589
+ _text: "end of scenario",
4590
+ operation: "step_complete",
4591
+ log: "***** " + "end of scenario" + " *****\n",
4592
+ };
4593
+ try {
4594
+ await _preCommand(state, this);
4595
+ }
4596
+ catch (e) {
4597
+ await _commandError(state, e, this);
4598
+ }
4599
+ finally {
4600
+ await _commandFinally(state, this);
4601
+ }
4602
+ }
4603
+ networkAfterStep(this.stepName, this.context);
4604
+ if (process.env.TEMP_RUN === "true") {
4605
+ // Put a sleep for some time to allow the browser to finish processing
4606
+ if (!this.stepTags.includes("fast-mode")) {
4607
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4608
+ }
4609
+ }
4610
+ if (this.trace) {
4611
+ await this.context.playContext.tracing.groupEnd();
2678
4612
  }
2679
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2680
4613
  }
2681
4614
  }
2682
4615
  function createTimedPromise(promise, label) {
@@ -2684,156 +4617,5 @@ function createTimedPromise(promise, label) {
2684
4617
  .then((result) => ({ status: "fulfilled", label, result }))
2685
4618
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2686
4619
  }
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
4620
  export { StableBrowser };
2839
4621
  //# sourceMappingURL=stable_browser.js.map