automation_model 1.0.506-dev → 1.0.506

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