automation_model 1.0.482-dev → 1.0.482

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