automation_model 1.0.515-dev → 1.0.515

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +133 -0
  2. package/lib/analyze_helper.js.map +1 -1
  3. package/lib/api.d.ts +2 -2
  4. package/lib/api.js +162 -121
  5. package/lib/api.js.map +1 -1
  6. package/lib/auto_page.d.ts +7 -2
  7. package/lib/auto_page.js +308 -20
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.d.ts +3 -2
  10. package/lib/browser_manager.js +239 -59
  11. package/lib/browser_manager.js.map +1 -1
  12. package/lib/bruno.d.ts +2 -0
  13. package/lib/bruno.js +381 -0
  14. package/lib/bruno.js.map +1 -0
  15. package/lib/check_performance.d.ts +1 -0
  16. package/lib/check_performance.js +57 -0
  17. package/lib/check_performance.js.map +1 -0
  18. package/lib/command_common.d.ts +5 -4
  19. package/lib/command_common.js +122 -21
  20. package/lib/command_common.js.map +1 -1
  21. package/lib/constants.d.ts +4 -0
  22. package/lib/constants.js +2 -0
  23. package/lib/constants.js.map +1 -0
  24. package/lib/date_time.js.map +1 -1
  25. package/lib/drawRect.js.map +1 -1
  26. package/lib/environment.d.ts +1 -0
  27. package/lib/environment.js +1 -0
  28. package/lib/environment.js.map +1 -1
  29. package/lib/error-messages.js +26 -2
  30. package/lib/error-messages.js.map +1 -1
  31. package/lib/file_checker.d.ts +1 -0
  32. package/lib/file_checker.js +172 -0
  33. package/lib/file_checker.js.map +1 -0
  34. package/lib/find_function.js.map +1 -1
  35. package/lib/generation_scripts.d.ts +4 -0
  36. package/lib/generation_scripts.js +2 -0
  37. package/lib/generation_scripts.js.map +1 -0
  38. package/lib/index.d.ts +3 -0
  39. package/lib/index.js +4 -0
  40. package/lib/index.js.map +1 -1
  41. package/lib/init_browser.d.ts +4 -3
  42. package/lib/init_browser.js +173 -99
  43. package/lib/init_browser.js.map +1 -1
  44. package/lib/locate_element.js +16 -14
  45. package/lib/locate_element.js.map +1 -1
  46. package/lib/locator.d.ts +37 -0
  47. package/lib/locator.js +172 -0
  48. package/lib/locator.js.map +1 -1
  49. package/lib/locator_log.d.ts +26 -0
  50. package/lib/locator_log.js +69 -0
  51. package/lib/locator_log.js.map +1 -0
  52. package/lib/network.d.ts +3 -1
  53. package/lib/network.js +418 -68
  54. package/lib/network.js.map +1 -1
  55. package/lib/route.d.ts +83 -0
  56. package/lib/route.js +695 -0
  57. package/lib/route.js.map +1 -0
  58. package/lib/scripts/axe.mini.js +23989 -0
  59. package/lib/snapshot_validation.d.ts +37 -0
  60. package/lib/snapshot_validation.js +360 -0
  61. package/lib/snapshot_validation.js.map +1 -0
  62. package/lib/stable_browser.d.ts +140 -42
  63. package/lib/stable_browser.js +2431 -676
  64. package/lib/stable_browser.js.map +1 -1
  65. package/lib/table.d.ts +15 -0
  66. package/lib/table.js +257 -0
  67. package/lib/table.js.map +1 -0
  68. package/lib/table_analyze.js.map +1 -1
  69. package/lib/table_helper.d.ts +19 -0
  70. package/lib/table_helper.js +130 -0
  71. package/lib/table_helper.js.map +1 -0
  72. package/lib/test_context.d.ts +5 -0
  73. package/lib/test_context.js +4 -0
  74. package/lib/test_context.js.map +1 -1
  75. package/lib/utils.d.ts +38 -3
  76. package/lib/utils.js +778 -35
  77. package/lib/utils.js.map +1 -1
  78. package/package.json +32 -13
  79. package/lib/axe/axe.mini.js +0 -12
@@ -1,4 +1,5 @@
1
1
  // @ts-nocheck
2
+ import { check_performance } from "./check_performance.js";
2
3
  import { expect } from "@playwright/test";
3
4
  import dayjs from "dayjs";
4
5
  import fs from "fs";
@@ -10,60 +11,103 @@ import { getDateTimeValue } from "./date_time.js";
10
11
  import drawRectangle from "./drawRect.js";
11
12
  //import { closeUnexpectedPopups } from "./popups.js";
12
13
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { maskValue, replaceWithLocalTestData } from "./utils.js";
14
+ import errorStackParser from "error-stack-parser";
15
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
14
16
  import csv from "csv-parser";
15
17
  import { Readable } from "node:stream";
16
18
  import readline from "readline";
17
- import { getContext } from "./init_browser.js";
19
+ import { getContext, refreshBrowser } from "./init_browser.js";
20
+ import { getTestData } from "./auto_page.js";
18
21
  import { locate_element } from "./locate_element.js";
