automation_model 1.0.511-dev → 1.0.511

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