automation_model 1.0.481-dev → 1.0.481

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