automation_model 1.0.516-dev → 1.0.516

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