19
- import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot } from "./command_common.js";
20
- import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
21
- const Types = {
22
+ import { randomUUID } from "crypto";
23
+ import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
24
+ import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
25
+ import { LocatorLog } from "./locator_log.js";
26
+ import axios from "axios";
27
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
28
+ import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
29
+ import { loadBrunoParams } from "./bruno.js";
30
+ import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
31
+ import { existsSync } from "node:fs";
32
+ import _ from "lodash";
33
+ export const Types = {
22
34
  CLICK: "click_element",
35
+ WAIT_ELEMENT: "wait_element",
23
36
  NAVIGATE: "navigate",
37
+ GO_BACK: "go_back",
38
+ GO_FORWARD: "go_forward",
24
39
  FILL: "fill_element",
25
- EXECUTE: "execute_page_method",
26
- OPEN: "open_environment",
40
+ EXECUTE: "execute_page_method", //
41
+ OPEN: "open_environment", //
27
42
  COMPLETE: "step_complete",
28
43
  ASK: "information_needed",
29
- GET_PAGE_STATUS: "get_page_status",
30
- CLICK_ROW_ACTION: "click_row_action",
44
+ GET_PAGE_STATUS: "get_page_status", ///
45
+ CLICK_ROW_ACTION: "click_row_action", //
31
46
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
47
+ VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
48
+ VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
32
49
  ANALYZE_TABLE: "analyze_table",
33
- SELECT: "select_combobox",
50
+ SELECT: "select_combobox", //
51
+ VERIFY_PROPERTY: "verify_element_property",
34
52
  VERIFY_PAGE_PATH: "verify_page_path",
53
+ VERIFY_PAGE_TITLE: "verify_page_title",
35
54
  TYPE_PRESS: "type_press",
36
55
  PRESS: "press_key",
37
56
  HOVER: "hover_element",
38
57
  CHECK: "check_element",
39
58
  UNCHECK: "uncheck_element",
40
59
  EXTRACT: "extract_attribute",
60
+ EXTRACT_PROPERTY: "extract_property",
41
61
  CLOSE_PAGE: "close_page",
62
+ TABLE_OPERATION: "table_operation",
42
63
  SET_DATE_TIME: "set_date_time",
43
64
  SET_VIEWPORT: "set_viewport",
44
65
  VERIFY_VISUAL: "verify_visual",
45
66
  LOAD_DATA: "load_data",
46
67
  SET_INPUT: "set_input",
47
68
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
69
+ VERIFY_ATTRIBUTE: "verify_element_attribute",
70
+ VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
71
+ BRUNO: "bruno",
72
+ VERIFY_FILE_EXISTS: "verify_file_exists",
73
+ SET_INPUT_FILES: "set_input_files",
74
+ SNAPSHOT_VALIDATION: "snapshot_validation",
75
+ REPORT_COMMAND: "report_command",
76
+ STEP_COMPLETE: "step_complete",
77
+ SLEEP: "sleep",
78
+ CONDITIONAL_WAIT: "conditional_wait",
48
79
  };
49
80
  export const apps = {};
81
+ const formatElementName = (elementName) => {
82
+ return elementName ? JSON.stringify(elementName) : "element";
83
+ };
50
84
  class StableBrowser {
51
85
  browser;
52
86
  page;
53
87
  logger;
54
88
  context;
55
89
  world;
90
+ fastMode;
91
+ stepTags;
56
92
  project_path = null;
57
93
  webLogFile = null;
58
94
  networkLogger = null;
59
95
  configuration = null;
60
96
  appName = "main";
61
- constructor(browser, page, logger = null, context = null, world = null) {
97
+ tags = null;
98
+ isRecording = false;
99
+ initSnapshotTaken = false;
100
+ onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
101
+ // set to true if the step issue a report
102
+ inStepReport = false;
103
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
62
104
  this.browser = browser;
63
105
  this.page = page;
64
106
  this.logger = logger;
65
107
  this.context = context;
66
108
  this.world = world;
109
+ this.fastMode = fastMode;
110
+ this.stepTags = stepTags;
67
111
  if (!this.logger) {
68
112
  this.logger = console;
69
113
  }
@@ -92,16 +136,32 @@ class StableBrowser {
92
136
  context.pages = [this.page];
93
137
  const logFolder = path.join(this.project_path, "logs", "web");
94
138
  this.world = world;
139
+ if (this.configuration && this.configuration.fastMode === true) {
140
+ this.fastMode = true;
141
+ }
142
+ if (process.env.FAST_MODE === "true") {
143
+ // console.log("Fast mode enabled from environment variable");
144
+ this.fastMode = true;
145
+ }
146
+ if (process.env.FAST_MODE === "false") {
147
+ this.fastMode = false;
148
+ }
149
+ if (this.context) {
150
+ this.context.fastMode = this.fastMode;
151
+ }
95
152
  this.registerEventListeners(this.context);
96
153
  registerNetworkEvents(this.world, this, this.context, this.page);
97
154
  registerDownloadEvent(this.page, this.world, this.context);
98
155
  }
99
156
  registerEventListeners(context) {
100
157
  this.registerConsoleLogListener(this.page, context);
101
- this.registerRequestListener(this.page, context, this.webLogFile);
158
+ // this.registerRequestListener(this.page, context, this.webLogFile);
102
159
  if (!context.pageLoading) {
103
160
  context.pageLoading = { status: false };
104
161
  }
162
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
163
+ this.page.on("dialog", (dialog) => dialog.accept());
164
+ }
105
165
  context.playContext.on("page", async function (page) {
106
166
  if (this.configuration && this.configuration.closePopups === true) {
107
167
  console.log("close unexpected popups");
@@ -110,11 +170,20 @@ class StableBrowser {
110
170
  }
111
171
  context.pageLoading.status = true;
112
172
  this.page = page;
173
+ try {
174
+ if (this.configuration && this.configuration.acceptDialog) {
175
+ await page.on("dialog", (dialog) => dialog.accept());
176
+ }
177
+ }
178
+ catch (error) {
179
+ console.error("Error on dialog accept registration", error);
180
+ }
113
181
  context.page = page;
114
182
  context.pages.push(page);
115
183
  registerNetworkEvents(this.world, this, context, this.page);
116
184
  registerDownloadEvent(this.page, this.world, context);
117
185
  page.on("close", async () => {
186
+ // return if browser context is already closed
118
187
  if (this.context && this.context.pages && this.context.pages.length > 1) {
119
188
  this.context.pages.pop();
120
189
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -124,7 +193,12 @@ class StableBrowser {
124
193
  console.log("Switched to page " + title);
125
194
  }
126
195
  catch (error) {
127
- console.error("Error on page close", error);
196
+ if (error?.message?.includes("Target page, context or browser has been closed")) {
197
+ // Ignore this error
198
+ }
199
+ else {
200
+ console.error("Error on page close", error);
201
+ }
128
202
  }
129
203
  }
130
204
  });
@@ -133,7 +207,12 @@ class StableBrowser {
133
207
  console.log("Switch page: " + (await page.title()));
134
208
  }
135
209
  catch (e) {
136
- this.logger.error("error on page load " + e);
210
+ if (e?.message?.includes("Target page, context or browser has been closed")) {
211
+ // Ignore this error
212
+ }
213
+ else {
214
+ this.logger.error("error on page load " + e);
215
+ }
137
216
  }
138
217
  context.pageLoading.status = false;
139
218
  }.bind(this));
@@ -145,7 +224,7 @@ class StableBrowser {
145
224
  }
146
225
  let newContextCreated = false;
147
226
  if (!apps[appName]) {
148
- let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
227
+ let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder, null, null, this.tags);
149
228
  newContextCreated = true;
150
229
  apps[appName] = {
151
230
  context: newContext,
@@ -154,31 +233,41 @@ class StableBrowser {
154
233
  };
155
234
  }
156
235
  const tempContext = {};
157
- this._copyContext(this, tempContext);
158
- this._copyContext(apps[appName], this);
236
+ _copyContext(this, tempContext);
237
+ _copyContext(apps[appName], this);
159
238
  apps[this.appName] = tempContext;
160
239
  this.appName = appName;
161
240
  if (newContextCreated) {
162
241
  this.registerEventListeners(this.context);
163
242
  await this.goto(this.context.environment.baseUrl);
164
- await this.waitForPageLoad();
243
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
244
+ await this.waitForPageLoad();
245
+ }
165
246
  }
166
247
  }
167
- _copyContext(from, to) {
168
- to.browser = from.browser;
169
- to.page = from.page;
170
- to.context = from.context;
171
- }
172
- getWebLogFile(logFolder) {
173
- if (!fs.existsSync(logFolder)) {
174
- fs.mkdirSync(logFolder, { recursive: true });
248
+ async switchTab(tabTitleOrIndex) {
249
+ // first check if the tabNameOrIndex is a number
250
+ let index = parseInt(tabTitleOrIndex);
251
+ if (!isNaN(index)) {
252
+ if (index >= 0 && index < this.context.pages.length) {
253
+ this.page = this.context.pages[index];
254
+ this.context.page = this.page;
255
+ await this.page.bringToFront();
256
+ return;
257
+ }
175
258
  }
176
- let nextIndex = 1;
177
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
178
- nextIndex++;
259
+ // if the tabNameOrIndex is a string, find the tab by name
260
+ for (let i = 0; i < this.context.pages.length; i++) {
261
+ let page = this.context.pages[i];
262
+ let title = await page.title();
263
+ if (title.includes(tabTitleOrIndex)) {
264
+ this.page = page;
265
+ this.context.page = this.page;
266
+ await this.page.bringToFront();
267
+ return;
268
+ }
179
269
  }
180
- const fileName = nextIndex + ".json";
181
- return path.join(logFolder, fileName);
270
+ throw new Error("Tab not found: " + tabTitleOrIndex);
182
271
  }
183
272
  registerConsoleLogListener(page, context) {
184
273
  if (!this.context.webLogger) {
@@ -229,7 +318,7 @@ class StableBrowser {
229
318
  this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
230
319
  }
231
320
  catch (error) {
232
- console.error("Error in request listener", error);
321
+ // console.error("Error in request listener", error);
233
322
  context.networkLogger.push({
234
323
  error: "not able to listen",
235
324
  message: error.message,
@@ -243,55 +332,117 @@ class StableBrowser {
243
332
  // async closeUnexpectedPopups() {
244
333
  // await closeUnexpectedPopups(this.page);
245
334
  // }
246
- async goto(url) {
335
+ async goto(url, world = null, options = {}) {
336
+ if (!url) {
337
+ throw new Error("url is null, verify that the environment file is correct");
338
+ }
339
+ url = await this._replaceWithLocalData(url, this.world);
247
340
  if (!url.startsWith("http")) {
248
341
  url = "https://" + url;
249
342
  }
250
- await this.page.goto(url, {
251
- timeout: 60000,
252
- });
253
- }
254
- _fixUsingParams(text, _params) {
255
- if (!_params || typeof text !== "string") {
256
- return text;
343
+ const state = {
344
+ value: url,
345
+ world: world,
346
+ type: Types.NAVIGATE,
347
+ text: `Navigate Page to: ${url}`,
348
+ operation: "goto",
349
+ log: "***** navigate page to " + url + " *****\n",
350
+ info: {},
351
+ locate: false,
352
+ scroll: false,
353
+ screenshot: false,
354
+ highlight: false,
355
+ };
356
+ let timeout = 60000;
357
+ if (this.configuration && this.configuration.page_timeout) {
358
+ timeout = this.configuration.page_timeout;
257
359
  }
258
- for (let key in _params) {
259
- let regValue = key;
260
- if (key.startsWith("_")) {
261
- // remove the _ prefix
262
- regValue = key.substring(1);
263
- }
264
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
360
+ if (options && options["timeout"]) {
361
+ timeout = options["timeout"];
362
+ }
363
+ try {
364
+ await _preCommand(state, this);
365
+ await this.page.goto(url, {
366
+ timeout: timeout,
367
+ });
368
+ await _screenshot(state, this);
369
+ }
370
+ catch (error) {
371
+ console.error("Error on goto", error);
372
+ await _commandError(state, error, this);
373
+ }
374
+ finally {
375
+ await _commandFinally(state, this);
265
376
  }
266
- return text;
267
377
  }
268
- _fixLocatorUsingParams(locator, _params) {
269
- // check if not null
270
- if (!locator) {
271
- return locator;
378
+ async goBack(options, world = null) {
379
+ const state = {
380
+ value: "",
381
+ world: world,
382
+ type: Types.GO_BACK,
383
+ text: `Browser navigate back`,
384
+ operation: "goBack",
385
+ log: "***** navigate back *****\n",
386
+ info: {},
387
+ locate: false,
388
+ scroll: false,
389
+ screenshot: false,
390
+ highlight: false,
391
+ };
392
+ try {
393
+ await _preCommand(state, this);
394
+ await this.page.goBack({
395
+ waitUntil: "load",
396
+ });
397
+ await _screenshot(state, this);
398
+ }
399
+ catch (error) {
400
+ console.error("Error on goBack", error);
401
+ await _commandError(state, error, this);
402
+ }
403
+ finally {
404
+ await _commandFinally(state, this);
272
405
  }
273
- // clone the locator
274
- locator = JSON.parse(JSON.stringify(locator));
275
- this.scanAndManipulate(locator, _params);
276
- return locator;
277
406
  }
278
- _isObject(value) {
279
- return value && typeof value === "object" && value.constructor === Object;
407
+ async goForward(options, world = null) {
408
+ const state = {
409
+ value: "",
410
+ world: world,
411
+ type: Types.GO_FORWARD,
412
+ text: `Browser navigate forward`,
413
+ operation: "goForward",
414
+ log: "***** navigate forward *****\n",
415
+ info: {},
416
+ locate: false,
417
+ scroll: false,
418
+ screenshot: false,
419
+ highlight: false,
420
+ };
421
+ try {
422
+ await _preCommand(state, this);
423
+ await this.page.goForward({
424
+ waitUntil: "load",
425
+ });
426
+ await _screenshot(state, this);
427
+ }
428
+ catch (error) {
429
+ console.error("Error on goForward", error);
430
+ await _commandError(state, error, this);
431
+ }
432
+ finally {
433
+ await _commandFinally(state, this);
434
+ }
280
435
  }
281
- scanAndManipulate(currentObj, _params) {
282
- for (const key in currentObj) {
283
- if (typeof currentObj[key] === "string") {
284
- // Perform string manipulation
285
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
286
- }
287
- else if (this._isObject(currentObj[key])) {
288
- // Recursively scan nested objects
289
- this.scanAndManipulate(currentObj[key], _params);
436
+ async _getLocator(locator, scope, _params) {
437
+ locator = _fixLocatorUsingParams(locator, _params);
438
+ // locator = await this._replaceWithLocalData(locator);
439
+ for (let key in locator) {
440
+ if (typeof locator[key] !== "string")
441
+ continue;
442
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
443
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
290
444
  }
291
445
  }
292
- }
293
- _getLocator(locator, scope, _params) {
294
- locator = this._fixLocatorUsingParams(locator, _params);
295
446
  let locatorReturn;
296
447
  if (locator.role) {
297
448
  if (locator.role[1].nameReg) {
@@ -299,7 +450,7 @@ class StableBrowser {
299
450
  delete locator.role[1].nameReg;
300
451
  }
301
452
  // if (locator.role[1].name) {
302
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
453
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
303
454
  // }
304
455
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
305
456
  }
@@ -342,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
+ await _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
+ await _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,37 +1442,29 @@ 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) {
1025
- if (!values) {
1026
- throw new Error("values is null");
1027
- }
1464
+ // commented out this condition as some select elements have NULL values by default.
1465
+ // if (!values) {
1466
+ // throw new Error("values is null");
1467
+ // }
1028
1468
  const state = {
1029
1469
  selectors,
1030
1470
  _params,
@@ -1033,6 +1473,7 @@ class StableBrowser {
1033
1473
  value: values.toString(),
1034
1474
  type: Types.SELECT,
1035
1475
  text: `Select option: ${values}`,
1476
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1036
1477
  operation: "selectOption",
1037
1478
  log: "***** select option " + selectors.element_name + " *****\n",
1038
1479
  };
@@ -1046,14 +1487,14 @@ class StableBrowser {
1046
1487
  state.info.log += "selectOption failed, will try force" + "\n";
1047
1488
  await state.element.selectOption(values, { timeout: 10000, force: true });
1048
1489
  }
1049
- await this.waitForPageLoad();
1490
+ //await this.waitForPageLoad();
1050
1491
  return state.info;
1051
1492
  }
1052
1493
  catch (e) {
1053
1494
  await _commandError(state, e, this);
1054
1495
  }
1055
1496
  finally {
1056
- _commandFinally(state, this);
1497
+ await _commandFinally(state, this);
1057
1498
  }
1058
1499
  }
1059
1500
  async type(_value, _params = null, options = {}, world = null) {
@@ -1067,6 +1508,7 @@ class StableBrowser {
1067
1508
  highlight: false,
1068
1509
  type: Types.TYPE_PRESS,
1069
1510
  text: `Type value: ${_value}`,
1511
+ _text: `Type value: ${_value}`,
1070
1512
  operation: "type",
1071
1513
  log: "",
1072
1514
  };
@@ -1098,7 +1540,7 @@ class StableBrowser {
1098
1540
  await _commandError(state, e, this);
1099
1541
  }
1100
1542
  finally {
1101
- _commandFinally(state, this);
1543
+ await _commandFinally(state, this);
1102
1544
  }
1103
1545
  }
1104
1546
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1134,7 +1576,7 @@ class StableBrowser {
1134
1576
  await _commandError(state, e, this);
1135
1577
  }
1136
1578
  finally {
1137
- _commandFinally(state, this);
1579
+ await _commandFinally(state, this);
1138
1580
  }
1139
1581
  }
1140
1582
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1146,14 +1588,15 @@ class StableBrowser {
1146
1588
  world,
1147
1589
  type: Types.SET_DATE_TIME,
1148
1590
  text: `Set date time value: ${value}`,
1591
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1149
1592
  operation: "setDateTime",
1150
1593
  log: "***** set date time value " + selectors.element_name + " *****\n",
1151
- throwError: false,
1594
+ // throwError: false,
1152
1595
  };
1153
1596
  try {
1154
1597
  await _preCommand(state, this);
1155
1598
  try {
1156
- await state.element.click();
1599
+ await performAction("click", state.element, options, this, state, _params);
1157
1600
  await new Promise((resolve) => setTimeout(resolve, 500));
1158
1601
  if (format) {
1159
1602
  state.value = dayjs(state.value).format(format);
@@ -1202,7 +1645,7 @@ class StableBrowser {
1202
1645
  await _commandError(state, e, this);
1203
1646
  }
1204
1647
  finally {
1205
- _commandFinally(state, this);
1648
+ await _commandFinally(state, this);
1206
1649
  }
1207
1650
  }
1208
1651
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1217,17 +1660,29 @@ class StableBrowser {
1217
1660
  world,
1218
1661
  type: Types.FILL,
1219
1662
  text: `Click type input with value: ${_value}`,
1663
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1220
1664
  operation: "clickType",
1221
1665
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1222
1666
  };
1667
+ if (!options) {
1668
+ options = {};
1669
+ }
1223
1670
  if (newValue !== _value) {
1224
1671
  //this.logger.info(_value + "=" + newValue);
1225
1672
  _value = newValue;
1226
1673
  }
1227
1674
  try {
1228
1675
  await _preCommand(state, this);
1676
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1677
+ // tag the element
1678
+ let newElementSelector = await state.element.evaluate((el, token) => {
1679
+ // use attribute and not id
1680
+ const attrName = `data-blinq-id-${token}`;
1681
+ el.setAttribute(attrName, "");
1682
+ return `[${attrName}]`;
1683
+ }, randomToken);
1229
1684
  state.info.value = _value;
1230
- if (options === null || options === undefined || !options.press) {
1685
+ if (!options.press) {
1231
1686
  try {
1232
1687
  let currentValue = await state.element.inputValue();
1233
1688
  if (currentValue) {
@@ -1238,13 +1693,9 @@ class StableBrowser {
1238
1693
  this.logger.info("unable to clear input value");
1239
1694
  }
1240
1695
  }
1241
- if (options === null || options === undefined || options.press) {
1242
- try {
1243
- await state.element.click({ timeout: 5000 });
1244
- }
1245
- catch (e) {
1246
- await state.element.dispatchEvent("click");
1247
- }
1696
+ if (options.press) {
1697
+ options.timeout = 5000;
1698
+ await performAction("click", state.element, options, this, state, _params);
1248
1699
  }
1249
1700
  else {
1250
1701
  try {
@@ -1255,6 +1706,25 @@ class StableBrowser {
1255
1706
  }
1256
1707
  }
1257
1708
  await new Promise((resolve) => setTimeout(resolve, 500));
1709
+ // check if the element exist after the click (no wait)
1710
+ const count = await state.element.count({ timeout: 0 });
1711
+ if (count === 0) {
1712
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1713
+ const scope = state.element._frame ?? element.page();
1714
+ let prefixSelector = "";
1715
+ const frameControlSelector = " >> internal:control=enter-frame";
1716
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1717
+ if (frameSelectorIndex !== -1) {
1718
+ // remove everything after the >> internal:control=enter-frame
1719
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1720
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1721
+ }
1722
+ // if (element?._frame?._selector) {
1723
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1724
+ // }
1725
+ const newSelector = prefixSelector + newElementSelector;
1726
+ state.element = scope.locator(newSelector).first();
1727
+ }
1258
1728
  const valueSegment = state.value.split("&&");
1259
1729
  for (let i = 0; i < valueSegment.length; i++) {
1260
1730
  if (i > 0) {
@@ -1275,14 +1745,21 @@ class StableBrowser {
1275
1745
  await new Promise((resolve) => setTimeout(resolve, 500));
1276
1746
  }
1277
1747
  }
1748
+ //if (!this.fastMode) {
1278
1749
  await _screenshot(state, this);
1750
+ //}
1279
1751
  if (enter === true) {
1280
1752
  await new Promise((resolve) => setTimeout(resolve, 2000));
1281
1753
  await this.page.keyboard.press("Enter");
1282
1754
  await this.waitForPageLoad();
1283
1755
  }
1284
1756
  else if (enter === false) {
1285
- await state.element.dispatchEvent("change");
1757
+ try {
1758
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1759
+ }
1760
+ catch (e) {
1761
+ // ignore
1762
+ }
1286
1763
  //await this.page.keyboard.press("Tab");
1287
1764
  }
1288
1765
  else {
@@ -1297,7 +1774,7 @@ class StableBrowser {
1297
1774
  await _commandError(state, e, this);
1298
1775
  }
1299
1776
  finally {
1300
- _commandFinally(state, this);
1777
+ await _commandFinally(state, this);
1301
1778
  }
1302
1779
  }
1303
1780
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1319,30 +1796,67 @@ class StableBrowser {
1319
1796
  if (enter) {
1320
1797
  await new Promise((resolve) => setTimeout(resolve, 2000));
1321
1798
  await this.page.keyboard.press("Enter");
1799
+ await this.waitForPageLoad();
1322
1800
  }
1323
- await this.waitForPageLoad();
1324
1801
  return state.info;
1325
1802
  }
1326
1803
  catch (e) {
1327
1804
  await _commandError(state, e, this);
1328
1805
  }
1329
1806
  finally {
1330
- _commandFinally(state, this);
1807
+ await _commandFinally(state, this);
1808
+ }
1809
+ }
1810
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1811
+ const state = {
1812
+ selectors,
1813
+ _params,
1814
+ files,
1815
+ value: '"' + files.join('", "') + '"',
1816
+ options,
1817
+ world,
1818
+ type: Types.SET_INPUT_FILES,
1819
+ text: `Set input files`,
1820
+ _text: `Set input files on ${selectors.element_name}`,
1821
+ operation: "setInputFiles",
1822
+ log: "***** set input files " + selectors.element_name + " *****\n",
1823
+ };
1824
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1825
+ try {
1826
+ await _preCommand(state, this);
1827
+ for (let i = 0; i < files.length; i++) {
1828
+ const file = files[i];
1829
+ const filePath = path.join(uploadsFolder, file);
1830
+ if (!fs.existsSync(filePath)) {
1831
+ throw new Error(`File not found: ${filePath}`);
1832
+ }
1833
+ state.files[i] = filePath;
1834
+ }
1835
+ await state.element.setInputFiles(files);
1836
+ return state.info;
1837
+ }
1838
+ catch (e) {
1839
+ await _commandError(state, e, this);
1840
+ }
1841
+ finally {
1842
+ await _commandFinally(state, this);
1331
1843
  }
1332
1844
  }
1333
1845
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1334
1846
  return await this._getText(selectors, 0, _params, options, info, world);
1335
1847
  }
1336
1848
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1849
+ const timeout = this._getFindElementTimeout(options);
1337
1850
  _validateSelectors(selectors);
1338
1851
  let screenshotId = null;
1339
1852
  let screenshotPath = null;
1340
1853
  if (!info.log) {
1341
1854
  info.log = "";
1855
+ info.locatorLog = new LocatorLog(selectors);
1342
1856
  }
1343
1857
  info.operation = "getText";
1344
1858
  info.selectors = selectors;
1345
- let element = await this._locate(selectors, info, _params);
1859
+ let element = await this._locate(selectors, info, _params, timeout);
1346
1860
  if (climb > 0) {
1347
1861
  const climbArray = [];
1348
1862
  for (let i = 0; i < climb; i++) {
@@ -1361,6 +1875,18 @@ class StableBrowser {
1361
1875
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1362
1876
  try {
1363
1877
  await this._highlightElements(element);
1878
+ // if (world && world.screenshot && !world.screenshotPath) {
1879
+ // // console.log(`Highlighting for get text while running from recorder`);
1880
+ // this._highlightElements(element)
1881
+ // .then(async () => {
1882
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1883
+ // this._unhighlightElements(element).then(
1884
+ // () => {}
1885
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1886
+ // );
1887
+ // })
1888
+ // .catch(e);
1889
+ // }
1364
1890
  const elementText = await element.innerText();
1365
1891
  return {
1366
1892
  text: elementText,
@@ -1372,7 +1898,7 @@ class StableBrowser {
1372
1898
  }
1373
1899
  catch (e) {
1374
1900
  //await this.closeUnexpectedPopups();
1375
- this.logger.info("no innerText will use textContent");
1901
+ this.logger.info("no innerText, will use textContent");
1376
1902
  const elementText = await element.textContent();
1377
1903
  return { text: elementText, screenshotId, screenshotPath, value: value };
1378
1904
  }
@@ -1397,6 +1923,7 @@ class StableBrowser {
1397
1923
  highlight: false,
1398
1924
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1399
1925
  text: `Verify element contains pattern: ${pattern}`,
1926
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1400
1927
  operation: "containsPattern",
1401
1928
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1402
1929
  };
@@ -1428,10 +1955,12 @@ class StableBrowser {
1428
1955
  await _commandError(state, e, this);
1429
1956
  }
1430
1957
  finally {
1431
- _commandFinally(state, this);
1958
+ await _commandFinally(state, this);
1432
1959
  }
1433
1960
  }
1434
1961
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1962
+ const timeout = this._getFindElementTimeout(options);
1963
+ const startTime = Date.now();
1435
1964
  const state = {
1436
1965
  selectors,
1437
1966
  _params,
@@ -1458,61 +1987,137 @@ class StableBrowser {
1458
1987
  }
1459
1988
  let foundObj = null;
1460
1989
  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;
1990
+ while (Date.now() - startTime < timeout) {
1991
+ try {
1992
+ await _preCommand(state, this);
1993
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1994
+ if (foundObj && foundObj.element) {
1995
+ await this.scrollIfNeeded(foundObj.element, state.info);
1474
1996
  }
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])) {
1997
+ await _screenshot(state, this);
1998
+ const dateAlternatives = findDateAlternatives(text);
1999
+ const numberAlternatives = findNumberAlternatives(text);
2000
+ if (dateAlternatives.date) {
2001
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
2002
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
2003
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
2004
+ return state.info;
2005
+ }
2006
+ }
2007
+ }
2008
+ else if (numberAlternatives.number) {
2009
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
2010
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
2011
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
2012
+ return state.info;
2013
+ }
2014
+ }
2015
+ }
2016
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1482
2017
  return state.info;
1483
2018
  }
1484
2019
  }
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);
2020
+ catch (e) {
2021
+ // Log error but continue retrying until timeout is reached
2022
+ this.logger.warn("Retrying containsText due to: " + e.message);
2023
+ }
2024
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1491
2025
  }
1492
- return state.info;
2026
+ state.info.foundText = foundObj?.text;
2027
+ state.info.value = foundObj?.value;
2028
+ throw new Error("element doesn't contain text " + text);
1493
2029
  }
1494
2030
  catch (e) {
1495
2031
  await _commandError(state, e, this);
2032
+ throw e;
1496
2033
  }
1497
2034
  finally {
1498
- _commandFinally(state, this);
2035
+ await _commandFinally(state, this);
1499
2036
  }
1500
2037
  }
1501
- _getDataFile(world = null) {
1502
- let dataFile = null;
1503
- if (world && world.reportFolder) {
1504
- dataFile = path.join(world.reportFolder, "data.json");
2038
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
2039
+ const timeout = this._getFindElementTimeout(options);
2040
+ const startTime = Date.now();
2041
+ const state = {
2042
+ _params,
2043
+ value: referanceSnapshot,
2044
+ options,
2045
+ world,
2046
+ locate: false,
2047
+ scroll: false,
2048
+ screenshot: true,
2049
+ highlight: false,
2050
+ type: Types.SNAPSHOT_VALIDATION,
2051
+ text: `verify snapshot: ${referanceSnapshot}`,
2052
+ operation: "snapshotValidation",
2053
+ log: "***** verify snapshot *****\n",
2054
+ };
2055
+ if (!referanceSnapshot) {
2056
+ throw new Error("referanceSnapshot is null");
2057
+ }
2058
+ let text = null;
2059
+ const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
2060
+ if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
2061
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
1505
2062
  }
1506
- else if (this.reportFolder) {
1507
- dataFile = path.join(this.reportFolder, "data.json");
2063
+ else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
2064
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
1508
2065
  }
1509
- else if (this.context && this.context.reportFolder) {
1510
- dataFile = path.join(this.context.reportFolder, "data.json");
2066
+ else if (referanceSnapshot.startsWith("yaml:")) {
2067
+ text = referanceSnapshot.substring(5);
1511
2068
  }
1512
2069
  else {
1513
- dataFile = "data.json";
2070
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
2071
+ }
2072
+ state.text = text;
2073
+ const newValue = await this._replaceWithLocalData(text, world);
2074
+ await _preCommand(state, this);
2075
+ let foundObj = null;
2076
+ try {
2077
+ let matchResult = null;
2078
+ while (Date.now() - startTime < timeout) {
2079
+ try {
2080
+ let scope = null;
2081
+ if (!frameSelectors) {
2082
+ scope = this.page;
2083
+ }
2084
+ else {
2085
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
2086
+ }
2087
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
2088
+ if (snapshot && snapshot.length <= 10) {
2089
+ console.log("Page snapshot length is suspiciously small:", snapshot);
2090
+ }
2091
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
2092
+ if (matchResult === undefined) {
2093
+ console.log("snapshotValidation returned undefined");
2094
+ }
2095
+ if (matchResult.errorLine !== -1) {
2096
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
2097
+ }
2098
+ // highlight and screenshot
2099
+ try {
2100
+ await await highlightSnapshot(newValue, scope);
2101
+ await _screenshot(state, this);
2102
+ }
2103
+ catch (e) { }
2104
+ return state.info;
2105
+ }
2106
+ catch (e) {
2107
+ // Log error but continue retrying until timeout is reached
2108
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
2109
+ }
2110
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
2111
+ }
2112
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
2113
+ }
2114
+ catch (e) {
2115
+ await _commandError(state, e, this);
2116
+ throw e;
2117
+ }
2118
+ finally {
2119
+ await _commandFinally(state, this);
1514
2120
  }
1515
- return dataFile;
1516
2121
  }
1517
2122
  async waitForUserInput(message, world = null) {
1518
2123
  if (!message) {
@@ -1542,13 +2147,22 @@ class StableBrowser {
1542
2147
  return;
1543
2148
  }
1544
2149
  // if data file exists, load it
1545
- const dataFile = this._getDataFile(world);
2150
+ const dataFile = _getDataFile(world, this.context, this);
1546
2151
  let data = this.getTestData(world);
1547
2152
  // merge the testData with the existing data
1548
2153
  Object.assign(data, testData);
1549
2154
  // save the data to the file
1550
2155
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1551
2156
  }
2157
+ overwriteTestData(testData, world = null) {
2158
+ if (!testData) {
2159
+ return;
2160
+ }
2161
+ // if data file exists, load it
2162
+ const dataFile = _getDataFile(world, this.context, this);
2163
+ // save the data to the file
2164
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
2165
+ }
1552
2166
  _getDataFilePath(fileName) {
1553
2167
  let dataFile = path.join(this.project_path, "data", fileName);
1554
2168
  if (fs.existsSync(dataFile)) {
@@ -1645,14 +2259,12 @@ class StableBrowser {
1645
2259
  }
1646
2260
  }
1647
2261
  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;
2262
+ return _getTestData(world, this.context, this);
1654
2263
  }
1655
2264
  async _screenShot(options = {}, world = null, info = null) {
2265
+ if (!options) {
2266
+ options = {};
2267
+ }
1656
2268
  // collect url/path/title
1657
2269
  if (info) {
1658
2270
  if (!info.title) {
@@ -1677,13 +2289,11 @@ class StableBrowser {
1677
2289
  if (!fs.existsSync(world.screenshotPath)) {
1678
2290
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1679
2291
  }
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");
2292
+ // to make sure the path doesn't start with -
2293
+ const uuidStr = "id_" + randomUUID();
2294
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1685
2295
  try {
1686
- await this.takeScreenshot(screenshotPath);
2296
+ await this.takeScreenshot(screenshotPath, options.fullPage === true);
1687
2297
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1688
2298
  // // save the buffer to the screenshot path asynchrously
1689
2299
  // fs.writeFile(screenshotPath, buffer, (err) => {
@@ -1691,20 +2301,20 @@ class StableBrowser {
1691
2301
  // this.logger.info("unable to save screenshot " + screenshotPath);
1692
2302
  // }
1693
2303
  // });
2304
+ result.screenshotId = uuidStr;
2305
+ result.screenshotPath = screenshotPath;
2306
+ if (info && info.box) {
2307
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
2308
+ }
1694
2309
  }
1695
2310
  catch (e) {
1696
2311
  this.logger.info("unable to take screenshot, ignored");
1697
2312
  }
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
2313
  }
1704
2314
  else if (options && options.screenshot) {
1705
2315
  result.screenshotPath = options.screenshotPath;
1706
2316
  try {
1707
- await this.takeScreenshot(options.screenshotPath);
2317
+ await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
1708
2318
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1709
2319
  // // save the buffer to the screenshot path asynchrously
1710
2320
  // fs.writeFile(options.screenshotPath, buffer, (err) => {
@@ -1722,7 +2332,7 @@ class StableBrowser {
1722
2332
  }
1723
2333
  return result;
1724
2334
  }
1725
- async takeScreenshot(screenshotPath) {
2335
+ async takeScreenshot(screenshotPath, fullPage = false) {
1726
2336
  const playContext = this.context.playContext;
1727
2337
  // Using CDP to capture the screenshot
1728
2338
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
@@ -1734,17 +2344,20 @@ class StableBrowser {
1734
2344
  document.documentElement.clientWidth,
1735
2345
  ])));
1736
2346
  let screenshotBuffer = null;
2347
+ // if (focusedElement) {
2348
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2349
+ // await this._unhighlightElements(focusedElement);
2350
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2351
+ // console.log(`Unhighlighted previous element`);
2352
+ // }
2353
+ // if (focusedElement) {
2354
+ // await this._highlightElements(focusedElement);
2355
+ // }
1737
2356
  if (this.context.browserName === "chromium") {
1738
2357
  const client = await playContext.newCDPSession(this.page);
1739
2358
  const { data } = await client.send("Page.captureScreenshot", {
1740
2359
  format: "png",
1741
- // clip: {
1742
- // x: 0,
1743
- // y: 0,
1744
- // width: viewportWidth,
1745
- // height: viewportHeight,
1746
- // scale: 1,
1747
- // },
2360
+ captureBeyondViewport: fullPage,
1748
2361
  });
1749
2362
  await client.detach();
1750
2363
  if (!screenshotPath) {
@@ -1753,8 +2366,12 @@ class StableBrowser {
1753
2366
  screenshotBuffer = Buffer.from(data, "base64");
1754
2367
  }
1755
2368
  else {
1756
- screenshotBuffer = await this.page.screenshot();
2369
+ screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
1757
2370
  }
2371
+ // if (focusedElement) {
2372
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2373
+ // await this._unhighlightElements(focusedElement);
2374
+ // }
1758
2375
  let image = await Jimp.read(screenshotBuffer);
1759
2376
  // Get the image dimensions
1760
2377
  const { width, height } = image.bitmap;
@@ -1767,6 +2384,7 @@ class StableBrowser {
1767
2384
  else {
1768
2385
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1769
2386
  }
2387
+ return screenshotBuffer;
1770
2388
  }
1771
2389
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1772
2390
  const state = {
@@ -1789,7 +2407,7 @@ class StableBrowser {
1789
2407
  await _commandError(state, e, this);
1790
2408
  }
1791
2409
  finally {
1792
- _commandFinally(state, this);
2410
+ await _commandFinally(state, this);
1793
2411
  }
1794
2412
  }
1795
2413
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1802,8 +2420,10 @@ class StableBrowser {
1802
2420
  world,
1803
2421
  type: Types.EXTRACT,
1804
2422
  text: `Extract attribute from element`,
2423
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1805
2424
  operation: "extractAttribute",
1806
2425
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
2426
+ allowDisabled: true,
1807
2427
  };
1808
2428
  await new Promise((resolve) => setTimeout(resolve, 2000));
1809
2429
  try {
@@ -1818,22 +2438,480 @@ class StableBrowser {
1818
2438
  case "value":
1819
2439
  state.value = await state.element.inputValue();
1820
2440
  break;
2441
+ case "text":
2442
+ state.value = await state.element.textContent();
2443
+ break;
1821
2444
  default:
1822
2445
  state.value = await state.element.getAttribute(attribute);
1823
2446
  break;
1824
2447
  }
2448
+ if (options !== null) {
2449
+ if (options.regex && options.regex !== "") {
2450
+ // Construct a regex pattern from the provided string
2451
+ const regex = options.regex.slice(1, -1);
2452
+ const regexPattern = new RegExp(regex, "g");
2453
+ const matches = state.value.match(regexPattern);
2454
+ if (matches) {
2455
+ let newValue = "";
2456
+ for (const match of matches) {
2457
+ newValue += match;
2458
+ }
2459
+ state.value = newValue;
2460
+ }
2461
+ }
2462
+ if (options.trimSpaces && options.trimSpaces === true) {
2463
+ state.value = state.value.trim();
2464
+ }
2465
+ }
1825
2466
  state.info.value = state.value;
1826
2467
  this.setTestData({ [variable]: state.value }, world);
1827
2468
  this.logger.info("set test data: " + variable + "=" + state.value);
2469
+ if (process.env.MODE === "executions") {
2470
+ const globalDataFile = "global_test_data.json";
2471
+ if (existsSync(globalDataFile)) {
2472
+ this.saveTestDataAsGlobal({}, world);
2473
+ }
2474
+ }
2475
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1828
2476
  return state.info;
1829
2477
  }
1830
2478
  catch (e) {
1831
2479
  await _commandError(state, e, this);
1832
2480
  }
1833
2481
  finally {
1834
- _commandFinally(state, this);
2482
+ await _commandFinally(state, this);
1835
2483
  }
1836
2484
  }
2485
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2486
+ const state = {
2487
+ selectors,
2488
+ _params,
2489
+ property,
2490
+ variable,
2491
+ options,
2492
+ world,
2493
+ type: Types.EXTRACT_PROPERTY,
2494
+ text: `Extract property from element`,
2495
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2496
+ operation: "extractProperty",
2497
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2498
+ allowDisabled: true,
2499
+ };
2500
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2501
+ try {
2502
+ await _preCommand(state, this);
2503
+ switch (property) {
2504
+ case "inner_text":
2505
+ state.value = await state.element.innerText();
2506
+ break;
2507
+ case "href":
2508
+ state.value = await state.element.getAttribute("href");
2509
+ break;
2510
+ case "value":
2511
+ state.value = await state.element.inputValue();
2512
+ break;
2513
+ case "text":
2514
+ state.value = await state.element.textContent();
2515
+ break;
2516
+ default:
2517
+ if (property.startsWith("dataset.")) {
2518
+ const dataAttribute = property.substring(8);
2519
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2520
+ }
2521
+ else {
2522
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2523
+ }
2524
+ }
2525
+ if (options !== null) {
2526
+ if (options.regex && options.regex !== "") {
2527
+ // Construct a regex pattern from the provided string
2528
+ const regex = options.regex.slice(1, -1);
2529
+ const regexPattern = new RegExp(regex, "g");
2530
+ const matches = state.value.match(regexPattern);
2531
+ if (matches) {
2532
+ let newValue = "";
2533
+ for (const match of matches) {
2534
+ newValue += match;
2535
+ }
2536
+ state.value = newValue;
2537
+ }
2538
+ }
2539
+ if (options.trimSpaces && options.trimSpaces === true) {
2540
+ state.value = state.value.trim();
2541
+ }
2542
+ }
2543
+ state.info.value = state.value;
2544
+ this.setTestData({ [variable]: state.value }, world);
2545
+ this.logger.info("set test data: " + variable + "=" + state.value);
2546
+ if (process.env.MODE === "executions") {
2547
+ const globalDataFile = "global_test_data.json";
2548
+ if (existsSync(globalDataFile)) {
2549
+ this.saveTestDataAsGlobal({}, world);
2550
+ }
2551
+ }
2552
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2553
+ return state.info;
2554
+ }
2555
+ catch (e) {
2556
+ await _commandError(state, e, this);
2557
+ }
2558
+ finally {
2559
+ await _commandFinally(state, this);
2560
+ }
2561
+ }
2562
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2563
+ const state = {
2564
+ selectors,
2565
+ _params,
2566
+ attribute,
2567
+ value,
2568
+ options,
2569
+ world,
2570
+ type: Types.VERIFY_ATTRIBUTE,
2571
+ highlight: true,
2572
+ screenshot: true,
2573
+ text: `Verify element attribute`,
2574
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2575
+ operation: "verifyAttribute",
2576
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2577
+ allowDisabled: true,
2578
+ };
2579
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2580
+ let val;
2581
+ let expectedValue;
2582
+ try {
2583
+ await _preCommand(state, this);
2584
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2585
+ state.info.expectedValue = expectedValue;
2586
+ switch (attribute) {
2587
+ case "innerText":
2588
+ val = String(await state.element.innerText());
2589
+ break;
2590
+ case "text":
2591
+ val = String(await state.element.textContent());
2592
+ break;
2593
+ case "value":
2594
+ val = String(await state.element.inputValue());
2595
+ break;
2596
+ case "checked":
2597
+ val = String(await state.element.isChecked());
2598
+ break;
2599
+ case "disabled":
2600
+ val = String(await state.element.isDisabled());
2601
+ break;
2602
+ case "readOnly":
2603
+ const isEditable = await state.element.isEditable();
2604
+ val = String(!isEditable);
2605
+ break;
2606
+ default:
2607
+ val = String(await state.element.getAttribute(attribute));
2608
+ break;
2609
+ }
2610
+ state.info.value = val;
2611
+ let regex;
2612
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2613
+ const patternBody = expectedValue.slice(1, -1);
2614
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2615
+ regex = new RegExp(processedPattern, "gs");
2616
+ state.info.regex = true;
2617
+ }
2618
+ else {
2619
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2620
+ regex = new RegExp(escapedPattern, "g");
2621
+ }
2622
+ if (attribute === "innerText") {
2623
+ if (state.info.regex) {
2624
+ if (!regex.test(val)) {
2625
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2626
+ state.info.failCause.assertionFailed = true;
2627
+ state.info.failCause.lastError = errorMessage;
2628
+ throw new Error(errorMessage);
2629
+ }
2630
+ }
2631
+ else {
2632
+ const valLines = val.split("\n");
2633
+ const expectedLines = expectedValue.split("\n");
2634
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2635
+ if (!isPart) {
2636
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2637
+ state.info.failCause.assertionFailed = true;
2638
+ state.info.failCause.lastError = errorMessage;
2639
+ throw new Error(errorMessage);
2640
+ }
2641
+ }
2642
+ }
2643
+ else {
2644
+ if (!val.match(regex)) {
2645
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2646
+ state.info.failCause.assertionFailed = true;
2647
+ state.info.failCause.lastError = errorMessage;
2648
+ throw new Error(errorMessage);
2649
+ }
2650
+ }
2651
+ return state.info;
2652
+ }
2653
+ catch (e) {
2654
+ await _commandError(state, e, this);
2655
+ }
2656
+ finally {
2657
+ await _commandFinally(state, this);
2658
+ }
2659
+ }
2660
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2661
+ const state = {
2662
+ selectors,
2663
+ _params,
2664
+ property,
2665
+ value,
2666
+ options,
2667
+ world,
2668
+ type: Types.VERIFY_PROPERTY,
2669
+ highlight: true,
2670
+ screenshot: true,
2671
+ text: `Verify element property`,
2672
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2673
+ operation: "verifyProperty",
2674
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2675
+ allowDisabled: true,
2676
+ };
2677
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2678
+ let val;
2679
+ let expectedValue;
2680
+ try {
2681
+ await _preCommand(state, this);
2682
+ expectedValue = await this._replaceWithLocalData(value, world);
2683
+ state.info.expectedValue = expectedValue;
2684
+ switch (property) {
2685
+ case "innerText":
2686
+ val = String(await state.element.innerText());
2687
+ break;
2688
+ case "text":
2689
+ val = String(await state.element.textContent());
2690
+ break;
2691
+ case "value":
2692
+ val = String(await state.element.inputValue());
2693
+ break;
2694
+ case "checked":
2695
+ val = String(await state.element.isChecked());
2696
+ break;
2697
+ case "disabled":
2698
+ val = String(await state.element.isDisabled());
2699
+ break;
2700
+ case "readOnly":
2701
+ const isEditable = await state.element.isEditable();
2702
+ val = String(!isEditable);
2703
+ break;
2704
+ case "innerHTML":
2705
+ val = String(await state.element.innerHTML());
2706
+ break;
2707
+ case "outerHTML":
2708
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2709
+ break;
2710
+ default:
2711
+ if (property.startsWith("dataset.")) {
2712
+ const dataAttribute = property.substring(8);
2713
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2714
+ }
2715
+ else {
2716
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2717
+ }
2718
+ }
2719
+ // Helper function to remove all style="" attributes
2720
+ const removeStyleAttributes = (htmlString) => {
2721
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2722
+ };
2723
+ // Remove style attributes for innerHTML and outerHTML properties
2724
+ if (property === "innerHTML" || property === "outerHTML") {
2725
+ val = removeStyleAttributes(val);
2726
+ expectedValue = removeStyleAttributes(expectedValue);
2727
+ }
2728
+ state.info.value = val;
2729
+ let regex;
2730
+ state.info.value = val;
2731
+ const isRegex = expectedValue.startsWith("regex:");
2732
+ const isContains = expectedValue.startsWith("contains:");
2733
+ const isExact = expectedValue.startsWith("exact:");
2734
+ let matchPassed = false;
2735
+ if (isRegex) {
2736
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2737
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2738
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2739
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2740
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2741
+ const regex = new RegExp(patternBody, flags);
2742
+ state.info.regex = true;
2743
+ matchPassed = regex.test(val);
2744
+ }
2745
+ else {
2746
+ // Fallback: treat as literal
2747
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2748
+ const regex = new RegExp(escapedPattern, "g");
2749
+ matchPassed = regex.test(val);
2750
+ }
2751
+ }
2752
+ else if (isContains) {
2753
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2754
+ matchPassed = val.includes(containsValue);
2755
+ }
2756
+ else if (isExact) {
2757
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2758
+ matchPassed = val === exactValue;
2759
+ }
2760
+ else if (property === "innerText") {
2761
+ // Default innerText logic
2762
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2763
+ const valLines = val.split("\n");
2764
+ const expectedLines = normalizedExpectedValue.split("\n");
2765
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2766
+ }
2767
+ else {
2768
+ // Fallback exact or loose match
2769
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2770
+ const regex = new RegExp(escapedPattern, "g");
2771
+ matchPassed = regex.test(val);
2772
+ }
2773
+ if (!matchPassed) {
2774
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2775
+ state.info.failCause.assertionFailed = true;
2776
+ state.info.failCause.lastError = errorMessage;
2777
+ throw new Error(errorMessage);
2778
+ }
2779
+ return state.info;
2780
+ }
2781
+ catch (e) {
2782
+ await _commandError(state, e, this);
2783
+ }
2784
+ finally {
2785
+ await _commandFinally(state, this);
2786
+ }
2787
+ }
2788
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2789
+ // Convert timeout from seconds to milliseconds
2790
+ const timeoutMs = timeout * 1000;
2791
+ const state = {
2792
+ selectors,
2793
+ _params,
2794
+ condition,
2795
+ timeout: timeoutMs, // Store as milliseconds for internal use
2796
+ options,
2797
+ world,
2798
+ type: Types.CONDITIONAL_WAIT,
2799
+ highlight: true,
2800
+ screenshot: true,
2801
+ text: `Conditional wait for element`,
2802
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2803
+ operation: "conditionalWait",
2804
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2805
+ allowDisabled: true,
2806
+ info: {},
2807
+ };
2808
+ state.options ??= { timeout: timeoutMs };
2809
+ // Initialize startTime outside try block to ensure it's always accessible
2810
+ const startTime = Date.now();
2811
+ let conditionMet = false;
2812
+ let currentValue = null;
2813
+ let lastError = null;
2814
+ // Main retry loop - continues until timeout or condition is met
2815
+ while (Date.now() - startTime < timeoutMs) {
2816
+ const elapsedTime = Date.now() - startTime;
2817
+ const remainingTime = timeoutMs - elapsedTime;
2818
+ try {
2819
+ // Try to execute _preCommand (element location)
2820
+ await _preCommand(state, this);
2821
+ // If _preCommand succeeds, start condition checking
2822
+ const checkCondition = async () => {
2823
+ try {
2824
+ switch (condition.toLowerCase()) {
2825
+ case "checked":
2826
+ currentValue = await state.element.isChecked();
2827
+ return currentValue === true;
2828
+ case "unchecked":
2829
+ currentValue = await state.element.isChecked();
2830
+ return currentValue === false;
2831
+ case "visible":
2832
+ currentValue = await state.element.isVisible();
2833
+ return currentValue === true;
2834
+ case "hidden":
2835
+ currentValue = await state.element.isVisible();
2836
+ return currentValue === false;
2837
+ case "enabled":
2838
+ currentValue = await state.element.isDisabled();
2839
+ return currentValue === false;
2840
+ case "disabled":
2841
+ currentValue = await state.element.isDisabled();
2842
+ return currentValue === true;
2843
+ case "editable":
2844
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2845
+ currentValue = await state.element.isContentEditable();
2846
+ return currentValue === true;
2847
+ default:
2848
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2849
+ state.info.success = false;
2850
+ return false;
2851
+ }
2852
+ }
2853
+ catch (error) {
2854
+ // Don't throw here, just return false to continue retrying
2855
+ return false;
2856
+ }
2857
+ };
2858
+ // Inner loop for condition checking (once element is located)
2859
+ while (Date.now() - startTime < timeoutMs) {
2860
+ const currentElapsedTime = Date.now() - startTime;
2861
+ conditionMet = await checkCondition();
2862
+ if (conditionMet) {
2863
+ break;
2864
+ }
2865
+ // Check if we still have time for another attempt
2866
+ if (Date.now() - startTime + 50 < timeoutMs) {
2867
+ await new Promise((res) => setTimeout(res, 50));
2868
+ }
2869
+ else {
2870
+ break;
2871
+ }
2872
+ }
2873
+ // If we got here and condition is met, break out of main loop
2874
+ if (conditionMet) {
2875
+ break;
2876
+ }
2877
+ // If condition not met but no exception, we've timed out
2878
+ break;
2879
+ }
2880
+ catch (e) {
2881
+ lastError = e;
2882
+ const currentElapsedTime = Date.now() - startTime;
2883
+ const timeLeft = timeoutMs - currentElapsedTime;
2884
+ // Check if we have enough time left to retry
2885
+ if (timeLeft > 100) {
2886
+ await new Promise((resolve) => setTimeout(resolve, 50));
2887
+ }
2888
+ else {
2889
+ break;
2890
+ }
2891
+ }
2892
+ }
2893
+ const actualWaitTime = Date.now() - startTime;
2894
+ state.info = {
2895
+ success: conditionMet,
2896
+ conditionMet,
2897
+ actualWaitTime,
2898
+ currentValue,
2899
+ lastError: lastError?.message || null,
2900
+ message: conditionMet
2901
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2902
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2903
+ };
2904
+ if (lastError) {
2905
+ state.log += `Last error: ${lastError.message}\n`;
2906
+ }
2907
+ try {
2908
+ await _commandFinally(state, this);
2909
+ }
2910
+ catch (finallyError) {
2911
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2912
+ }
2913
+ return state.info;
2914
+ }
1837
2915
  async extractEmailData(emailAddress, options, world) {
1838
2916
  if (!emailAddress) {
1839
2917
  throw new Error("email address is null");
@@ -1852,7 +2930,7 @@ class StableBrowser {
1852
2930
  if (options && options.timeout) {
1853
2931
  timeout = options.timeout;
1854
2932
  }
1855
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2933
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1856
2934
  const request = {
1857
2935
  method: "POST",
1858
2936
  url: serviceUrl,
@@ -1897,6 +2975,12 @@ class StableBrowser {
1897
2975
  emailUrl = url;
1898
2976
  codeOrUrlFound = true;
1899
2977
  }
2978
+ if (process.env.MODE === "executions") {
2979
+ const globalDataFile = "global_test_data.json";
2980
+ if (existsSync(globalDataFile)) {
2981
+ this.saveTestDataAsGlobal({}, world);
2982
+ }
2983
+ }
1900
2984
  if (codeOrUrlFound) {
1901
2985
  return { emailUrl, emailCode };
1902
2986
  }
@@ -1923,27 +3007,32 @@ class StableBrowser {
1923
3007
  async _highlightElements(scope, css) {
1924
3008
  try {
1925
3009
  if (!scope) {
3010
+ // console.log(`Scope is not defined`);
1926
3011
  return;
1927
3012
  }
1928
3013
  if (!css) {
1929
3014
  scope
1930
3015
  .evaluate((node) => {
1931
3016
  if (node && node.style) {
1932
- let originalBorder = node.style.border;
1933
- node.style.border = "2px solid red";
3017
+ let originalOutline = node.style.outline;
3018
+ // console.log(`Original outline was: ${originalOutline}`);
3019
+ // node.__previousOutline = originalOutline;
3020
+ node.style.outline = "2px solid red";
3021
+ // console.log(`New outline is: ${node.style.outline}`);
1934
3022
  if (window) {
1935
3023
  window.addEventListener("beforeunload", function (e) {
1936
- node.style.border = originalBorder;
3024
+ node.style.outline = originalOutline;
1937
3025
  });
1938
3026
  }
1939
3027
  setTimeout(function () {
1940
- node.style.border = originalBorder;
3028
+ node.style.outline = originalOutline;
1941
3029
  }, 2000);
1942
3030
  }
1943
3031
  })
1944
3032
  .then(() => { })
1945
3033
  .catch((e) => {
1946
3034
  // ignore
3035
+ // console.error(`Could not highlight node : ${e}`);
1947
3036
  });
1948
3037
  }
1949
3038
  else {
@@ -1959,17 +3048,18 @@ class StableBrowser {
1959
3048
  if (!element.style) {
1960
3049
  return;
1961
3050
  }
1962
- var originalBorder = element.style.border;
3051
+ let originalOutline = element.style.outline;
3052
+ element.__previousOutline = originalOutline;
1963
3053
  // Set the new border to be red and 2px solid
1964
- element.style.border = "2px solid red";
3054
+ element.style.outline = "2px solid red";
1965
3055
  if (window) {
1966
3056
  window.addEventListener("beforeunload", function (e) {
1967
- element.style.border = originalBorder;
3057
+ element.style.outline = originalOutline;
1968
3058
  });
1969
3059
  }
1970
3060
  // Set a timeout to revert to the original border after 2 seconds
1971
3061
  setTimeout(function () {
1972
- element.style.border = originalBorder;
3062
+ element.style.outline = originalOutline;
1973
3063
  }, 2000);
1974
3064
  }
1975
3065
  return;
@@ -1977,6 +3067,7 @@ class StableBrowser {
1977
3067
  .then(() => { })
1978
3068
  .catch((e) => {
1979
3069
  // ignore
3070
+ // console.error(`Could not highlight css: ${e}`);
1980
3071
  });
1981
3072
  }
1982
3073
  }
@@ -1984,8 +3075,49 @@ class StableBrowser {
1984
3075
  console.debug(error);
1985
3076
  }
1986
3077
  }
3078
+ _matcher(text) {
3079
+ if (!text) {
3080
+ return { matcher: "contains", queryText: "" };
3081
+ }
3082
+ if (text.length < 2) {
3083
+ return { matcher: "contains", queryText: text };
3084
+ }
3085
+ const split = text.split(":");
3086
+ const matcher = split[0].toLowerCase();
3087
+ const queryText = split.slice(1).join(":").trim();
3088
+ return { matcher, queryText };
3089
+ }
3090
+ _getDomain(url) {
3091
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
3092
+ return "";
3093
+ }
3094
+ let hostnameFragments = url.split("/")[2].split(".");
3095
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
3096
+ return hostnameFragments.join("-").split(":").join("-");
3097
+ }
3098
+ let n = hostnameFragments.length;
3099
+ let fragments = [...hostnameFragments];
3100
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
3101
+ hostnameFragments.pop();
3102
+ n = hostnameFragments.length;
3103
+ }
3104
+ if (n == 0) {
3105
+ if (fragments[0] === "www")
3106
+ fragments = fragments.slice(1);
3107
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
3108
+ }
3109
+ if (hostnameFragments[0] === "www")
3110
+ hostnameFragments = hostnameFragments.slice(1);
3111
+ return hostnameFragments.join(".");
3112
+ }
3113
+ /**
3114
+ * Verify the page path matches the given path.
3115
+ * @param {string} pathPart - The path to verify.
3116
+ * @param {object} options - Options for verification.
3117
+ * @param {object} world - The world context.
3118
+ * @returns {Promise<object>} - The state info after verification.
3119
+ */
1987
3120
  async verifyPagePath(pathPart, options = {}, world = null) {
1988
- const startTime = Date.now();
1989
3121
  let error = null;
1990
3122
  let screenshotId = null;
1991
3123
  let screenshotPath = null;
@@ -1999,51 +3131,242 @@ class StableBrowser {
1999
3131
  pathPart = newValue;
2000
3132
  }
2001
3133
  info.pathPart = pathPart;
3134
+ const { matcher, queryText } = this._matcher(pathPart);
3135
+ const state = {
3136
+ text_search: queryText,
3137
+ options,
3138
+ world,
3139
+ locate: false,
3140
+ scroll: false,
3141
+ highlight: false,
3142
+ type: Types.VERIFY_PAGE_PATH,
3143
+ text: `Verify the page url is ${queryText}`,
3144
+ _text: `Verify the page url is ${queryText}`,
3145
+ operation: "verifyPagePath",
3146
+ log: "***** verify page url is " + queryText + " *****\n",
3147
+ };
2002
3148
  try {
3149
+ await _preCommand(state, this);
3150
+ state.info.text = queryText;
2003
3151
  for (let i = 0; i < 30; i++) {
2004
3152
  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;
3153
+ switch (matcher) {
3154
+ case "exact":
3155
+ if (url !== queryText) {
3156
+ if (i === 29) {
3157
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
3158
+ }
3159
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3160
+ continue;
3161
+ }
3162
+ break;
3163
+ case "contains":
3164
+ if (!url.includes(queryText)) {
3165
+ if (i === 29) {
3166
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
3167
+ }
3168
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3169
+ continue;
3170
+ }
3171
+ break;
3172
+ case "starts-with":
3173
+ {
3174
+ const domain = this._getDomain(url);
3175
+ if (domain.length > 0 && domain !== queryText) {
3176
+ if (i === 29) {
3177
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
3178
+ }
3179
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3180
+ continue;
3181
+ }
3182
+ }
3183
+ break;
3184
+ case "ends-with":
3185
+ {
3186
+ const urlObj = new URL(url);
3187
+ let route = "/";
3188
+ if (urlObj.pathname !== "/") {
3189
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
3190
+ }
3191
+ else {
3192
+ route = "/";
3193
+ }
3194
+ if (route !== queryText) {
3195
+ if (i === 29) {
3196
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
3197
+ }
3198
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3199
+ continue;
3200
+ }
3201
+ }
3202
+ break;
3203
+ case "regex":
3204
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3205
+ if (!regex.test(url)) {
3206
+ if (i === 29) {
3207
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
3208
+ }
3209
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3210
+ continue;
3211
+ }
3212
+ break;
3213
+ default:
3214
+ console.log("Unknown matching type, defaulting to contains matching");
3215
+ if (!url.includes(pathPart)) {
3216
+ if (i === 29) {
3217
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
3218
+ }
3219
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3220
+ continue;
3221
+ }
2011
3222
  }
2012
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2013
- return info;
3223
+ await _screenshot(state, this);
3224
+ return state.info;
2014
3225
  }
2015
3226
  }
2016
3227
  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);
3228
+ state.info.failCause.lastError = e.message;
3229
+ state.info.failCause.assertionFailed = true;
3230
+ await _commandError(state, e, this);
2025
3231
  }
2026
3232
  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
- });
3233
+ await _commandFinally(state, this);
3234
+ }
3235
+ }
3236
+ /**
3237
+ * Verify the page title matches the given title.
3238
+ * @param {string} title - The title to verify.
3239
+ * @param {object} options - Options for verification.
3240
+ * @param {object} world - The world context.
3241
+ * @returns {Promise<object>} - The state info after verification.
3242
+ */
3243
+ async verifyPageTitle(title, options = {}, world = null) {
3244
+ let error = null;
3245
+ let screenshotId = null;
3246
+ let screenshotPath = null;
3247
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3248
+ const newValue = await this._replaceWithLocalData(title, world);
3249
+ if (newValue !== title) {
3250
+ this.logger.info(title + "=" + newValue);
3251
+ title = newValue;
3252
+ }
3253
+ const { matcher, queryText } = this._matcher(title);
3254
+ const state = {
3255
+ text_search: queryText,
3256
+ options,
3257
+ world,
3258
+ locate: false,
3259
+ scroll: false,
3260
+ highlight: false,
3261
+ type: Types.VERIFY_PAGE_TITLE,
3262
+ text: `Verify the page title is ${queryText}`,
3263
+ _text: `Verify the page title is ${queryText}`,
3264
+ operation: "verifyPageTitle",
3265
+ log: "***** verify page title is " + queryText + " *****\n",
3266
+ };
3267
+ try {
3268
+ await _preCommand(state, this);
3269
+ state.info.text = queryText;
3270
+ for (let i = 0; i < 30; i++) {
3271
+ const foundTitle = await this.page.title();
3272
+ switch (matcher) {
3273
+ case "exact":
3274
+ if (foundTitle !== queryText) {
3275
+ if (i === 29) {
3276
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
3277
+ }
3278
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3279
+ continue;
3280
+ }
3281
+ break;
3282
+ case "contains":
3283
+ if (!foundTitle.includes(queryText)) {
3284
+ if (i === 29) {
3285
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
3286
+ }
3287
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3288
+ continue;
3289
+ }
3290
+ break;
3291
+ case "starts-with":
3292
+ if (!foundTitle.startsWith(queryText)) {
3293
+ if (i === 29) {
3294
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
3295
+ }
3296
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3297
+ continue;
3298
+ }
3299
+ break;
3300
+ case "ends-with":
3301
+ if (!foundTitle.endsWith(queryText)) {
3302
+ if (i === 29) {
3303
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
3304
+ }
3305
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3306
+ continue;
3307
+ }
3308
+ break;
3309
+ case "regex":
3310
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3311
+ if (!regex.test(foundTitle)) {
3312
+ if (i === 29) {
3313
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
3314
+ }
3315
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3316
+ continue;
3317
+ }
3318
+ break;
3319
+ default:
3320
+ console.log("Unknown matching type, defaulting to contains matching");
3321
+ if (!foundTitle.includes(title)) {
3322
+ if (i === 29) {
3323
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
3324
+ }
3325
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3326
+ continue;
3327
+ }
3328
+ }
3329
+ await _screenshot(state, this);
3330
+ return state.info;
3331
+ }
3332
+ }
3333
+ catch (e) {
3334
+ state.info.failCause.lastError = e.message;
3335
+ state.info.failCause.assertionFailed = true;
3336
+ await _commandError(state, e, this);
3337
+ }
3338
+ finally {
3339
+ await _commandFinally(state, this);
3340
+ }
3341
+ }
3342
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
3343
+ const frames = this.page.frames();
3344
+ let results = [];
3345
+ // let ignoreCase = false;
3346
+ for (let i = 0; i < frames.length; i++) {
3347
+ if (dateAlternatives.date) {
3348
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
3349
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3350
+ result.frame = frames[i];
3351
+ results.push(result);
3352
+ }
3353
+ }
3354
+ else if (numberAlternatives.number) {
3355
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
3356
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3357
+ result.frame = frames[i];
3358
+ results.push(result);
3359
+ }
3360
+ }
3361
+ else {
3362
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
3363
+ result.frame = frames[i];
3364
+ results.push(result);
3365
+ }
2046
3366
  }
3367
+ state.info.results = results;
3368
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3369
+ return resultWithElementsFound;
2047
3370
  }
2048
3371
  async verifyTextExistInPage(text, options = {}, world = null) {
2049
3372
  text = unEscapeString(text);
@@ -2054,13 +3377,26 @@ class StableBrowser {
2054
3377
  locate: false,
2055
3378
  scroll: false,
2056
3379
  highlight: false,
2057
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2058
- text: `Verify text exists in page`,
3380
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
3381
+ text: `Verify the text '${maskValue(text)}' exists in page`,
3382
+ _text: `Verify the text '${text}' exists in page`,
2059
3383
  operation: "verifyTextExistInPage",
2060
3384
  log: "***** verify text " + text + " exists in page *****\n",
2061
3385
  };
2062
- const timeout = this._getLoadTimeout(options);
2063
- await new Promise((resolve) => setTimeout(resolve, 2000));
3386
+ if (testForRegex(text)) {
3387
+ text = text.replace(/\\"/g, '"');
3388
+ }
3389
+ const timeout = this._getFindElementTimeout(options);
3390
+ //if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
3391
+ let stepFastMode = this.stepTags.includes("fast-mode");
3392
+ if (!stepFastMode) {
3393
+ if (!this.fastMode) {
3394
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3395
+ }
3396
+ else {
3397
+ await new Promise((resolve) => setTimeout(resolve, 500));
3398
+ }
3399
+ }
2064
3400
  const newValue = await this._replaceWithLocalData(text, world);
2065
3401
  if (newValue !== text) {
2066
3402
  this.logger.info(text + "=" + newValue);
@@ -2068,35 +3404,24 @@ class StableBrowser {
2068
3404
  }
2069
3405
  let dateAlternatives = findDateAlternatives(text);
2070
3406
  let numberAlternatives = findNumberAlternatives(text);
3407
+ if (stepFastMode) {
3408
+ state.onlyFailuresScreenshot = true;
3409
+ state.scroll = false;
3410
+ state.highlight = false;
3411
+ }
2071
3412
  try {
2072
3413
  await _preCommand(state, this);
2073
3414
  state.info.text = text;
2074
3415
  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
- }
3416
+ let resultWithElementsFound = {
3417
+ length: 0,
3418
+ };
3419
+ try {
3420
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3421
+ }
3422
+ catch (error) {
3423
+ // ignore
2097
3424
  }
2098
- state.info.results = results;
2099
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2100
3425
  if (resultWithElementsFound.length === 0) {
2101
3426
  if (Date.now() - state.startTime > timeout) {
2102
3427
  throw new Error(`Text ${text} not found in page`);
@@ -2104,26 +3429,30 @@ class StableBrowser {
2104
3429
  await new Promise((resolve) => setTimeout(resolve, 1000));
2105
3430
  continue;
2106
3431
  }
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");
3432
+ try {
3433
+ if (resultWithElementsFound[0].randomToken) {
3434
+ const frame = resultWithElementsFound[0].frame;
3435
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
3436
+ await this._highlightElements(frame, dataAttribute);
3437
+ const element = await frame.locator(dataAttribute).first();
3438
+ if (element) {
3439
+ await this.scrollIfNeeded(element, state.info);
3440
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3441
+ }
2115
3442
  }
3443
+ await _screenshot(state, this);
3444
+ return state.info;
3445
+ }
3446
+ catch (error) {
3447
+ console.error(error);
2116
3448
  }
2117
- await _screenshot(state, this);
2118
- return state.info;
2119
3449
  }
2120
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2121
3450
  }
2122
3451
  catch (e) {
2123
3452
  await _commandError(state, e, this);
2124
3453
  }
2125
3454
  finally {
2126
- _commandFinally(state, this);
3455
+ await _commandFinally(state, this);
2127
3456
  }
2128
3457
  }
2129
3458
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2136,11 +3465,15 @@ class StableBrowser {
2136
3465
  scroll: false,
2137
3466
  highlight: false,
2138
3467
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2139
- text: `Verify text does not exist in page`,
3468
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
3469
+ _text: `Verify the text '${text}' does not exist in page`,
2140
3470
  operation: "verifyTextNotExistInPage",
2141
3471
  log: "***** verify text " + text + " does not exist in page *****\n",
2142
3472
  };
2143
- const timeout = this._getLoadTimeout(options);
3473
+ if (testForRegex(text)) {
3474
+ text = text.replace(/\\"/g, '"');
3475
+ }
3476
+ const timeout = this._getFindElementTimeout(options);
2144
3477
  await new Promise((resolve) => setTimeout(resolve, 2000));
2145
3478
  const newValue = await this._replaceWithLocalData(text, world);
2146
3479
  if (newValue !== text) {
@@ -2152,32 +3485,16 @@ class StableBrowser {
2152
3485
  try {
2153
3486
  await _preCommand(state, this);
2154
3487
  state.info.text = text;
3488
+ let resultWithElementsFound = {
3489
+ length: null, // initial cannot be 0
3490
+ };
2155
3491
  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
- }
3492
+ try {
3493
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3494
+ }
3495
+ catch (error) {
3496
+ // ignore
2178
3497
  }
2179
- state.info.results = results;
2180
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2181
3498
  if (resultWithElementsFound.length === 0) {
2182
3499
  await _screenshot(state, this);
2183
3500
  return state.info;
@@ -2192,18 +3509,158 @@ class StableBrowser {
2192
3509
  await _commandError(state, e, this);
2193
3510
  }
2194
3511
  finally {
2195
- _commandFinally(state, this);
3512
+ await _commandFinally(state, this);
2196
3513
  }
2197
3514
  }
2198
- _getServerUrl() {
2199
- let serviceUrl = "https://api.blinq.io";
2200
- if (process.env.NODE_ENV_BLINQ === "dev") {
2201
- serviceUrl = "https://dev.api.blinq.io";
3515
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
3516
+ textAnchor = unEscapeString(textAnchor);
3517
+ textToVerify = unEscapeString(textToVerify);
3518
+ const state = {
3519
+ text_search: textToVerify,
3520
+ options,
3521
+ world,
3522
+ locate: false,
3523
+ scroll: false,
3524
+ highlight: false,
3525
+ type: Types.VERIFY_TEXT_WITH_RELATION,
3526
+ text: `Verify text with relation to another text`,
3527
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
3528
+ operation: "verify_text_with_relation",
3529
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3530
+ };
3531
+ const cmdStartTime = Date.now();
3532
+ let cmdEndTime = null;
3533
+ const timeout = this._getFindElementTimeout(options);
3534
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3535
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
3536
+ if (newValue !== textAnchor) {
3537
+ this.logger.info(textAnchor + "=" + newValue);
3538
+ textAnchor = newValue;
3539
+ }
3540
+ newValue = await this._replaceWithLocalData(textToVerify, world);
3541
+ if (newValue !== textToVerify) {
3542
+ this.logger.info(textToVerify + "=" + newValue);
3543
+ textToVerify = newValue;
3544
+ }
3545
+ let dateAlternatives = findDateAlternatives(textToVerify);
3546
+ let numberAlternatives = findNumberAlternatives(textToVerify);
3547
+ let foundAncore = false;
3548
+ try {
3549
+ await _preCommand(state, this);
3550
+ state.info.text = textToVerify;
3551
+ let resultWithElementsFound = {
3552
+ length: 0,
3553
+ };
3554
+ while (true) {
3555
+ try {
3556
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
3557
+ }
3558
+ catch (error) {
3559
+ // ignore
3560
+ }
3561
+ if (resultWithElementsFound.length === 0) {
3562
+ if (Date.now() - state.startTime > timeout) {
3563
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
3564
+ }
3565
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3566
+ continue;
3567
+ }
3568
+ else {
3569
+ cmdEndTime = Date.now();
3570
+ if (cmdEndTime - cmdStartTime > 55000) {
3571
+ if (foundAncore) {
3572
+ throw new Error(`Text ${textToVerify} not found in page`);
3573
+ }
3574
+ else {
3575
+ throw new Error(`Text ${textAnchor} not found in page`);
3576
+ }
3577
+ }
3578
+ }
3579
+ try {
3580
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
3581
+ foundAncore = true;
3582
+ const result = resultWithElementsFound[i];
3583
+ const token = result.randomToken;
3584
+ const frame = result.frame;
3585
+ let css = `[data-blinq-id-${token}]`;
3586
+ const climbArray1 = [];
3587
+ for (let i = 0; i < climb; i++) {
3588
+ climbArray1.push("..");
3589
+ }
3590
+ let climbXpath = "xpath=" + climbArray1.join("/");
3591
+ if (Number(climb) > 0) {
3592
+ css = css + " >> " + climbXpath;
3593
+ }
3594
+ const count = await frame.locator(css).count();
3595
+ for (let j = 0; j < count; j++) {
3596
+ const continer = await frame.locator(css).nth(j);
3597
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
3598
+ if (result.elementCount > 0) {
3599
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
3600
+ await this._highlightElements(frame, dataAttribute);
3601
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
3602
+ // if (world && world.screenshot && !world.screenshotPath) {
3603
+ // console.log(`Highlighting for vtrt while running from recorder`);
3604
+ // this._highlightElements(frame, dataAttribute)
3605
+ // .then(async () => {
3606
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
3607
+ // this._unhighlightElements(frame, dataAttribute).then(
3608
+ // () => {}
3609
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
3610
+ // );
3611
+ // })
3612
+ // .catch(e);
3613
+ // }
3614
+ //await this._highlightElements(frame, cssAnchor);
3615
+ const element = await frame.locator(dataAttribute).first();
3616
+ // await new Promise((resolve) => setTimeout(resolve, 100));
3617
+ // await this._unhighlightElements(frame, dataAttribute);
3618
+ if (element) {
3619
+ await this.scrollIfNeeded(element, state.info);
3620
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3621
+ }
3622
+ await _screenshot(state, this);
3623
+ return state.info;
3624
+ }
3625
+ }
3626
+ }
3627
+ }
3628
+ catch (error) {
3629
+ console.error(error);
3630
+ }
3631
+ }
3632
+ // await expect(element).toHaveCount(1, { timeout: 10000 });
3633
+ }
3634
+ catch (e) {
3635
+ await _commandError(state, e, this);
2202
3636
  }
2203
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2204
- serviceUrl = "https://stage.api.blinq.io";
3637
+ finally {
3638
+ await _commandFinally(state, this);
3639
+ }
3640
+ }
3641
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
3642
+ const frames = this.page.frames();
3643
+ let results = [];
3644
+ let ignoreCase = false;
3645
+ for (let i = 0; i < frames.length; i++) {
3646
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
3647
+ result.frame = frames[i];
3648
+ const climbArray = [];
3649
+ for (let i = 0; i < climb; i++) {
3650
+ climbArray.push("..");
3651
+ }
3652
+ let climbXpath = "xpath=" + climbArray.join("/");
3653
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
3654
+ const count = await frames[i].locator(newLocator).count();
3655
+ if (count > 0) {
3656
+ result.elementCount = count;
3657
+ result.locator = newLocator;
3658
+ results.push(result);
3659
+ }
2205
3660
  }
2206
- return serviceUrl;
3661
+ // state.info.results = results;
3662
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3663
+ return resultWithElementsFound;
2207
3664
  }
2208
3665
  async visualVerification(text, options = {}, world = null) {
2209
3666
  const startTime = Date.now();
@@ -2219,14 +3676,17 @@ class StableBrowser {
2219
3676
  throw new Error("TOKEN is not set");
2220
3677
  }
2221
3678
  try {
2222
- let serviceUrl = this._getServerUrl();
3679
+ let serviceUrl = _getServerUrl();
2223
3680
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2224
3681
  info.screenshotPath = screenshotPath;
2225
3682
  const screenshot = await this.takeScreenshot();
2226
- const request = {
2227
- method: "POST",
3683
+ let request = {
3684
+ method: "post",
3685
+ maxBodyLength: Infinity,
2228
3686
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2229
3687
  headers: {
3688
+ "x-bvt-project-id": path.basename(this.project_path),
3689
+ "x-source": "aaa",
2230
3690
  "Content-Type": "application/json",
2231
3691
  Authorization: `Bearer ${process.env.TOKEN}`,
2232
3692
  },
@@ -2235,7 +3695,7 @@ class StableBrowser {
2235
3695
  screenshot: screenshot,
2236
3696
  }),
2237
3697
  };
2238
- let result = await this.context.api.request(request);
3698
+ const result = await axios.request(request);
2239
3699
  if (result.data.status !== true) {
2240
3700
  throw new Error("Visual validation failed");
2241
3701
  }
@@ -2256,13 +3716,14 @@ class StableBrowser {
2256
3716
  Object.assign(e, { info: info });
2257
3717
  error = e;
2258
3718
  // throw e;
2259
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3719
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
2260
3720
  }
2261
3721
  finally {
2262
3722
  const endTime = Date.now();
2263
- this._reportToWorld(world, {
3723
+ _reportToWorld(world, {
2264
3724
  type: Types.VERIFY_VISUAL,
2265
3725
  text: "Visual verification",
3726
+ _text: "Visual verification of " + text,
2266
3727
  screenshotId,
2267
3728
  result: error
2268
3729
  ? {
@@ -2308,6 +3769,7 @@ class StableBrowser {
2308
3769
  let screenshotPath = null;
2309
3770
  const info = {};
2310
3771
  info.log = "";
3772
+ info.locatorLog = new LocatorLog(selectors);
2311
3773
  info.operation = "getTableData";
2312
3774
  info.selectors = selectors;
2313
3775
  try {
@@ -2328,7 +3790,7 @@ class StableBrowser {
2328
3790
  }
2329
3791
  finally {
2330
3792
  const endTime = Date.now();
2331
- this._reportToWorld(world, {
3793
+ _reportToWorld(world, {
2332
3794
  element_name: selectors.element_name,
2333
3795
  type: Types.GET_TABLE_DATA,
2334
3796
  text: "Get table data",
@@ -2383,7 +3845,7 @@ class StableBrowser {
2383
3845
  info.operation = "analyzeTable";
2384
3846
  info.selectors = selectors;
2385
3847
  info.query = query;
2386
- query = this._fixUsingParams(query, _params);
3848
+ query = _fixUsingParams(query, _params);
2387
3849
  info.query_fixed = query;
2388
3850
  info.operator = operator;
2389
3851
  info.value = value;
@@ -2494,7 +3956,7 @@ class StableBrowser {
2494
3956
  }
2495
3957
  finally {
2496
3958
  const endTime = Date.now();
2497
- this._reportToWorld(world, {
3959
+ _reportToWorld(world, {
2498
3960
  element_name: selectors.element_name,
2499
3961
  type: Types.ANALYZE_TABLE,
2500
3962
  text: "Analyze table",
@@ -2515,8 +3977,51 @@ class StableBrowser {
2515
3977
  });
2516
3978
  }
2517
3979
  }
3980
+ /**
3981
+ * Explicit wait/sleep function that pauses execution for a specified duration
3982
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3983
+ * @param options - Optional configuration object
3984
+ * @param world - Optional world context
3985
+ * @returns Promise that resolves after the specified duration
3986
+ */
3987
+ async sleep(duration = 1000, options = {}, world = null) {
3988
+ const state = {
3989
+ duration,
3990
+ options,
3991
+ world,
3992
+ locate: false,
3993
+ scroll: false,
3994
+ screenshot: false,
3995
+ highlight: false,
3996
+ type: Types.SLEEP,
3997
+ text: `Sleep for ${duration} ms`,
3998
+ _text: `Sleep for ${duration} ms`,
3999
+ operation: "sleep",
4000
+ log: `***** Sleep for ${duration} ms *****\n`,
4001
+ };
4002
+ try {
4003
+ await _preCommand(state, this);
4004
+ if (duration < 0) {
4005
+ throw new Error("Sleep duration cannot be negative");
4006
+ }
4007
+ await new Promise((resolve) => setTimeout(resolve, duration));
4008
+ return state.info;
4009
+ }
4010
+ catch (e) {
4011
+ await _commandError(state, e, this);
4012
+ }
4013
+ finally {
4014
+ await _commandFinally(state, this);
4015
+ }
4016
+ }
2518
4017
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2519
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
4018
+ try {
4019
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
4020
+ }
4021
+ catch (error) {
4022
+ this.logger.debug(error);
4023
+ throw error;
4024
+ }
2520
4025
  }
2521
4026
  _getLoadTimeout(options) {
2522
4027
  let timeout = 15000;
@@ -2528,7 +4033,54 @@ class StableBrowser {
2528
4033
  }
2529
4034
  return timeout;
2530
4035
  }
4036
+ _getFindElementTimeout(options) {
4037
+ if (options && options.timeout) {
4038
+ return options.timeout;
4039
+ }
4040
+ if (this.configuration.find_element_timeout) {
4041
+ return this.configuration.find_element_timeout;
4042
+ }
4043
+ return 30000;
4044
+ }
4045
+ async saveStoreState(path = null, world = null) {
4046
+ const storageState = await this.page.context().storageState();
4047
+ path = await this._replaceWithLocalData(path, this.world);
4048
+ //const testDataFile = _getDataFile(world, this.context, this);
4049
+ if (path) {
4050
+ // save { storageState: storageState } into the path
4051
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
4052
+ }
4053
+ else {
4054
+ await this.setTestData({ storageState: storageState }, world);
4055
+ }
4056
+ }
4057
+ async restoreSaveState(path = null, world = null) {
4058
+ path = await this._replaceWithLocalData(path, this.world);
4059
+ await refreshBrowser(this, path, world);
4060
+ this.registerEventListeners(this.context);
4061
+ registerNetworkEvents(this.world, this, this.context, this.page);
4062
+ registerDownloadEvent(this.page, this.world, this.context);
4063
+ if (this.onRestoreSaveState) {
4064
+ await this.onRestoreSaveState(path);
4065
+ }
4066
+ }
2531
4067
  async waitForPageLoad(options = {}, world = null) {
4068
+ // try {
4069
+ // let currentPagePath = null;
4070
+ // currentPagePath = new URL(this.page.url()).pathname;
4071
+ // if (this.latestPagePath) {
4072
+ // // get the currect page path and compare with the latest page path
4073
+ // if (this.latestPagePath === currentPagePath) {
4074
+ // // if the page path is the same, do not wait for page load
4075
+ // console.log("No page change: " + currentPagePath);
4076
+ // return;
4077
+ // }
4078
+ // }
4079
+ // this.latestPagePath = currentPagePath;
4080
+ // } catch (e) {
4081
+ // console.debug("Error getting current page path: ", e);
4082
+ // }
4083
+ //console.log("Waiting for page load");
2532
4084
  let timeout = this._getLoadTimeout(options);
2533
4085
  const promiseArray = [];
2534
4086
  // let waitForNetworkIdle = true;
@@ -2561,13 +4113,15 @@ class StableBrowser {
2561
4113
  else if (e.label === "domcontentloaded") {
2562
4114
  console.log("waited for the domcontent loaded timeout");
2563
4115
  }
2564
- console.log(".");
2565
4116
  }
2566
4117
  finally {
2567
- await new Promise((resolve) => setTimeout(resolve, 2000));
4118
+ await new Promise((resolve) => setTimeout(resolve, 500));
4119
+ if (options && !options.noSleep) {
4120
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4121
+ }
2568
4122
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2569
4123
  const endTime = Date.now();
2570
- this._reportToWorld(world, {
4124
+ _reportToWorld(world, {
2571
4125
  type: Types.GET_PAGE_STATUS,
2572
4126
  text: "Wait for page load",
2573
4127
  screenshotId,
@@ -2595,21 +4149,134 @@ class StableBrowser {
2595
4149
  highlight: false,
2596
4150
  type: Types.CLOSE_PAGE,
2597
4151
  text: `Close page`,
4152
+ _text: `Close the page`,
2598
4153
  operation: "closePage",
2599
4154
  log: "***** close page *****\n",
2600
- throwError: false,
4155
+ // throwError: false,
2601
4156
  };
2602
4157
  try {
2603
4158
  await _preCommand(state, this);
2604
4159
  await this.page.close();
2605
4160
  }
2606
4161
  catch (e) {
2607
- console.log(".");
2608
4162
  await _commandError(state, e, this);
2609
4163
  }
2610
4164
  finally {
2611
- _commandFinally(state, this);
4165
+ await _commandFinally(state, this);
4166
+ }
4167
+ }
4168
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
4169
+ let operation = null;
4170
+ if (!options || !options.operation) {
4171
+ throw new Error("operation is not defined");
4172
+ }
4173
+ operation = options.operation;
4174
+ // validate operation is one of the supported operations
4175
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
4176
+ throw new Error("operation is not supported");
4177
+ }
4178
+ const state = {
4179
+ options,
4180
+ world,
4181
+ locate: false,
4182
+ scroll: false,
4183
+ highlight: false,
4184
+ type: Types.TABLE_OPERATION,
4185
+ text: `Table operation`,
4186
+ _text: `Table ${operation} operation`,
4187
+ operation: operation,
4188
+ log: "***** Table operation *****\n",
4189
+ };
4190
+ const timeout = this._getFindElementTimeout(options);
4191
+ try {
4192
+ await _preCommand(state, this);
4193
+ const start = Date.now();
4194
+ let cellArea = null;
4195
+ while (true) {
4196
+ try {
4197
+ cellArea = await _findCellArea(headerText, rowText, this, state);
4198
+ if (cellArea) {
4199
+ break;
4200
+ }
4201
+ }
4202
+ catch (e) {
4203
+ // ignore
4204
+ }
4205
+ if (Date.now() - start > timeout) {
4206
+ throw new Error(`Cell not found in table`);
4207
+ }
4208
+ await new Promise((resolve) => setTimeout(resolve, 1000));
4209
+ }
4210
+ switch (operation) {
4211
+ case "click":
4212
+ if (!options.css) {
4213
+ // will click in the center of the cell
4214
+ let xOffset = 0;
4215
+ let yOffset = 0;
4216
+ if (options.xOffset) {
4217
+ xOffset = options.xOffset;
4218
+ }
4219
+ if (options.yOffset) {
4220
+ yOffset = options.yOffset;
4221
+ }
4222
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
4223
+ }
4224
+ else {
4225
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4226
+ if (results.length === 0) {
4227
+ throw new Error(`Element not found in cell area`);
4228
+ }
4229
+ state.element = results[0];
4230
+ await performAction("click", state.element, options, this, state, _params);
4231
+ }
4232
+ break;
4233
+ case "hover+click":
4234
+ if (!options.css) {
4235
+ throw new Error("css is not defined");
4236
+ }
4237
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4238
+ if (results.length === 0) {
4239
+ throw new Error(`Element not found in cell area`);
4240
+ }
4241
+ state.element = results[0];
4242
+ await performAction("hover+click", state.element, options, this, state, _params);
4243
+ break;
4244
+ case "hover":
4245
+ if (!options.css) {
4246
+ throw new Error("css is not defined");
4247
+ }
4248
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4249
+ if (result1.length === 0) {
4250
+ throw new Error(`Element not found in cell area`);
4251
+ }
4252
+ state.element = result1[0];
4253
+ await performAction("hover", state.element, options, this, state, _params);
4254
+ break;
4255
+ default:
4256
+ throw new Error("operation is not supported");
4257
+ }
4258
+ }
4259
+ catch (e) {
4260
+ await _commandError(state, e, this);
4261
+ }
4262
+ finally {
4263
+ await _commandFinally(state, this);
4264
+ }
4265
+ }
4266
+ saveTestDataAsGlobal(options, world) {
4267
+ const dataFile = _getDataFile(world, this.context, this);
4268
+ if (process.env.MODE === "executions") {
4269
+ const globalDataFile = path.join(this.project_path, "global_test_data.json");
4270
+ const dataFileContents = fs.existsSync(dataFile) ? JSON.parse(fs.readFileSync(dataFile)) : {};
4271
+ const globalDataFileContents = fs.existsSync(globalDataFile) ? JSON.parse(fs.readFileSync(globalDataFile)) : {};
4272
+ const mergedData = JSON.stringify(_.merge({}, dataFileContents, globalDataFileContents), null, 2);
4273
+ fs.writeFileSync(dataFile, mergedData);
4274
+ fs.writeFileSync(globalDataFile, mergedData);
4275
+ this.logger.info("Save the scenario test data to " + dataFile + " as global for the following scenarios.");
4276
+ return;
2612
4277
  }
4278
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
4279
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
2613
4280
  }
2614
4281
  async setViewportSize(width, hight, options = {}, world = null) {
2615
4282
  const startTime = Date.now();
@@ -2627,16 +4294,16 @@ class StableBrowser {
2627
4294
  await this.page.setViewportSize({ width: width, height: hight });
2628
4295
  }
2629
4296
  catch (e) {
2630
- console.log(".");
2631
4297
  await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2632
4298
  }
2633
4299
  finally {
2634
4300
  await new Promise((resolve) => setTimeout(resolve, 2000));
2635
4301
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2636
4302
  const endTime = Date.now();
2637
- this._reportToWorld(world, {
4303
+ _reportToWorld(world, {
2638
4304
  type: Types.SET_VIEWPORT,
2639
4305
  text: "set viewport size to " + width + "x" + hight,
4306
+ _text: "Set the viewport size to " + width + "x" + hight,
2640
4307
  screenshotId,
2641
4308
  result: error
2642
4309
  ? {
@@ -2664,14 +4331,13 @@ class StableBrowser {
2664
4331
  await this.page.reload();
2665
4332
  }
2666
4333
  catch (e) {
2667
- console.log(".");
2668
4334
  await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2669
4335
  }
2670
4336
  finally {
2671
4337
  await new Promise((resolve) => setTimeout(resolve, 2000));
2672
4338
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2673
4339
  const endTime = Date.now();
2674
- this._reportToWorld(world, {
4340
+ _reportToWorld(world, {
2675
4341
  type: Types.GET_PAGE_STATUS,
2676
4342
  text: "page relaod",
2677
4343
  screenshotId,
@@ -2707,36 +4373,276 @@ class StableBrowser {
2707
4373
  console.log("#-#");
2708
4374
  }
2709
4375
  }
2710
- _reportToWorld(world, properties) {
2711
- if (!world || !world.attach) {
2712
- return;
4376
+ async beforeScenario(world, scenario) {
4377
+ if (world && world.attach) {
4378
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4379
+ }
4380
+ this.context.loadedRoutes = null;
4381
+ this.beforeScenarioCalled = true;
4382
+ if (scenario && scenario.pickle && scenario.pickle.name) {
4383
+ this.scenarioName = scenario.pickle.name;
4384
+ }
4385
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
4386
+ this.featureName = scenario.gherkinDocument.feature.name;
4387
+ }
4388
+ if (this.context) {
4389
+ this.context.examplesRow = extractStepExampleParameters(scenario);
4390
+ }
4391
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
4392
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
4393
+ // check if @global_test_data tag is present
4394
+ if (this.tags.includes("@global_test_data" /* TAG_CONSTANTS.GLOBAL_TEST_DATA */)) {
4395
+ this.saveTestDataAsGlobal({}, world);
4396
+ }
4397
+ if (this.tags.includes("@fast_mode" /* TAG_CONSTANTS.FAST_MODE */)) {
4398
+ this.fastMode = true;
4399
+ }
4400
+ }
4401
+ // update test data based on feature/scenario
4402
+ let envName = null;
4403
+ if (this.context && this.context.environment) {
4404
+ envName = this.context.environment.name;
4405
+ }
4406
+ if (!process.env.TEMP_RUN) {
4407
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
4408
+ }
4409
+ await loadBrunoParams(this.context, this.context.environment.name);
4410
+ if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
4411
+ this.trace = true;
4412
+ const traceFolder = path.join(this.context.reportFolder, "trace");
4413
+ if (!fs.existsSync(traceFolder)) {
4414
+ fs.mkdirSync(traceFolder, { recursive: true });
4415
+ }
4416
+ this.traceFolder = traceFolder;
4417
+ await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
4418
+ }
4419
+ }
4420
+ async afterScenario(world, scenario) {
4421
+ const id = scenario.testCaseStartedId;
4422
+ if (this.trace) {
4423
+ await this.context.playContext.tracing.stop({
4424
+ path: path.join(this.traceFolder, `trace-${id}.zip`),
4425
+ });
4426
+ }
4427
+ }
4428
+ getGherkinKeyword(step) {
4429
+ if (!step?.type) {
4430
+ return "";
4431
+ }
4432
+ switch (step.type) {
4433
+ case "Context":
4434
+ return "Given";
4435
+ case "Action":
4436
+ return "When";
4437
+ case "Outcome":
4438
+ return "Then";
4439
+ case "Conjunction":
4440
+ return "And";
4441
+ default:
4442
+ return "";
2713
4443
  }
2714
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2715
4444
  }
2716
4445
  async beforeStep(world, step) {
2717
- this.stepName = step.pickleStep.text;
2718
- this.logger.info("step: " + this.stepName);
4446
+ if (step?.pickleStep && this.trace) {
4447
+ const keyword = this.getGherkinKeyword(step.pickleStep);
4448
+ this.traceGroupName = `${keyword} ${step.pickleStep.text}`;
4449
+ await this.context.playContext.tracing.group(this.traceGroupName);
4450
+ }
4451
+ this.stepTags = [];
4452
+ if (!this.beforeScenarioCalled) {
4453
+ this.beforeScenario(world, step);
4454
+ this.context.loadedRoutes = null;
4455
+ }
2719
4456
  if (this.stepIndex === undefined) {
2720
4457
  this.stepIndex = 0;
2721
4458
  }
2722
4459
  else {
2723
4460
  this.stepIndex++;
2724
4461
  }
4462
+ if (step && step.pickleStep && step.pickleStep.text) {
4463
+ this.stepName = step.pickleStep.text;
4464
+ let printableStepName = this.stepName;
4465
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4466
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4467
+ return `\x1b[33m"${p1}"\x1b[0m`;
4468
+ });
4469
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4470
+ }
4471
+ else if (step && step.text) {
4472
+ this.stepName = step.text;
4473
+ }
4474
+ else {
4475
+ this.stepName = "step " + this.stepIndex;
4476
+ }
2725
4477
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2726
4478
  if (this.context.browserObject.context) {
2727
4479
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
2728
4480
  }
2729
4481
  }
4482
+ if (this.initSnapshotTaken === false) {
4483
+ this.initSnapshotTaken = true;
4484
+ if (world &&
4485
+ world.attach &&
4486
+ !process.env.DISABLE_SNAPSHOT &&
4487
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4488
+ const snapshot = await this.getAriaSnapshot();
4489
+ if (snapshot) {
4490
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
4491
+ }
4492
+ }
4493
+ }
4494
+ this.context.routeResults = null;
4495
+ this.context.loadedRoutes = null;
4496
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4497
+ networkBeforeStep(this.stepName, this.context);
4498
+ this.inStepReport = false;
4499
+ }
4500
+ setStepTags(tags) {
4501
+ this.stepTags = tags;
4502
+ }
4503
+ async getAriaSnapshot() {
4504
+ try {
4505
+ // find the page url
4506
+ const url = await this.page.url();
4507
+ // extract the path from the url
4508
+ const path = new URL(url).pathname;
4509
+ // get the page title
4510
+ const title = await this.page.title();
4511
+ // go over other frams
4512
+ const frames = this.page.frames();
4513
+ const snapshots = [];
4514
+ const content = [`- path: ${path}`, `- title: ${title}`];
4515
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
4516
+ for (let i = 0; i < frames.length; i++) {
4517
+ const frame = frames[i];
4518
+ try {
4519
+ // Ensure frame is attached and has body
4520
+ const body = frame.locator("body");
4521
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4522
+ const snapshot = await body.ariaSnapshot({ timeout });
4523
+ if (!snapshot) {
4524
+ continue;
4525
+ }
4526
+ content.push(`- frame: ${i}`);
4527
+ content.push(snapshot);
4528
+ }
4529
+ catch (innerErr) {
4530
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4531
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4532
+ }
4533
+ }
4534
+ return content.join("\n");
4535
+ }
4536
+ catch (e) {
4537
+ console.log("Error in getAriaSnapshot");
4538
+ //console.debug(e);
4539
+ }
4540
+ return null;
4541
+ }
4542
+ /**
4543
+ * Sends command with custom payload to report.
4544
+ * @param commandText - Title of the command to be shown in the report.
4545
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
4546
+ * @param content - Content of the command to be shown in the report.
4547
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
4548
+ * @param world - Optional world context.
4549
+ * @public
4550
+ */
4551
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
4552
+ const state = {
4553
+ options,
4554
+ world,
4555
+ locate: false,
4556
+ scroll: false,
4557
+ screenshot: options.screenshot ?? false,
4558
+ highlight: options.highlight ?? false,
4559
+ type: Types.REPORT_COMMAND,
4560
+ text: commandText,
4561
+ _text: commandText,
4562
+ operation: "report_command",
4563
+ log: "***** " + commandText + " *****\n",
4564
+ };
4565
+ try {
4566
+ await _preCommand(state, this);
4567
+ const payload = {
4568
+ type: options.type ?? "text",
4569
+ content: content,
4570
+ screenshotId: null,
4571
+ };
4572
+ state.payload = payload;
4573
+ if (commandStatus === "FAILED") {
4574
+ state.throwError = true;
4575
+ throw new Error(commandText);
4576
+ }
4577
+ }
4578
+ catch (e) {
4579
+ await _commandError(state, e, this);
4580
+ }
4581
+ finally {
4582
+ await _commandFinally(state, this);
4583
+ }
2730
4584
  }
2731
- async afterStep(world, step) {
4585
+ async afterStep(world, step, result) {
2732
4586
  this.stepName = null;
2733
- if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2734
- if (this.context.browserObject.context) {
2735
- await this.context.browserObject.context.tracing.stopChunk({
2736
- path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
2737
- });
4587
+ if (this.context) {
4588
+ this.context.examplesRow = null;
4589
+ }
4590
+ if (!this.inStepReport) {
4591
+ // check the step result
4592
+ if (result && result.status === "FAILED" && world && world.attach) {
4593
+ await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
4594
+ }
4595
+ }
4596
+ if (world &&
4597
+ world.attach &&
4598
+ !process.env.DISABLE_SNAPSHOT &&
4599
+ !this.fastMode &&
4600
+ !this.stepTags.includes("fast-mode")) {
4601
+ const snapshot = await this.getAriaSnapshot();
4602
+ if (snapshot) {
4603
+ const obj = {};
4604
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4605
+ }
4606
+ }
4607
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4608
+ if (this.context.routeResults) {
4609
+ if (world && world.attach) {
4610
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4611
+ }
4612
+ }
4613
+ if (!process.env.TEMP_RUN) {
4614
+ const state = {
4615
+ world,
4616
+ locate: false,
4617
+ scroll: false,
4618
+ screenshot: true,
4619
+ highlight: true,
4620
+ type: Types.STEP_COMPLETE,
4621
+ text: "end of scenario",
4622
+ _text: "end of scenario",
4623
+ operation: "step_complete",
4624
+ log: "***** " + "end of scenario" + " *****\n",
4625
+ };
4626
+ try {
4627
+ await _preCommand(state, this);
4628
+ }
4629
+ catch (e) {
4630
+ await _commandError(state, e, this);
4631
+ }
4632
+ finally {
4633
+ await _commandFinally(state, this);
2738
4634
  }
2739
4635
  }
4636
+ networkAfterStep(this.stepName, this.context);
4637
+ if (process.env.TEMP_RUN === "true") {
4638
+ // Put a sleep for some time to allow the browser to finish processing
4639
+ if (!this.stepTags.includes("fast-mode")) {
4640
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4641
+ }
4642
+ }
4643
+ if (this.trace) {
4644
+ await this.context.playContext.tracing.groupEnd();
4645
+ }
2740
4646
  }
2741
4647
  }
2742
4648
  function createTimedPromise(promise, label) {
@@ -2744,156 +4650,5 @@ function createTimedPromise(promise, label) {
2744
4650
  .then((result) => ({ status: "fulfilled", label, result }))
2745
4651
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2746
4652
  }
2747
- const KEYBOARD_EVENTS = [
2748
- "ALT",
2749
- "AltGraph",
2750
- "CapsLock",
2751
- "Control",
2752
- "Fn",
2753
- "FnLock",
2754
- "Hyper",
2755
- "Meta",
2756
- "NumLock",
2757
- "ScrollLock",
2758
- "Shift",
2759
- "Super",
2760
- "Symbol",
2761
- "SymbolLock",
2762
- "Enter",
2763
- "Tab",
2764
- "ArrowDown",
2765
- "ArrowLeft",
2766
- "ArrowRight",
2767
- "ArrowUp",
2768
- "End",
2769
- "Home",
2770
- "PageDown",
2771
- "PageUp",
2772
- "Backspace",
2773
- "Clear",
2774
- "Copy",
2775
- "CrSel",
2776
- "Cut",
2777
- "Delete",
2778
- "EraseEof",
2779
- "ExSel",
2780
- "Insert",
2781
- "Paste",
2782
- "Redo",
2783
- "Undo",
2784
- "Accept",
2785
- "Again",
2786
- "Attn",
2787
- "Cancel",
2788
- "ContextMenu",
2789
- "Escape",
2790
- "Execute",
2791
- "Find",
2792
- "Finish",
2793
- "Help",
2794
- "Pause",
2795
- "Play",
2796
- "Props",
2797
- "Select",
2798
- "ZoomIn",
2799
- "ZoomOut",
2800
- "BrightnessDown",
2801
- "BrightnessUp",
2802
- "Eject",
2803
- "LogOff",
2804
- "Power",
2805
- "PowerOff",
2806
- "PrintScreen",
2807
- "Hibernate",
2808
- "Standby",
2809
- "WakeUp",
2810
- "AllCandidates",
2811
- "Alphanumeric",
2812
- "CodeInput",
2813
- "Compose",
2814
- "Convert",
2815
- "Dead",
2816
- "FinalMode",
2817
- "GroupFirst",
2818
- "GroupLast",
2819
- "GroupNext",
2820
- "GroupPrevious",
2821
- "ModeChange",
2822
- "NextCandidate",
2823
- "NonConvert",
2824
- "PreviousCandidate",
2825
- "Process",
2826
- "SingleCandidate",
2827
- "HangulMode",
2828
- "HanjaMode",
2829
- "JunjaMode",
2830
- "Eisu",
2831
- "Hankaku",
2832
- "Hiragana",
2833
- "HiraganaKatakana",
2834
- "KanaMode",
2835
- "KanjiMode",
2836
- "Katakana",
2837
- "Romaji",
2838
- "Zenkaku",
2839
- "ZenkakuHanaku",
2840
- "F1",
2841
- "F2",
2842
- "F3",
2843
- "F4",
2844
- "F5",
2845
- "F6",
2846
- "F7",
2847
- "F8",
2848
- "F9",
2849
- "F10",
2850
- "F11",
2851
- "F12",
2852
- "Soft1",
2853
- "Soft2",
2854
- "Soft3",
2855
- "Soft4",
2856
- "ChannelDown",
2857
- "ChannelUp",
2858
- "Close",
2859
- "MailForward",
2860
- "MailReply",
2861
- "MailSend",
2862
- "MediaFastForward",
2863
- "MediaPause",
2864
- "MediaPlay",
2865
- "MediaPlayPause",
2866
- "MediaRecord",
2867
- "MediaRewind",
2868
- "MediaStop",
2869
- "MediaTrackNext",
2870
- "MediaTrackPrevious",
2871
- "AudioBalanceLeft",
2872
- "AudioBalanceRight",
2873
- "AudioBassBoostDown",
2874
- "AudioBassBoostToggle",
2875
- "AudioBassBoostUp",
2876
- "AudioFaderFront",
2877
- "AudioFaderRear",
2878
- "AudioSurroundModeNext",
2879
- "AudioTrebleDown",
2880
- "AudioTrebleUp",
2881
- "AudioVolumeDown",
2882
- "AudioVolumeMute",
2883
- "AudioVolumeUp",
2884
- "MicrophoneToggle",
2885
- "MicrophoneVolumeDown",
2886
- "MicrophoneVolumeMute",
2887
- "MicrophoneVolumeUp",
2888
- "TV",
2889
- "TV3DMode",
2890
- "TVAntennaCable",
2891
- "TVAudioDescription",
2892
- ];
2893
- function unEscapeString(str) {
2894
- const placeholder = "__NEWLINE__";
2895
- str = str.replace(new RegExp(placeholder, "g"), "\n");
2896
- return str;
2897
- }
2898
4653
  export { StableBrowser };
2899
4654
  //# sourceMappingURL=stable_browser.js.map