automation_model 1.0.472-dev → 1.0.472

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