automation_model 1.0.485-dev → 1.0.485

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