automation_model 1.0.477-dev → 1.0.477

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 +283 -29
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.d.ts +6 -3
  10. package/lib/browser_manager.js +194 -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 +133 -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 -47
  61. package/lib/stable_browser.js +2565 -865
  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 +735 -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.locator(":root").evaluate((_node, [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,33 +669,218 @@ 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) {
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) {
570
758
  if (!timeout) {
571
759
  timeout = 30000;
572
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
+ }
573
768
  for (let i = 0; i < 3; i++) {
574
769
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
575
770
  for (let j = 0; j < selectors.locators.length; j++) {
576
771
  let selector = selectors.locators[j];
577
772
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
578
773
  }
579
- 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
+ }
580
817
  if (!element.rerun) {
581
- 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();
582
873
  }
583
874
  }
584
875
  throw new Error("unable to locate element " + JSON.stringify(selectors));
585
876
  }
586
- async _findFrameScope(selectors, timeout = 30000) {
877
+ async _findFrameScope(selectors, timeout = 30000, info) {
878
+ if (!info) {
879
+ info = {};
880
+ info.failCause = {};
881
+ info.log = "";
882
+ }
883
+ let startTime = Date.now();
587
884
  let scope = this.page;
588
885
  if (selectors.frame) {
589
886
  return selectors.frame;
@@ -593,7 +890,7 @@ class StableBrowser {
593
890
  for (let i = 0; i < frame.selectors.length; i++) {
594
891
  let frameLocator = frame.selectors[i];
595
892
  if (frameLocator.css) {
596
- let testframescope = framescope.frameLocator(frameLocator.css);
893
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
597
894
  if (frameLocator.index) {
598
895
  testframescope = framescope.nth(frameLocator.index);
599
896
  }
@@ -605,7 +902,7 @@ class StableBrowser {
605
902
  break;
606
903
  }
607
904
  catch (error) {
608
- console.error("frame not found " + frameLocator.css);
905
+ // console.error("frame not found " + frameLocator.css);
609
906
  }
610
907
  }
611
908
  }
@@ -614,9 +911,11 @@ class StableBrowser {
614
911
  }
615
912
  return framescope;
616
913
  };
914
+ let fLocator = null;
617
915
  while (true) {
618
916
  let frameFound = false;
619
917
  if (selectors.nestFrmLoc) {
918
+ fLocator = selectors.nestFrmLoc;
620
919
  scope = await findFrame(selectors.nestFrmLoc, scope);
621
920
  frameFound = true;
622
921
  break;
@@ -625,6 +924,7 @@ class StableBrowser {
625
924
  for (let i = 0; i < selectors.frameLocators.length; i++) {
626
925
  let frameLocator = selectors.frameLocators[i];
627
926
  if (frameLocator.css) {
927
+ fLocator = frameLocator.css;
628
928
  scope = scope.frameLocator(frameLocator.css);
629
929
  frameFound = true;
630
930
  break;
@@ -632,16 +932,25 @@ class StableBrowser {
632
932
  }
633
933
  }
634
934
  if (!frameFound && selectors.iframe_src) {
935
+ fLocator = selectors.iframe_src;
635
936
  scope = this.page.frame({ url: selectors.iframe_src });
636
937
  }
637
938
  if (!scope) {
638
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
639
- 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}"`;
640
946
  throw new Error("unable to locate iframe " + selectors.iframe_src);
641
947
  }
642
948
  await new Promise((resolve) => setTimeout(resolve, 1000));
643
949
  }
644
950
  else {
951
+ if (info && info.locatorLog) {
952
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
953
+ }
645
954
  break;
646
955
  }
647
956
  }
@@ -651,20 +960,34 @@ class StableBrowser {
651
960
  }
652
961
  return scope;
653
962
  }
654
- async _getDocumentBody(selectors, timeout = 30000) {
655
- let scope = await this._findFrameScope(selectors, timeout);
963
+ async _getDocumentBody(selectors, timeout = 30000, info) {
964
+ let scope = await this._findFrameScope(selectors, timeout, info);
656
965
  return scope.evaluate(() => {
657
966
  var bodyContent = document.body.innerHTML;
658
967
  return bodyContent;
659
968
  });
660
969
  }
661
- async _locate_internal(selectors, info, _params, timeout = 30000) {
970
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
971
+ if (selectors.locators && Array.isArray(selectors.locators)) {
972
+ selectors.locators.forEach((locator) => {
973
+ locator.index = locator.index ?? 0;
974
+ if (locator.css && !locator.css.endsWith(">> visible=true")) {
975
+ locator.css = locator.css + " >> visible=true";
976
+ }
977
+ });
978
+ }
979
+ if (!info) {
980
+ info = {};
981
+ info.failCause = {};
982
+ info.log = "";
983
+ info.locatorLog = new LocatorLog(selectors);
984
+ }
662
985
  let highPriorityTimeout = 5000;
663
986
  let visibleOnlyTimeout = 6000;
664
- let startTime = performance.now();
987
+ let startTime = Date.now();
665
988
  let locatorsCount = 0;
989
+ let lazy_scroll = false;
666
990
  //let arrayMode = Array.isArray(selectors);
667
- let scope = await this._findFrameScope(selectors, timeout);
668
991
  let selectorsLocators = null;
669
992
  selectorsLocators = selectors.locators;
670
993
  // group selectors by priority
@@ -692,6 +1015,7 @@ class StableBrowser {
692
1015
  let highPriorityOnly = true;
693
1016
  let visibleOnly = true;
694
1017
  while (true) {
1018
+ let scope = await this._findFrameScope(selectors, timeout, info);
695
1019
  locatorsCount = 0;
696
1020
  let result = [];
697
1021
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -700,18 +1024,13 @@ class StableBrowser {
700
1024
  }
701
1025
  // info.log += "scanning locators in priority 1" + "\n";
702
1026
  let onlyPriority3 = selectorsLocators[0].priority === 3;
703
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
1027
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
704
1028
  if (result.foundElements.length === 0) {
705
1029
  // info.log += "scanning locators in priority 2" + "\n";
706
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
1030
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
707
1031
  }
708
- if (result.foundElements.length === 0 && onlyPriority3) {
709
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
710
- }
711
- else {
712
- if (result.foundElements.length === 0 && !highPriorityOnly) {
713
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
714
- }
1032
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
1033
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
715
1034
  }
716
1035
  let foundElements = result.foundElements;
717
1036
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -751,24 +1070,43 @@ class StableBrowser {
751
1070
  return maxCountElement.locator;
752
1071
  }
753
1072
  }
754
- if (performance.now() - startTime > timeout) {
1073
+ if (Date.now() - startTime > timeout) {
755
1074
  break;
756
1075
  }
757
- if (performance.now() - startTime > highPriorityTimeout) {
758
- info.log += "high priority timeout, will try all elements" + "\n";
1076
+ if (Date.now() - startTime > highPriorityTimeout) {
1077
+ //info.log += "high priority timeout, will try all elements" + "\n";
759
1078
  highPriorityOnly = false;
1079
+ if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
1080
+ lazy_scroll = true;
1081
+ await scrollPageToLoadLazyElements(this.page);
1082
+ }
760
1083
  }
761
- if (performance.now() - startTime > visibleOnlyTimeout) {
762
- info.log += "visible only timeout, will try all elements" + "\n";
1084
+ if (Date.now() - startTime > visibleOnlyTimeout) {
1085
+ //info.log += "visible only timeout, will try all elements" + "\n";
763
1086
  visibleOnly = false;
764
1087
  }
765
1088
  await new Promise((resolve) => setTimeout(resolve, 1000));
1089
+ // sheck of more of half of the timeout has passed
1090
+ if (Date.now() - startTime > timeout / 2) {
1091
+ highPriorityOnly = false;
1092
+ visibleOnly = false;
1093
+ }
766
1094
  }
767
1095
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
768
- info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
1096
+ // if (info.locatorLog) {
1097
+ // const lines = info.locatorLog.toString().split("\n");
1098
+ // for (let line of lines) {
1099
+ // this.logger.debug(line);
1100
+ // }
1101
+ // }
1102
+ //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
1103
+ info.failCause.locatorNotFound = true;
1104
+ if (!info?.failCause?.lastError) {
1105
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
1106
+ }
769
1107
  throw new Error("failed to locate first element no elements found, " + info.log);
770
1108
  }
771
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
1109
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
772
1110
  let foundElements = [];
773
1111
  const result = {
774
1112
  foundElements: foundElements,
@@ -776,31 +1114,88 @@ class StableBrowser {
776
1114
  for (let i = 0; i < locatorsGroup.length; i++) {
777
1115
  let foundLocators = [];
778
1116
  try {
779
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
1117
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
780
1118
  }
781
1119
  catch (e) {
782
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
783
- this.logger.debug(e);
1120
+ // this call can fail it the browser is navigating
1121
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
1122
+ // this.logger.debug(e);
784
1123
  foundLocators = [];
785
1124
  try {
786
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
1125
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
787
1126
  }
788
1127
  catch (e) {
789
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
1128
+ if (logErrors) {
1129
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
1130
+ }
790
1131
  }
791
1132
  }
792
1133
  if (foundLocators.length === 1) {
1134
+ let box = null;
1135
+ if (!this.onlyFailuresScreenshot) {
1136
+ box = await foundLocators[0].boundingBox();
1137
+ }
793
1138
  result.foundElements.push({
794
1139
  locator: foundLocators[0],
795
- box: await foundLocators[0].boundingBox(),
1140
+ box: box,
796
1141
  unique: true,
797
1142
  });
798
1143
  result.locatorIndex = i;
799
1144
  }
1145
+ if (foundLocators.length > 1) {
1146
+ // remove elements that consume the same space with 10 pixels tolerance
1147
+ const boxes = [];
1148
+ for (let j = 0; j < foundLocators.length; j++) {
1149
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
1150
+ }
1151
+ for (let j = 0; j < boxes.length; j++) {
1152
+ for (let k = 0; k < boxes.length; k++) {
1153
+ if (j === k) {
1154
+ continue;
1155
+ }
1156
+ // check if x, y, width, height are the same with 10 pixels tolerance
1157
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
1158
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
1159
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
1160
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
1161
+ // as the element is not unique, will remove it
1162
+ boxes.splice(k, 1);
1163
+ k--;
1164
+ }
1165
+ }
1166
+ }
1167
+ if (boxes.length === 1) {
1168
+ result.foundElements.push({
1169
+ locator: boxes[0].locator.first(),
1170
+ box: boxes[0].box,
1171
+ unique: true,
1172
+ });
1173
+ result.locatorIndex = i;
1174
+ }
1175
+ else if (logErrors) {
1176
+ info.failCause.foundMultiple = true;
1177
+ if (info.locatorLog) {
1178
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
1179
+ }
1180
+ }
1181
+ }
800
1182
  }
801
1183
  return result;
802
1184
  }
803
1185
  async simpleClick(elementDescription, _params, options = {}, world = null) {
1186
+ const state = {
1187
+ locate: false,
1188
+ scroll: false,
1189
+ highlight: false,
1190
+ _params,
1191
+ options,
1192
+ world,
1193
+ type: Types.CLICK,
1194
+ text: "Click element",
1195
+ operation: "simpleClick",
1196
+ log: "***** click on " + elementDescription + " *****\n",
1197
+ };
1198
+ _preCommand(state, this);
804
1199
  const startTime = Date.now();
805
1200
  let timeout = 30000;
806
1201
  if (options && options.timeout) {
@@ -809,12 +1204,12 @@ class StableBrowser {
809
1204
  while (true) {
810
1205
  try {
811
1206
  const result = await locate_element(this.context, elementDescription, "click");
812
- if ((result === null || result === void 0 ? void 0 : result.elementNumber) >= 0) {
1207
+ if (result?.elementNumber >= 0) {
813
1208
  const selectors = {
814
- frame: result === null || result === void 0 ? void 0 : result.frame,
1209
+ frame: result?.frame,
815
1210
  locators: [
816
1211
  {
817
- css: result === null || result === void 0 ? void 0 : result.css,
1212
+ css: result?.css,
818
1213
  },
819
1214
  ],
820
1215
  };
@@ -824,13 +1219,32 @@ class StableBrowser {
824
1219
  }
825
1220
  catch (e) {
826
1221
  if (performance.now() - startTime > timeout) {
827
- throw e;
1222
+ // throw e;
1223
+ try {
1224
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1225
+ }
1226
+ finally {
1227
+ await _commandFinally(state, this);
1228
+ }
828
1229
  }
829
1230
  }
830
1231
  await new Promise((resolve) => setTimeout(resolve, 3000));
831
1232
  }
832
1233
  }
833
1234
  async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
1235
+ const state = {
1236
+ locate: false,
1237
+ scroll: false,
1238
+ highlight: false,
1239
+ _params,
1240
+ options,
1241
+ world,
1242
+ type: Types.FILL,
1243
+ text: "Fill element",
1244
+ operation: "simpleClickType",
1245
+ log: "***** click type on " + elementDescription + " *****\n",
1246
+ };
1247
+ _preCommand(state, this);
834
1248
  const startTime = Date.now();
835
1249
  let timeout = 30000;
836
1250
  if (options && options.timeout) {
@@ -839,12 +1253,12 @@ class StableBrowser {
839
1253
  while (true) {
840
1254
  try {
841
1255
  const result = await locate_element(this.context, elementDescription, "fill", value);
842
- if ((result === null || result === void 0 ? void 0 : result.elementNumber) >= 0) {
1256
+ if (result?.elementNumber >= 0) {
843
1257
  const selectors = {
844
- frame: result === null || result === void 0 ? void 0 : result.frame,
1258
+ frame: result?.frame,
845
1259
  locators: [
846
1260
  {
847
- css: result === null || result === void 0 ? void 0 : result.css,
1261
+ css: result?.css,
848
1262
  },
849
1263
  ],
850
1264
  };
@@ -854,7 +1268,13 @@ class StableBrowser {
854
1268
  }
855
1269
  catch (e) {
856
1270
  if (performance.now() - startTime > timeout) {
857
- throw e;
1271
+ // throw e;
1272
+ try {
1273
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1274
+ }
1275
+ finally {
1276
+ await _commandFinally(state, this);
1277
+ }
858
1278
  }
859
1279
  }
860
1280
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -867,34 +1287,68 @@ class StableBrowser {
867
1287
  options,
868
1288
  world,
869
1289
  text: "Click element",
1290
+ _text: "Click on " + selectors.element_name,
870
1291
  type: Types.CLICK,
871
1292
  operation: "click",
872
1293
  log: "***** click on " + selectors.element_name + " *****\n",
873
1294
  };
1295
+ check_performance("click_all ***", this.context, true);
874
1296
  try {
1297
+ check_performance("click_preCommand", this.context, true);
875
1298
  await _preCommand(state, this);
876
- if (state.options && state.options.context) {
877
- state.selectors.locators[0].text = state.options.context;
1299
+ check_performance("click_preCommand", this.context, false);
1300
+ await performAction("click", state.element, options, this, state, _params);
1301
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1302
+ check_performance("click_waitForPageLoad", this.context, true);
1303
+ await this.waitForPageLoad({ noSleep: true });
1304
+ check_performance("click_waitForPageLoad", this.context, false);
878
1305
  }
879
- try {
880
- await state.element.click();
881
- await new Promise((resolve) => setTimeout(resolve, 1000));
882
- }
883
- catch (e) {
884
- // await this.closeUnexpectedPopups();
885
- state.element = await this._locate(selectors, state.info, _params);
886
- await state.element.dispatchEvent("click");
887
- await new Promise((resolve) => setTimeout(resolve, 1000));
888
- }
889
- await this.waitForPageLoad();
890
1306
  return state.info;
891
1307
  }
892
1308
  catch (e) {
893
1309
  await _commandError(state, e, this);
894
1310
  }
895
1311
  finally {
896
- _commandFinally(state, this);
1312
+ check_performance("click_commandFinally", this.context, true);
1313
+ await _commandFinally(state, this);
1314
+ check_performance("click_commandFinally", this.context, false);
1315
+ check_performance("click_all ***", this.context, false);
1316
+ if (this.context.profile) {
1317
+ console.log(JSON.stringify(this.context.profile, null, 2));
1318
+ }
1319
+ }
1320
+ }
1321
+ async waitForElement(selectors, _params, options = {}, world = null) {
1322
+ const timeout = this._getFindElementTimeout(options);
1323
+ const state = {
1324
+ selectors,
1325
+ _params,
1326
+ options,
1327
+ world,
1328
+ text: "Wait for element",
1329
+ _text: "Wait for " + selectors.element_name,
1330
+ type: Types.WAIT_ELEMENT,
1331
+ operation: "waitForElement",
1332
+ log: "***** wait for " + selectors.element_name + " *****\n",
1333
+ };
1334
+ let found = false;
1335
+ try {
1336
+ await _preCommand(state, this);
1337
+ // if (state.options && state.options.context) {
1338
+ // state.selectors.locators[0].text = state.options.context;
1339
+ // }
1340
+ await state.element.waitFor({ timeout: timeout });
1341
+ found = true;
1342
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1343
+ }
1344
+ catch (e) {
1345
+ console.error("Error on waitForElement", e);
1346
+ // await _commandError(state, e, this);
1347
+ }
1348
+ finally {
1349
+ await _commandFinally(state, this);
897
1350
  }
1351
+ return found;
898
1352
  }
899
1353
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
900
1354
  const state = {
@@ -904,39 +1358,63 @@ class StableBrowser {
904
1358
  world,
905
1359
  type: checked ? Types.CHECK : Types.UNCHECK,
906
1360
  text: checked ? `Check element` : `Uncheck element`,
1361
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
907
1362
  operation: "setCheck",
908
1363
  log: "***** check " + selectors.element_name + " *****\n",
909
1364
  };
910
1365
  try {
911
- _preCommand(state, this);
1366
+ await _preCommand(state, this);
912
1367
  state.info.checked = checked;
913
1368
  // let element = await this._locate(selectors, info, _params);
914
1369
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
915
1370
  try {
916
- // await this._highlightElements(element);
917
- await state.element.setChecked(checked);
1371
+ // if (world && world.screenshot && !world.screenshotPath) {
1372
+ // console.log(`Highlighting while running from recorder`);
1373
+ await this._highlightElements(state.element);
1374
+ await state.element.setChecked(checked, { timeout: 2000 });
918
1375
  await new Promise((resolve) => setTimeout(resolve, 1000));
1376
+ // await this._unHighlightElements(element);
1377
+ // }
1378
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1379
+ // await this._unHighlightElements(element);
919
1380
  }
920
1381
  catch (e) {
921
1382
  if (e.message && e.message.includes("did not change its state")) {
922
1383
  this.logger.info("element did not change its state, ignoring...");
923
1384
  }
924
1385
  else {
925
- //await this.closeUnexpectedPopups();
926
- info.log += "setCheck failed, will try again" + "\n";
927
- state.element = await this._locate(selectors, state.info, _params);
928
- await state.element.setChecked(checked, { timeout: 5000, force: true });
929
1386
  await new Promise((resolve) => setTimeout(resolve, 1000));
1387
+ //await this.closeUnexpectedPopups();
1388
+ state.info.log += "setCheck failed, will try again" + "\n";
1389
+ state.element_found = false;
1390
+ try {
1391
+ state.element = await this._locate(selectors, state.info, _params, 100);
1392
+ state.element_found = true;
1393
+ // check the check state
1394
+ }
1395
+ catch (error) {
1396
+ // element dismissed
1397
+ }
1398
+ if (state.element_found) {
1399
+ const isChecked = await state.element.isChecked();
1400
+ if (isChecked !== checked) {
1401
+ // perform click
1402
+ await state.element.click({ timeout: 2000, force: true });
1403
+ }
1404
+ else {
1405
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1406
+ }
1407
+ }
930
1408
  }
931
1409
  }
932
- await this.waitForPageLoad();
1410
+ //await this.waitForPageLoad();
933
1411
  return state.info;
934
1412
  }
935
1413
  catch (e) {
936
1414
  await _commandError(state, e, this);
937
1415
  }
938
1416
  finally {
939
- _commandFinally(state, this);
1417
+ await _commandFinally(state, this);
940
1418
  }
941
1419
  }
942
1420
  async hover(selectors, _params, options = {}, world = null) {
@@ -947,30 +1425,22 @@ class StableBrowser {
947
1425
  world,
948
1426
  type: Types.HOVER,
949
1427
  text: `Hover element`,
1428
+ _text: `Hover on ${selectors.element_name}`,
950
1429
  operation: "hover",
951
1430
  log: "***** hover " + selectors.element_name + " *****\n",
952
1431
  };
953
1432
  try {
954
- _preCommand(state, this);
955
- try {
956
- await state.element.hover();
957
- await new Promise((resolve) => setTimeout(resolve, 1000));
958
- }
959
- catch (e) {
960
- //await this.closeUnexpectedPopups();
961
- state.info.log += "hover failed, will try again" + "\n";
962
- state.element = await this._locate(selectors, state.info, _params);
963
- await state.element.hover({ timeout: 10000 });
964
- await new Promise((resolve) => setTimeout(resolve, 1000));
965
- }
966
- await this.waitForPageLoad();
967
- return state.info;
1433
+ await _preCommand(state, this);
1434
+ await performAction("hover", state.element, options, this, state, _params);
1435
+ await _screenshot(state, this);
1436
+ //await this.waitForPageLoad();
1437
+ return state.info;
968
1438
  }
969
1439
  catch (e) {
970
1440
  await _commandError(state, e, this);
971
1441
  }
972
1442
  finally {
973
- _commandFinally(state, this);
1443
+ await _commandFinally(state, this);
974
1444
  }
975
1445
  }
976
1446
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -982,8 +1452,10 @@ class StableBrowser {
982
1452
  _params,
983
1453
  options,
984
1454
  world,
1455
+ value: values.toString(),
985
1456
  type: Types.SELECT,
986
1457
  text: `Select option: ${values}`,
1458
+ _text: `Select option: ${values} on ${selectors.element_name}`,
987
1459
  operation: "selectOption",
988
1460
  log: "***** select option " + selectors.element_name + " *****\n",
989
1461
  };
@@ -997,14 +1469,14 @@ class StableBrowser {
997
1469
  state.info.log += "selectOption failed, will try force" + "\n";
998
1470
  await state.element.selectOption(values, { timeout: 10000, force: true });
999
1471
  }
1000
- await this.waitForPageLoad();
1472
+ //await this.waitForPageLoad();
1001
1473
  return state.info;
1002
1474
  }
1003
1475
  catch (e) {
1004
1476
  await _commandError(state, e, this);
1005
1477
  }
1006
1478
  finally {
1007
- _commandFinally(state, this);
1479
+ await _commandFinally(state, this);
1008
1480
  }
1009
1481
  }
1010
1482
  async type(_value, _params = null, options = {}, world = null) {
@@ -1018,6 +1490,7 @@ class StableBrowser {
1018
1490
  highlight: false,
1019
1491
  type: Types.TYPE_PRESS,
1020
1492
  text: `Type value: ${_value}`,
1493
+ _text: `Type value: ${_value}`,
1021
1494
  operation: "type",
1022
1495
  log: "",
1023
1496
  };
@@ -1049,7 +1522,7 @@ class StableBrowser {
1049
1522
  await _commandError(state, e, this);
1050
1523
  }
1051
1524
  finally {
1052
- _commandFinally(state, this);
1525
+ await _commandFinally(state, this);
1053
1526
  }
1054
1527
  }
1055
1528
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1085,37 +1558,35 @@ class StableBrowser {
1085
1558
  await _commandError(state, e, this);
1086
1559
  }
1087
1560
  finally {
1088
- _commandFinally(state, this);
1561
+ await _commandFinally(state, this);
1089
1562
  }
1090
1563
  }
1091
1564
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1092
- _validateSelectors(selectors);
1093
- const startTime = Date.now();
1094
- let error = null;
1095
- let screenshotId = null;
1096
- let screenshotPath = null;
1097
- const info = {};
1098
- info.log = "";
1099
- info.operation = Types.SET_DATE_TIME;
1100
- info.selectors = selectors;
1101
- info.value = value;
1565
+ const state = {
1566
+ selectors,
1567
+ _params,
1568
+ value: await this._replaceWithLocalData(value, this),
1569
+ options,
1570
+ world,
1571
+ type: Types.SET_DATE_TIME,
1572
+ text: `Set date time value: ${value}`,
1573
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1574
+ operation: "setDateTime",
1575
+ log: "***** set date time value " + selectors.element_name + " *****\n",
1576
+ throwError: false,
1577
+ };
1102
1578
  try {
1103
- value = await this._replaceWithLocalData(value, this);
1104
- let element = await this._locate(selectors, info, _params);
1105
- //insert red border around the element
1106
- await this.scrollIfNeeded(element, info);
1107
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1108
- await this._highlightElements(element);
1579
+ await _preCommand(state, this);
1109
1580
  try {
1110
- await element.click();
1581
+ await performAction("click", state.element, options, this, state, _params);
1111
1582
  await new Promise((resolve) => setTimeout(resolve, 500));
1112
1583
  if (format) {
1113
- value = dayjs(value).format(format);
1114
- await element.fill(value);
1584
+ state.value = dayjs(state.value).format(format);
1585
+ await state.element.fill(state.value);
1115
1586
  }
1116
1587
  else {
1117
- const dateTimeValue = await getDateTimeValue({ value, element });
1118
- await element.evaluateHandle((el, dateTimeValue) => {
1588
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1589
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1119
1590
  el.value = ""; // clear input
1120
1591
  el.value = dateTimeValue;
1121
1592
  }, dateTimeValue);
@@ -1128,20 +1599,19 @@ class StableBrowser {
1128
1599
  }
1129
1600
  catch (err) {
1130
1601
  //await this.closeUnexpectedPopups();
1131
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1602
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1132
1603
  this.logger.info("Trying again");
1133
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1134
- info.screenshotPath = screenshotPath;
1135
- Object.assign(err, { info: info });
1604
+ await _screenshot(state, this);
1605
+ Object.assign(err, { info: state.info });
1136
1606
  await element.click();
1137
1607
  await new Promise((resolve) => setTimeout(resolve, 500));
1138
1608
  if (format) {
1139
- value = dayjs(value).format(format);
1140
- await element.fill(value);
1609
+ state.value = dayjs(state.value).format(format);
1610
+ await state.element.fill(state.value);
1141
1611
  }
1142
1612
  else {
1143
- const dateTimeValue = await getDateTimeValue({ value, element });
1144
- await element.evaluateHandle((el, dateTimeValue) => {
1613
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1614
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1145
1615
  el.value = ""; // clear input
1146
1616
  el.value = dateTimeValue;
1147
1617
  }, dateTimeValue);
@@ -1154,71 +1624,60 @@ class StableBrowser {
1154
1624
  }
1155
1625
  }
1156
1626
  catch (e) {
1157
- error = e;
1158
- throw e;
1627
+ await _commandError(state, e, this);
1159
1628
  }
1160
1629
  finally {
1161
- const endTime = Date.now();
1162
- this._reportToWorld(world, {
1163
- element_name: selectors.element_name,
1164
- type: Types.SET_DATE_TIME,
1165
- screenshotId,
1166
- value: value,
1167
- text: `setDateTime input with value: ${value}`,
1168
- result: error
1169
- ? {
1170
- status: "FAILED",
1171
- startTime,
1172
- endTime,
1173
- message: error === null || error === void 0 ? void 0 : error.message,
1174
- }
1175
- : {
1176
- status: "PASSED",
1177
- startTime,
1178
- endTime,
1179
- },
1180
- info: info,
1181
- });
1630
+ await _commandFinally(state, this);
1182
1631
  }
1183
1632
  }
1184
1633
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
1634
+ _value = unEscapeString(_value);
1635
+ const newValue = await this._replaceWithLocalData(_value, world);
1185
1636
  const state = {
1186
1637
  selectors,
1187
1638
  _params,
1188
- value: unEscapeString(_value),
1639
+ value: newValue,
1640
+ originalValue: _value,
1189
1641
  options,
1190
1642
  world,
1191
1643
  type: Types.FILL,
1192
1644
  text: `Click type input with value: ${_value}`,
1645
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1193
1646
  operation: "clickType",
1194
- log: "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n",
1647
+ log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1195
1648
  };
1196
- const newValue = await this._replaceWithLocalData(state.value, world);
1649
+ if (!options) {
1650
+ options = {};
1651
+ }
1197
1652
  if (newValue !== _value) {
1198
1653
  //this.logger.info(_value + "=" + newValue);
1199
1654
  _value = newValue;
1200
1655
  }
1201
1656
  try {
1202
1657
  await _preCommand(state, this);
1658
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1659
+ // tag the element
1660
+ let newElementSelector = await state.element.evaluate((el, token) => {
1661
+ // use attribute and not id
1662
+ const attrName = `data-blinq-id-${token}`;
1663
+ el.setAttribute(attrName, "");
1664
+ return `[${attrName}]`;
1665
+ }, randomToken);
1203
1666
  state.info.value = _value;
1204
- if (options === null || options === undefined || !options.press) {
1667
+ if (!options.press) {
1205
1668
  try {
1206
1669
  let currentValue = await state.element.inputValue();
1207
1670
  if (currentValue) {
1208
- await element.fill("");
1671
+ await state.element.fill("");
1209
1672
  }
1210
1673
  }
1211
1674
  catch (e) {
1212
1675
  this.logger.info("unable to clear input value");
1213
1676
  }
1214
1677
  }
1215
- if (options === null || options === undefined || options.press) {
1216
- try {
1217
- await state.element.click({ timeout: 5000 });
1218
- }
1219
- catch (e) {
1220
- await state.element.dispatchEvent("click");
1221
- }
1678
+ if (options.press) {
1679
+ options.timeout = 5000;
1680
+ await performAction("click", state.element, options, this, state, _params);
1222
1681
  }
1223
1682
  else {
1224
1683
  try {
@@ -1229,6 +1688,25 @@ class StableBrowser {
1229
1688
  }
1230
1689
  }
1231
1690
  await new Promise((resolve) => setTimeout(resolve, 500));
1691
+ // check if the element exist after the click (no wait)
1692
+ const count = await state.element.count({ timeout: 0 });
1693
+ if (count === 0) {
1694
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1695
+ const scope = state.element._frame ?? element.page();
1696
+ let prefixSelector = "";
1697
+ const frameControlSelector = " >> internal:control=enter-frame";
1698
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1699
+ if (frameSelectorIndex !== -1) {
1700
+ // remove everything after the >> internal:control=enter-frame
1701
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1702
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1703
+ }
1704
+ // if (element?._frame?._selector) {
1705
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1706
+ // }
1707
+ const newSelector = prefixSelector + newElementSelector;
1708
+ state.element = scope.locator(newSelector).first();
1709
+ }
1232
1710
  const valueSegment = state.value.split("&&");
1233
1711
  for (let i = 0; i < valueSegment.length; i++) {
1234
1712
  if (i > 0) {
@@ -1249,14 +1727,21 @@ class StableBrowser {
1249
1727
  await new Promise((resolve) => setTimeout(resolve, 500));
1250
1728
  }
1251
1729
  }
1730
+ //if (!this.fastMode) {
1252
1731
  await _screenshot(state, this);
1732
+ //}
1253
1733
  if (enter === true) {
1254
1734
  await new Promise((resolve) => setTimeout(resolve, 2000));
1255
1735
  await this.page.keyboard.press("Enter");
1256
1736
  await this.waitForPageLoad();
1257
1737
  }
1258
1738
  else if (enter === false) {
1259
- await state.element.dispatchEvent("change");
1739
+ try {
1740
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1741
+ }
1742
+ catch (e) {
1743
+ // ignore
1744
+ }
1260
1745
  //await this.page.keyboard.press("Tab");
1261
1746
  }
1262
1747
  else {
@@ -1271,7 +1756,7 @@ class StableBrowser {
1271
1756
  await _commandError(state, e, this);
1272
1757
  }
1273
1758
  finally {
1274
- _commandFinally(state, this);
1759
+ await _commandFinally(state, this);
1275
1760
  }
1276
1761
  }
1277
1762
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1293,30 +1778,67 @@ class StableBrowser {
1293
1778
  if (enter) {
1294
1779
  await new Promise((resolve) => setTimeout(resolve, 2000));
1295
1780
  await this.page.keyboard.press("Enter");
1781
+ await this.waitForPageLoad();
1782
+ }
1783
+ return state.info;
1784
+ }
1785
+ catch (e) {
1786
+ await _commandError(state, e, this);
1787
+ }
1788
+ finally {
1789
+ await _commandFinally(state, this);
1790
+ }
1791
+ }
1792
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1793
+ const state = {
1794
+ selectors,
1795
+ _params,
1796
+ files,
1797
+ value: '"' + files.join('", "') + '"',
1798
+ options,
1799
+ world,
1800
+ type: Types.SET_INPUT_FILES,
1801
+ text: `Set input files`,
1802
+ _text: `Set input files on ${selectors.element_name}`,
1803
+ operation: "setInputFiles",
1804
+ log: "***** set input files " + selectors.element_name + " *****\n",
1805
+ };
1806
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1807
+ try {
1808
+ await _preCommand(state, this);
1809
+ for (let i = 0; i < files.length; i++) {
1810
+ const file = files[i];
1811
+ const filePath = path.join(uploadsFolder, file);
1812
+ if (!fs.existsSync(filePath)) {
1813
+ throw new Error(`File not found: ${filePath}`);
1814
+ }
1815
+ state.files[i] = filePath;
1296
1816
  }
1297
- await this.waitForPageLoad();
1817
+ await state.element.setInputFiles(files);
1298
1818
  return state.info;
1299
1819
  }
1300
1820
  catch (e) {
1301
1821
  await _commandError(state, e, this);
1302
1822
  }
1303
1823
  finally {
1304
- _commandFinally(state, this);
1824
+ await _commandFinally(state, this);
1305
1825
  }
1306
1826
  }
1307
1827
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1308
1828
  return await this._getText(selectors, 0, _params, options, info, world);
1309
1829
  }
1310
1830
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1831
+ const timeout = this._getFindElementTimeout(options);
1311
1832
  _validateSelectors(selectors);
1312
1833
  let screenshotId = null;
1313
1834
  let screenshotPath = null;
1314
1835
  if (!info.log) {
1315
1836
  info.log = "";
1837
+ info.locatorLog = new LocatorLog(selectors);
1316
1838
  }
1317
1839
  info.operation = "getText";
1318
1840
  info.selectors = selectors;
1319
- let element = await this._locate(selectors, info, _params);
1841
+ let element = await this._locate(selectors, info, _params, timeout);
1320
1842
  if (climb > 0) {
1321
1843
  const climbArray = [];
1322
1844
  for (let i = 0; i < climb; i++) {
@@ -1335,6 +1857,18 @@ class StableBrowser {
1335
1857
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1336
1858
  try {
1337
1859
  await this._highlightElements(element);
1860
+ // if (world && world.screenshot && !world.screenshotPath) {
1861
+ // // console.log(`Highlighting for get text while running from recorder`);
1862
+ // this._highlightElements(element)
1863
+ // .then(async () => {
1864
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1865
+ // this._unhighlightElements(element).then(
1866
+ // () => {}
1867
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1868
+ // );
1869
+ // })
1870
+ // .catch(e);
1871
+ // }
1338
1872
  const elementText = await element.innerText();
1339
1873
  return {
1340
1874
  text: elementText,
@@ -1346,13 +1880,12 @@ class StableBrowser {
1346
1880
  }
1347
1881
  catch (e) {
1348
1882
  //await this.closeUnexpectedPopups();
1349
- this.logger.info("no innerText will use textContent");
1883
+ this.logger.info("no innerText, will use textContent");
1350
1884
  const elementText = await element.textContent();
1351
1885
  return { text: elementText, screenshotId, screenshotPath, value: value };
1352
1886
  }
1353
1887
  }
1354
1888
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1355
- var _a;
1356
1889
  if (!pattern) {
1357
1890
  throw new Error("pattern is null");
1358
1891
  }
@@ -1363,7 +1896,7 @@ class StableBrowser {
1363
1896
  selectors,
1364
1897
  _params,
1365
1898
  pattern,
1366
- value: text,
1899
+ value: pattern,
1367
1900
  options,
1368
1901
  world,
1369
1902
  locate: false,
@@ -1372,6 +1905,7 @@ class StableBrowser {
1372
1905
  highlight: false,
1373
1906
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1374
1907
  text: `Verify element contains pattern: ${pattern}`,
1908
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1375
1909
  operation: "containsPattern",
1376
1910
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1377
1911
  };
@@ -1392,22 +1926,23 @@ class StableBrowser {
1392
1926
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1393
1927
  pattern = pattern.replace("{text}", escapedText);
1394
1928
  let regex = new RegExp(pattern, "im");
1395
- 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))) {
1396
- state.info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1929
+ if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
1930
+ state.info.foundText = foundObj?.text;
1397
1931
  throw new Error("element doesn't contain text " + text);
1398
1932
  }
1399
1933
  return state.info;
1400
1934
  }
1401
1935
  catch (e) {
1402
- this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
1936
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1403
1937
  await _commandError(state, e, this);
1404
1938
  }
1405
1939
  finally {
1406
- _commandFinally(state, this);
1940
+ await _commandFinally(state, this);
1407
1941
  }
1408
1942
  }
1409
1943
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1410
- var _a, _b, _c;
1944
+ const timeout = this._getFindElementTimeout(options);
1945
+ const startTime = Date.now();
1411
1946
  const state = {
1412
1947
  selectors,
1413
1948
  _params,
@@ -1434,60 +1969,130 @@ class StableBrowser {
1434
1969
  }
1435
1970
  let foundObj = null;
1436
1971
  try {
1437
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1438
- if (foundObj && foundObj.element) {
1439
- await this.scrollIfNeeded(foundObj.element, state.info);
1440
- }
1441
- await _screenshot(state, this);
1442
- const dateAlternatives = findDateAlternatives(text);
1443
- const numberAlternatives = findNumberAlternatives(text);
1444
- if (dateAlternatives.date) {
1445
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1446
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
1447
- ((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
1448
- return state.info;
1972
+ while (Date.now() - startTime < timeout) {
1973
+ try {
1974
+ await _preCommand(state, this);
1975
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1976
+ if (foundObj && foundObj.element) {
1977
+ await this.scrollIfNeeded(foundObj.element, state.info);
1449
1978
  }
1450
- }
1451
- throw new Error("element doesn't contain text " + text);
1452
- }
1453
- else if (numberAlternatives.number) {
1454
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1455
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(numberAlternatives.numbers[i])) ||
1456
- ((_b = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _b === void 0 ? void 0 : _b.includes(numberAlternatives.numbers[i]))) {
1979
+ await _screenshot(state, this);
1980
+ const dateAlternatives = findDateAlternatives(text);
1981
+ const numberAlternatives = findNumberAlternatives(text);
1982
+ if (dateAlternatives.date) {
1983
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1984
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1985
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1986
+ return state.info;
1987
+ }
1988
+ }
1989
+ }
1990
+ else if (numberAlternatives.number) {
1991
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1992
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1993
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1994
+ return state.info;
1995
+ }
1996
+ }
1997
+ }
1998
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1457
1999
  return state.info;
1458
2000
  }
1459
2001
  }
1460
- throw new Error("element doesn't contain text " + text);
1461
- }
1462
- 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))) {
1463
- state.info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1464
- state.info.value = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value;
1465
- throw new Error("element doesn't contain text " + text);
2002
+ catch (e) {
2003
+ // Log error but continue retrying until timeout is reached
2004
+ this.logger.warn("Retrying containsText due to: " + e.message);
2005
+ }
2006
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1466
2007
  }
1467
- return state.info;
2008
+ state.info.foundText = foundObj?.text;
2009
+ state.info.value = foundObj?.value;
2010
+ throw new Error("element doesn't contain text " + text);
1468
2011
  }
1469
2012
  catch (e) {
1470
2013
  await _commandError(state, e, this);
2014
+ throw e;
1471
2015
  }
1472
2016
  finally {
1473
- _commandFinally(state, this);
2017
+ await _commandFinally(state, this);
1474
2018
  }
1475
2019
  }
1476
- _getDataFile(world = null) {
1477
- let dataFile = null;
1478
- if (world && world.reportFolder) {
1479
- dataFile = path.join(world.reportFolder, "data.json");
2020
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
2021
+ const timeout = this._getFindElementTimeout(options);
2022
+ const startTime = Date.now();
2023
+ const state = {
2024
+ _params,
2025
+ value: referanceSnapshot,
2026
+ options,
2027
+ world,
2028
+ locate: false,
2029
+ scroll: false,
2030
+ screenshot: true,
2031
+ highlight: false,
2032
+ type: Types.SNAPSHOT_VALIDATION,
2033
+ text: `verify snapshot: ${referanceSnapshot}`,
2034
+ operation: "snapshotValidation",
2035
+ log: "***** verify snapshot *****\n",
2036
+ };
2037
+ if (!referanceSnapshot) {
2038
+ throw new Error("referanceSnapshot is null");
1480
2039
  }
1481
- else if (this.reportFolder) {
1482
- dataFile = path.join(this.reportFolder, "data.json");
2040
+ let text = null;
2041
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
2042
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1483
2043
  }
1484
- else if (this.context && this.context.reportFolder) {
1485
- dataFile = path.join(this.context.reportFolder, "data.json");
2044
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
2045
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
2046
+ }
2047
+ else if (referanceSnapshot.startsWith("yaml:")) {
2048
+ text = referanceSnapshot.substring(5);
1486
2049
  }
1487
2050
  else {
1488
- dataFile = "data.json";
2051
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
2052
+ }
2053
+ state.text = text;
2054
+ const newValue = await this._replaceWithLocalData(text, world);
2055
+ await _preCommand(state, this);
2056
+ let foundObj = null;
2057
+ try {
2058
+ let matchResult = null;
2059
+ while (Date.now() - startTime < timeout) {
2060
+ try {
2061
+ let scope = null;
2062
+ if (!frameSelectors) {
2063
+ scope = this.page;
2064
+ }
2065
+ else {
2066
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
2067
+ }
2068
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
2069
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
2070
+ if (matchResult.errorLine !== -1) {
2071
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
2072
+ }
2073
+ // highlight and screenshot
2074
+ try {
2075
+ await await highlightSnapshot(newValue, scope);
2076
+ await _screenshot(state, this);
2077
+ }
2078
+ catch (e) { }
2079
+ return state.info;
2080
+ }
2081
+ catch (e) {
2082
+ // Log error but continue retrying until timeout is reached
2083
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
2084
+ }
2085
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
2086
+ }
2087
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
2088
+ }
2089
+ catch (e) {
2090
+ await _commandError(state, e, this);
2091
+ throw e;
2092
+ }
2093
+ finally {
2094
+ await _commandFinally(state, this);
1489
2095
  }
1490
- return dataFile;
1491
2096
  }
1492
2097
  async waitForUserInput(message, world = null) {
1493
2098
  if (!message) {
@@ -1517,13 +2122,22 @@ class StableBrowser {
1517
2122
  return;
1518
2123
  }
1519
2124
  // if data file exists, load it
1520
- const dataFile = this._getDataFile(world);
2125
+ const dataFile = _getDataFile(world, this.context, this);
1521
2126
  let data = this.getTestData(world);
1522
2127
  // merge the testData with the existing data
1523
2128
  Object.assign(data, testData);
1524
2129
  // save the data to the file
1525
2130
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1526
2131
  }
2132
+ overwriteTestData(testData, world = null) {
2133
+ if (!testData) {
2134
+ return;
2135
+ }
2136
+ // if data file exists, load it
2137
+ const dataFile = _getDataFile(world, this.context, this);
2138
+ // save the data to the file
2139
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
2140
+ }
1527
2141
  _getDataFilePath(fileName) {
1528
2142
  let dataFile = path.join(this.project_path, "data", fileName);
1529
2143
  if (fs.existsSync(dataFile)) {
@@ -1620,12 +2234,7 @@ class StableBrowser {
1620
2234
  }
1621
2235
  }
1622
2236
  getTestData(world = null) {
1623
- const dataFile = this._getDataFile(world);
1624
- let data = {};
1625
- if (fs.existsSync(dataFile)) {
1626
- data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1627
- }
1628
- return data;
2237
+ return _getTestData(world, this.context, this);
1629
2238
  }
1630
2239
  async _screenShot(options = {}, world = null, info = null) {
1631
2240
  // collect url/path/title
@@ -1652,11 +2261,9 @@ class StableBrowser {
1652
2261
  if (!fs.existsSync(world.screenshotPath)) {
1653
2262
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1654
2263
  }
1655
- let nextIndex = 1;
1656
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1657
- nextIndex++;
1658
- }
1659
- const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
2264
+ // to make sure the path doesn't start with -
2265
+ const uuidStr = "id_" + randomUUID();
2266
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1660
2267
  try {
1661
2268
  await this.takeScreenshot(screenshotPath);
1662
2269
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1666,15 +2273,15 @@ class StableBrowser {
1666
2273
  // this.logger.info("unable to save screenshot " + screenshotPath);
1667
2274
  // }
1668
2275
  // });
2276
+ result.screenshotId = uuidStr;
2277
+ result.screenshotPath = screenshotPath;
2278
+ if (info && info.box) {
2279
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
2280
+ }
1669
2281
  }
1670
2282
  catch (e) {
1671
2283
  this.logger.info("unable to take screenshot, ignored");
1672
2284
  }
1673
- result.screenshotId = nextIndex;
1674
- result.screenshotPath = screenshotPath;
1675
- if (info && info.box) {
1676
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1677
- }
1678
2285
  }
1679
2286
  else if (options && options.screenshot) {
1680
2287
  result.screenshotPath = options.screenshotPath;
@@ -1709,6 +2316,15 @@ class StableBrowser {
1709
2316
  document.documentElement.clientWidth,
1710
2317
  ])));
1711
2318
  let screenshotBuffer = null;
2319
+ // if (focusedElement) {
2320
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2321
+ // await this._unhighlightElements(focusedElement);
2322
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2323
+ // console.log(`Unhighlighted previous element`);
2324
+ // }
2325
+ // if (focusedElement) {
2326
+ // await this._highlightElements(focusedElement);
2327
+ // }
1712
2328
  if (this.context.browserName === "chromium") {
1713
2329
  const client = await playContext.newCDPSession(this.page);
1714
2330
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1730,6 +2346,10 @@ class StableBrowser {
1730
2346
  else {
1731
2347
  screenshotBuffer = await this.page.screenshot();
1732
2348
  }
2349
+ // if (focusedElement) {
2350
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2351
+ // await this._unhighlightElements(focusedElement);
2352
+ // }
1733
2353
  let image = await Jimp.read(screenshotBuffer);
1734
2354
  // Get the image dimensions
1735
2355
  const { width, height } = image.bitmap;
@@ -1742,6 +2362,7 @@ class StableBrowser {
1742
2362
  else {
1743
2363
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1744
2364
  }
2365
+ return screenshotBuffer;
1745
2366
  }
1746
2367
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1747
2368
  const state = {
@@ -1764,111 +2385,530 @@ class StableBrowser {
1764
2385
  await _commandError(state, e, this);
1765
2386
  }
1766
2387
  finally {
1767
- _commandFinally(state, this);
2388
+ await _commandFinally(state, this);
1768
2389
  }
1769
2390
  }
1770
2391
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1771
- _validateSelectors(selectors);
1772
- const startTime = Date.now();
1773
- let error = null;
1774
- let screenshotId = null;
1775
- let screenshotPath = null;
2392
+ const state = {
2393
+ selectors,
2394
+ _params,
2395
+ attribute,
2396
+ variable,
2397
+ options,
2398
+ world,
2399
+ type: Types.EXTRACT,
2400
+ text: `Extract attribute from element`,
2401
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
2402
+ operation: "extractAttribute",
2403
+ log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
2404
+ allowDisabled: true,
2405
+ };
1776
2406
  await new Promise((resolve) => setTimeout(resolve, 2000));
1777
- const info = {};
1778
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1779
- info.operation = "extract";
1780
- info.selectors = selectors;
1781
2407
  try {
1782
- const element = await this._locate(selectors, info, _params);
1783
- await this._highlightElements(element);
1784
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2408
+ await _preCommand(state, this);
1785
2409
  switch (attribute) {
1786
2410
  case "inner_text":
1787
- info.value = await element.innerText();
2411
+ state.value = await state.element.innerText();
1788
2412
  break;
1789
2413
  case "href":
1790
- info.value = await element.getAttribute("href");
2414
+ state.value = await state.element.getAttribute("href");
1791
2415
  break;
1792
2416
  case "value":
1793
- info.value = await element.inputValue();
2417
+ state.value = await state.element.inputValue();
2418
+ break;
2419
+ case "text":
2420
+ state.value = await state.element.textContent();
1794
2421
  break;
1795
2422
  default:
1796
- info.value = await element.getAttribute(attribute);
2423
+ state.value = await state.element.getAttribute(attribute);
1797
2424
  break;
1798
2425
  }
1799
- this[variable] = info.value;
1800
- if (world) {
1801
- world[variable] = info.value;
2426
+ if (options !== null) {
2427
+ if (options.regex && options.regex !== "") {
2428
+ // Construct a regex pattern from the provided string
2429
+ const regex = options.regex.slice(1, -1);
2430
+ const regexPattern = new RegExp(regex, "g");
2431
+ const matches = state.value.match(regexPattern);
2432
+ if (matches) {
2433
+ let newValue = "";
2434
+ for (const match of matches) {
2435
+ newValue += match;
2436
+ }
2437
+ state.value = newValue;
2438
+ }
2439
+ }
2440
+ if (options.trimSpaces && options.trimSpaces === true) {
2441
+ state.value = state.value.trim();
2442
+ }
1802
2443
  }
1803
- this.setTestData({ [variable]: info.value }, world);
1804
- this.logger.info("set test data: " + variable + "=" + info.value);
1805
- return info;
2444
+ state.info.value = state.value;
2445
+ this.setTestData({ [variable]: state.value }, world);
2446
+ this.logger.info("set test data: " + variable + "=" + state.value);
2447
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2448
+ return state.info;
1806
2449
  }
1807
2450
  catch (e) {
1808
- //await this.closeUnexpectedPopups();
1809
- this.logger.error("extract failed " + info.log);
1810
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1811
- info.screenshotPath = screenshotPath;
1812
- Object.assign(e, { info: info });
1813
- error = e;
1814
- throw e;
2451
+ await _commandError(state, e, this);
1815
2452
  }
1816
2453
  finally {
1817
- const endTime = Date.now();
1818
- this._reportToWorld(world, {
1819
- element_name: selectors.element_name,
1820
- type: Types.EXTRACT_ATTRIBUTE,
1821
- variable: variable,
1822
- value: info.value,
1823
- text: "Extract attribute from element",
1824
- screenshotId,
1825
- result: error
1826
- ? {
1827
- status: "FAILED",
1828
- startTime,
1829
- endTime,
1830
- message: error === null || error === void 0 ? void 0 : error.message,
1831
- }
1832
- : {
1833
- status: "PASSED",
1834
- startTime,
1835
- endTime,
1836
- },
1837
- info: info,
1838
- });
2454
+ await _commandFinally(state, this);
1839
2455
  }
1840
2456
  }
1841
- async extractEmailData(emailAddress, options, world) {
1842
- if (!emailAddress) {
1843
- throw new Error("email address is null");
1844
- }
1845
- // check if address contain @
1846
- if (emailAddress.indexOf("@") === -1) {
1847
- emailAddress = emailAddress + "@blinq-mail.io";
1848
- }
1849
- else {
1850
- if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
1851
- throw new Error("email address should end with @blinq-mail.io");
2457
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2458
+ const state = {
2459
+ selectors,
2460
+ _params,
2461
+ property,
2462
+ variable,
2463
+ options,
2464
+ world,
2465
+ type: Types.EXTRACT_PROPERTY,
2466
+ text: `Extract property from element`,
2467
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2468
+ operation: "extractProperty",
2469
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2470
+ allowDisabled: true,
2471
+ };
2472
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2473
+ try {
2474
+ await _preCommand(state, this);
2475
+ switch (property) {
2476
+ case "inner_text":
2477
+ state.value = await state.element.innerText();
2478
+ break;
2479
+ case "href":
2480
+ state.value = await state.element.getAttribute("href");
2481
+ break;
2482
+ case "value":
2483
+ state.value = await state.element.inputValue();
2484
+ break;
2485
+ case "text":
2486
+ state.value = await state.element.textContent();
2487
+ break;
2488
+ default:
2489
+ if (property.startsWith("dataset.")) {
2490
+ const dataAttribute = property.substring(8);
2491
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2492
+ }
2493
+ else {
2494
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2495
+ }
1852
2496
  }
2497
+ if (options !== null) {
2498
+ if (options.regex && options.regex !== "") {
2499
+ // Construct a regex pattern from the provided string
2500
+ const regex = options.regex.slice(1, -1);
2501
+ const regexPattern = new RegExp(regex, "g");
2502
+ const matches = state.value.match(regexPattern);
2503
+ if (matches) {
2504
+ let newValue = "";
2505
+ for (const match of matches) {
2506
+ newValue += match;
2507
+ }
2508
+ state.value = newValue;
2509
+ }
2510
+ }
2511
+ if (options.trimSpaces && options.trimSpaces === true) {
2512
+ state.value = state.value.trim();
2513
+ }
2514
+ }
2515
+ state.info.value = state.value;
2516
+ this.setTestData({ [variable]: state.value }, world);
2517
+ this.logger.info("set test data: " + variable + "=" + state.value);
2518
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2519
+ return state.info;
1853
2520
  }
1854
- const startTime = Date.now();
1855
- let timeout = 60000;
1856
- if (options && options.timeout) {
1857
- timeout = options.timeout;
2521
+ catch (e) {
2522
+ await _commandError(state, e, this);
1858
2523
  }
1859
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1860
- const request = {
1861
- method: "POST",
1862
- url: serviceUrl,
1863
- headers: {
1864
- "Content-Type": "application/json",
1865
- Authorization: `Bearer ${process.env.TOKEN}`,
1866
- },
1867
- data: JSON.stringify({
1868
- email: emailAddress,
1869
- }),
2524
+ finally {
2525
+ await _commandFinally(state, this);
2526
+ }
2527
+ }
2528
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2529
+ const state = {
2530
+ selectors,
2531
+ _params,
2532
+ attribute,
2533
+ value,
2534
+ options,
2535
+ world,
2536
+ type: Types.VERIFY_ATTRIBUTE,
2537
+ highlight: true,
2538
+ screenshot: true,
2539
+ text: `Verify element attribute`,
2540
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2541
+ operation: "verifyAttribute",
2542
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2543
+ allowDisabled: true,
1870
2544
  };
1871
- let errorCount = 0;
2545
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2546
+ let val;
2547
+ let expectedValue;
2548
+ try {
2549
+ await _preCommand(state, this);
2550
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2551
+ state.info.expectedValue = expectedValue;
2552
+ switch (attribute) {
2553
+ case "innerText":
2554
+ val = String(await state.element.innerText());
2555
+ break;
2556
+ case "text":
2557
+ val = String(await state.element.textContent());
2558
+ break;
2559
+ case "value":
2560
+ val = String(await state.element.inputValue());
2561
+ break;
2562
+ case "checked":
2563
+ val = String(await state.element.isChecked());
2564
+ break;
2565
+ case "disabled":
2566
+ val = String(await state.element.isDisabled());
2567
+ break;
2568
+ case "readOnly":
2569
+ const isEditable = await state.element.isEditable();
2570
+ val = String(!isEditable);
2571
+ break;
2572
+ default:
2573
+ val = String(await state.element.getAttribute(attribute));
2574
+ break;
2575
+ }
2576
+ state.info.value = val;
2577
+ let regex;
2578
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2579
+ const patternBody = expectedValue.slice(1, -1);
2580
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2581
+ regex = new RegExp(processedPattern, "gs");
2582
+ state.info.regex = true;
2583
+ }
2584
+ else {
2585
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2586
+ regex = new RegExp(escapedPattern, "g");
2587
+ }
2588
+ if (attribute === "innerText") {
2589
+ if (state.info.regex) {
2590
+ if (!regex.test(val)) {
2591
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2592
+ state.info.failCause.assertionFailed = true;
2593
+ state.info.failCause.lastError = errorMessage;
2594
+ throw new Error(errorMessage);
2595
+ }
2596
+ }
2597
+ else {
2598
+ const valLines = val.split("\n");
2599
+ const expectedLines = expectedValue.split("\n");
2600
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2601
+ if (!isPart) {
2602
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2603
+ state.info.failCause.assertionFailed = true;
2604
+ state.info.failCause.lastError = errorMessage;
2605
+ throw new Error(errorMessage);
2606
+ }
2607
+ }
2608
+ }
2609
+ else {
2610
+ if (!val.match(regex)) {
2611
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2612
+ state.info.failCause.assertionFailed = true;
2613
+ state.info.failCause.lastError = errorMessage;
2614
+ throw new Error(errorMessage);
2615
+ }
2616
+ }
2617
+ return state.info;
2618
+ }
2619
+ catch (e) {
2620
+ await _commandError(state, e, this);
2621
+ }
2622
+ finally {
2623
+ await _commandFinally(state, this);
2624
+ }
2625
+ }
2626
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2627
+ const state = {
2628
+ selectors,
2629
+ _params,
2630
+ property,
2631
+ value,
2632
+ options,
2633
+ world,
2634
+ type: Types.VERIFY_PROPERTY,
2635
+ highlight: true,
2636
+ screenshot: true,
2637
+ text: `Verify element property`,
2638
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2639
+ operation: "verifyProperty",
2640
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2641
+ allowDisabled: true,
2642
+ };
2643
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2644
+ let val;
2645
+ let expectedValue;
2646
+ try {
2647
+ await _preCommand(state, this);
2648
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2649
+ state.info.expectedValue = expectedValue;
2650
+ switch (property) {
2651
+ case "innerText":
2652
+ val = String(await state.element.innerText());
2653
+ break;
2654
+ case "text":
2655
+ val = String(await state.element.textContent());
2656
+ break;
2657
+ case "value":
2658
+ val = String(await state.element.inputValue());
2659
+ break;
2660
+ case "checked":
2661
+ val = String(await state.element.isChecked());
2662
+ break;
2663
+ case "disabled":
2664
+ val = String(await state.element.isDisabled());
2665
+ break;
2666
+ case "readOnly":
2667
+ const isEditable = await state.element.isEditable();
2668
+ val = String(!isEditable);
2669
+ break;
2670
+ case "innerHTML":
2671
+ val = String(await state.element.innerHTML());
2672
+ break;
2673
+ case "outerHTML":
2674
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2675
+ break;
2676
+ default:
2677
+ if (property.startsWith("dataset.")) {
2678
+ const dataAttribute = property.substring(8);
2679
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2680
+ }
2681
+ else {
2682
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2683
+ }
2684
+ }
2685
+ // Helper function to remove all style="" attributes
2686
+ const removeStyleAttributes = (htmlString) => {
2687
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2688
+ };
2689
+ // Remove style attributes for innerHTML and outerHTML properties
2690
+ if (property === "innerHTML" || property === "outerHTML") {
2691
+ val = removeStyleAttributes(val);
2692
+ expectedValue = removeStyleAttributes(expectedValue);
2693
+ }
2694
+ state.info.value = val;
2695
+ let regex;
2696
+ state.info.value = val;
2697
+ const isRegex = expectedValue.startsWith("regex:");
2698
+ const isContains = expectedValue.startsWith("contains:");
2699
+ const isExact = expectedValue.startsWith("exact:");
2700
+ let matchPassed = false;
2701
+ if (isRegex) {
2702
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2703
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2704
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2705
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2706
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2707
+ const regex = new RegExp(patternBody, flags);
2708
+ state.info.regex = true;
2709
+ matchPassed = regex.test(val);
2710
+ }
2711
+ else {
2712
+ // Fallback: treat as literal
2713
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2714
+ const regex = new RegExp(escapedPattern, "g");
2715
+ matchPassed = regex.test(val);
2716
+ }
2717
+ }
2718
+ else if (isContains) {
2719
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2720
+ matchPassed = val.includes(containsValue);
2721
+ }
2722
+ else if (isExact) {
2723
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2724
+ matchPassed = val === exactValue;
2725
+ }
2726
+ else if (property === "innerText") {
2727
+ // Default innerText logic
2728
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2729
+ const valLines = val.split("\n");
2730
+ const expectedLines = normalizedExpectedValue.split("\n");
2731
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2732
+ }
2733
+ else {
2734
+ // Fallback exact or loose match
2735
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2736
+ const regex = new RegExp(escapedPattern, "g");
2737
+ matchPassed = regex.test(val);
2738
+ }
2739
+ if (!matchPassed) {
2740
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2741
+ state.info.failCause.assertionFailed = true;
2742
+ state.info.failCause.lastError = errorMessage;
2743
+ throw new Error(errorMessage);
2744
+ }
2745
+ return state.info;
2746
+ }
2747
+ catch (e) {
2748
+ await _commandError(state, e, this);
2749
+ }
2750
+ finally {
2751
+ await _commandFinally(state, this);
2752
+ }
2753
+ }
2754
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2755
+ // Convert timeout from seconds to milliseconds
2756
+ const timeoutMs = timeout * 1000;
2757
+ const state = {
2758
+ selectors,
2759
+ _params,
2760
+ condition,
2761
+ timeout: timeoutMs, // Store as milliseconds for internal use
2762
+ options,
2763
+ world,
2764
+ type: Types.CONDITIONAL_WAIT,
2765
+ highlight: true,
2766
+ screenshot: true,
2767
+ text: `Conditional wait for element`,
2768
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2769
+ operation: "conditionalWait",
2770
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2771
+ allowDisabled: true,
2772
+ info: {},
2773
+ };
2774
+ state.options ??= { timeout: timeoutMs };
2775
+ // Initialize startTime outside try block to ensure it's always accessible
2776
+ const startTime = Date.now();
2777
+ let conditionMet = false;
2778
+ let currentValue = null;
2779
+ let lastError = null;
2780
+ // Main retry loop - continues until timeout or condition is met
2781
+ while (Date.now() - startTime < timeoutMs) {
2782
+ const elapsedTime = Date.now() - startTime;
2783
+ const remainingTime = timeoutMs - elapsedTime;
2784
+ try {
2785
+ // Try to execute _preCommand (element location)
2786
+ await _preCommand(state, this);
2787
+ // If _preCommand succeeds, start condition checking
2788
+ const checkCondition = async () => {
2789
+ try {
2790
+ switch (condition.toLowerCase()) {
2791
+ case "checked":
2792
+ currentValue = await state.element.isChecked();
2793
+ return currentValue === true;
2794
+ case "unchecked":
2795
+ currentValue = await state.element.isChecked();
2796
+ return currentValue === false;
2797
+ case "visible":
2798
+ currentValue = await state.element.isVisible();
2799
+ return currentValue === true;
2800
+ case "hidden":
2801
+ currentValue = await state.element.isVisible();
2802
+ return currentValue === false;
2803
+ case "enabled":
2804
+ currentValue = await state.element.isDisabled();
2805
+ return currentValue === false;
2806
+ case "disabled":
2807
+ currentValue = await state.element.isDisabled();
2808
+ return currentValue === true;
2809
+ case "editable":
2810
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2811
+ currentValue = await state.element.isContentEditable();
2812
+ return currentValue === true;
2813
+ default:
2814
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2815
+ state.info.success = false;
2816
+ return false;
2817
+ }
2818
+ }
2819
+ catch (error) {
2820
+ // Don't throw here, just return false to continue retrying
2821
+ return false;
2822
+ }
2823
+ };
2824
+ // Inner loop for condition checking (once element is located)
2825
+ while (Date.now() - startTime < timeoutMs) {
2826
+ const currentElapsedTime = Date.now() - startTime;
2827
+ conditionMet = await checkCondition();
2828
+ if (conditionMet) {
2829
+ break;
2830
+ }
2831
+ // Check if we still have time for another attempt
2832
+ if (Date.now() - startTime + 50 < timeoutMs) {
2833
+ await new Promise((res) => setTimeout(res, 50));
2834
+ }
2835
+ else {
2836
+ break;
2837
+ }
2838
+ }
2839
+ // If we got here and condition is met, break out of main loop
2840
+ if (conditionMet) {
2841
+ break;
2842
+ }
2843
+ // If condition not met but no exception, we've timed out
2844
+ break;
2845
+ }
2846
+ catch (e) {
2847
+ lastError = e;
2848
+ const currentElapsedTime = Date.now() - startTime;
2849
+ const timeLeft = timeoutMs - currentElapsedTime;
2850
+ // Check if we have enough time left to retry
2851
+ if (timeLeft > 100) {
2852
+ await new Promise((resolve) => setTimeout(resolve, 50));
2853
+ }
2854
+ else {
2855
+ break;
2856
+ }
2857
+ }
2858
+ }
2859
+ const actualWaitTime = Date.now() - startTime;
2860
+ state.info = {
2861
+ success: conditionMet,
2862
+ conditionMet,
2863
+ actualWaitTime,
2864
+ currentValue,
2865
+ lastError: lastError?.message || null,
2866
+ message: conditionMet
2867
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2868
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2869
+ };
2870
+ if (lastError) {
2871
+ state.log += `Last error: ${lastError.message}\n`;
2872
+ }
2873
+ try {
2874
+ await _commandFinally(state, this);
2875
+ }
2876
+ catch (finallyError) {
2877
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2878
+ }
2879
+ return state.info;
2880
+ }
2881
+ async extractEmailData(emailAddress, options, world) {
2882
+ if (!emailAddress) {
2883
+ throw new Error("email address is null");
2884
+ }
2885
+ // check if address contain @
2886
+ if (emailAddress.indexOf("@") === -1) {
2887
+ emailAddress = emailAddress + "@blinq-mail.io";
2888
+ }
2889
+ else {
2890
+ if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
2891
+ throw new Error("email address should end with @blinq-mail.io");
2892
+ }
2893
+ }
2894
+ const startTime = Date.now();
2895
+ let timeout = 60000;
2896
+ if (options && options.timeout) {
2897
+ timeout = options.timeout;
2898
+ }
2899
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2900
+ const request = {
2901
+ method: "POST",
2902
+ url: serviceUrl,
2903
+ headers: {
2904
+ "Content-Type": "application/json",
2905
+ Authorization: `Bearer ${process.env.TOKEN}`,
2906
+ },
2907
+ data: JSON.stringify({
2908
+ email: emailAddress,
2909
+ }),
2910
+ };
2911
+ let errorCount = 0;
1872
2912
  while (true) {
1873
2913
  try {
1874
2914
  let result = await this.context.api.request(request);
@@ -1912,7 +2952,8 @@ class StableBrowser {
1912
2952
  catch (e) {
1913
2953
  errorCount++;
1914
2954
  if (errorCount > 3) {
1915
- throw e;
2955
+ // throw e;
2956
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
1916
2957
  }
1917
2958
  // ignore
1918
2959
  }
@@ -1926,27 +2967,32 @@ class StableBrowser {
1926
2967
  async _highlightElements(scope, css) {
1927
2968
  try {
1928
2969
  if (!scope) {
2970
+ // console.log(`Scope is not defined`);
1929
2971
  return;
1930
2972
  }
1931
2973
  if (!css) {
1932
2974
  scope
1933
2975
  .evaluate((node) => {
1934
2976
  if (node && node.style) {
1935
- let originalBorder = node.style.border;
1936
- node.style.border = "2px solid red";
2977
+ let originalOutline = node.style.outline;
2978
+ // console.log(`Original outline was: ${originalOutline}`);
2979
+ // node.__previousOutline = originalOutline;
2980
+ node.style.outline = "2px solid red";
2981
+ // console.log(`New outline is: ${node.style.outline}`);
1937
2982
  if (window) {
1938
2983
  window.addEventListener("beforeunload", function (e) {
1939
- node.style.border = originalBorder;
2984
+ node.style.outline = originalOutline;
1940
2985
  });
1941
2986
  }
1942
2987
  setTimeout(function () {
1943
- node.style.border = originalBorder;
2988
+ node.style.outline = originalOutline;
1944
2989
  }, 2000);
1945
2990
  }
1946
2991
  })
1947
2992
  .then(() => { })
1948
2993
  .catch((e) => {
1949
2994
  // ignore
2995
+ // console.error(`Could not highlight node : ${e}`);
1950
2996
  });
1951
2997
  }
1952
2998
  else {
@@ -1962,17 +3008,18 @@ class StableBrowser {
1962
3008
  if (!element.style) {
1963
3009
  return;
1964
3010
  }
1965
- var originalBorder = element.style.border;
3011
+ let originalOutline = element.style.outline;
3012
+ element.__previousOutline = originalOutline;
1966
3013
  // Set the new border to be red and 2px solid
1967
- element.style.border = "2px solid red";
3014
+ element.style.outline = "2px solid red";
1968
3015
  if (window) {
1969
3016
  window.addEventListener("beforeunload", function (e) {
1970
- element.style.border = originalBorder;
3017
+ element.style.outline = originalOutline;
1971
3018
  });
1972
3019
  }
1973
3020
  // Set a timeout to revert to the original border after 2 seconds
1974
3021
  setTimeout(function () {
1975
- element.style.border = originalBorder;
3022
+ element.style.outline = originalOutline;
1976
3023
  }, 2000);
1977
3024
  }
1978
3025
  return;
@@ -1980,6 +3027,7 @@ class StableBrowser {
1980
3027
  .then(() => { })
1981
3028
  .catch((e) => {
1982
3029
  // ignore
3030
+ // console.error(`Could not highlight css: ${e}`);
1983
3031
  });
1984
3032
  }
1985
3033
  }
@@ -1987,8 +3035,49 @@ class StableBrowser {
1987
3035
  console.debug(error);
1988
3036
  }
1989
3037
  }
3038
+ _matcher(text) {
3039
+ if (!text) {
3040
+ return { matcher: "contains", queryText: "" };
3041
+ }
3042
+ if (text.length < 2) {
3043
+ return { matcher: "contains", queryText: text };
3044
+ }
3045
+ const split = text.split(":");
3046
+ const matcher = split[0].toLowerCase();
3047
+ const queryText = split.slice(1).join(":").trim();
3048
+ return { matcher, queryText };
3049
+ }
3050
+ _getDomain(url) {
3051
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
3052
+ return "";
3053
+ }
3054
+ let hostnameFragments = url.split("/")[2].split(".");
3055
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
3056
+ return hostnameFragments.join("-").split(":").join("-");
3057
+ }
3058
+ let n = hostnameFragments.length;
3059
+ let fragments = [...hostnameFragments];
3060
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
3061
+ hostnameFragments.pop();
3062
+ n = hostnameFragments.length;
3063
+ }
3064
+ if (n == 0) {
3065
+ if (fragments[0] === "www")
3066
+ fragments = fragments.slice(1);
3067
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
3068
+ }
3069
+ if (hostnameFragments[0] === "www")
3070
+ hostnameFragments = hostnameFragments.slice(1);
3071
+ return hostnameFragments.join(".");
3072
+ }
3073
+ /**
3074
+ * Verify the page path matches the given path.
3075
+ * @param {string} pathPart - The path to verify.
3076
+ * @param {object} options - Options for verification.
3077
+ * @param {object} world - The world context.
3078
+ * @returns {Promise<object>} - The state info after verification.
3079
+ */
1990
3080
  async verifyPagePath(pathPart, options = {}, world = null) {
1991
- const startTime = Date.now();
1992
3081
  let error = null;
1993
3082
  let screenshotId = null;
1994
3083
  let screenshotPath = null;
@@ -2002,159 +3091,520 @@ class StableBrowser {
2002
3091
  pathPart = newValue;
2003
3092
  }
2004
3093
  info.pathPart = pathPart;
3094
+ const { matcher, queryText } = this._matcher(pathPart);
3095
+ const state = {
3096
+ text_search: queryText,
3097
+ options,
3098
+ world,
3099
+ locate: false,
3100
+ scroll: false,
3101
+ highlight: false,
3102
+ type: Types.VERIFY_PAGE_PATH,
3103
+ text: `Verify the page url is ${queryText}`,
3104
+ _text: `Verify the page url is ${queryText}`,
3105
+ operation: "verifyPagePath",
3106
+ log: "***** verify page url is " + queryText + " *****\n",
3107
+ };
2005
3108
  try {
3109
+ await _preCommand(state, this);
3110
+ state.info.text = queryText;
2006
3111
  for (let i = 0; i < 30; i++) {
2007
3112
  const url = await this.page.url();
2008
- if (!url.includes(pathPart)) {
2009
- if (i === 29) {
2010
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
3113
+ switch (matcher) {
3114
+ case "exact":
3115
+ if (url !== queryText) {
3116
+ if (i === 29) {
3117
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
3118
+ }
3119
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3120
+ continue;
3121
+ }
3122
+ break;
3123
+ case "contains":
3124
+ if (!url.includes(queryText)) {
3125
+ if (i === 29) {
3126
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
3127
+ }
3128
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3129
+ continue;
3130
+ }
3131
+ break;
3132
+ case "starts-with":
3133
+ {
3134
+ const domain = this._getDomain(url);
3135
+ if (domain.length > 0 && domain !== queryText) {
3136
+ if (i === 29) {
3137
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
3138
+ }
3139
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3140
+ continue;
3141
+ }
3142
+ }
3143
+ break;
3144
+ case "ends-with":
3145
+ {
3146
+ const urlObj = new URL(url);
3147
+ let route = "/";
3148
+ if (urlObj.pathname !== "/") {
3149
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
3150
+ }
3151
+ else {
3152
+ route = "/";
3153
+ }
3154
+ if (route !== queryText) {
3155
+ if (i === 29) {
3156
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
3157
+ }
3158
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3159
+ continue;
3160
+ }
3161
+ }
3162
+ break;
3163
+ case "regex":
3164
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3165
+ if (!regex.test(url)) {
3166
+ if (i === 29) {
3167
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
3168
+ }
3169
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3170
+ continue;
3171
+ }
3172
+ break;
3173
+ default:
3174
+ console.log("Unknown matching type, defaulting to contains matching");
3175
+ if (!url.includes(pathPart)) {
3176
+ if (i === 29) {
3177
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
3178
+ }
3179
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3180
+ continue;
3181
+ }
3182
+ }
3183
+ await _screenshot(state, this);
3184
+ return state.info;
3185
+ }
3186
+ }
3187
+ catch (e) {
3188
+ state.info.failCause.lastError = e.message;
3189
+ state.info.failCause.assertionFailed = true;
3190
+ await _commandError(state, e, this);
3191
+ }
3192
+ finally {
3193
+ await _commandFinally(state, this);
3194
+ }
3195
+ }
3196
+ /**
3197
+ * Verify the page title matches the given title.
3198
+ * @param {string} title - The title to verify.
3199
+ * @param {object} options - Options for verification.
3200
+ * @param {object} world - The world context.
3201
+ * @returns {Promise<object>} - The state info after verification.
3202
+ */
3203
+ async verifyPageTitle(title, options = {}, world = null) {
3204
+ let error = null;
3205
+ let screenshotId = null;
3206
+ let screenshotPath = null;
3207
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3208
+ const newValue = await this._replaceWithLocalData(title, world);
3209
+ if (newValue !== title) {
3210
+ this.logger.info(title + "=" + newValue);
3211
+ title = newValue;
3212
+ }
3213
+ const { matcher, queryText } = this._matcher(title);
3214
+ const state = {
3215
+ text_search: queryText,
3216
+ options,
3217
+ world,
3218
+ locate: false,
3219
+ scroll: false,
3220
+ highlight: false,
3221
+ type: Types.VERIFY_PAGE_TITLE,
3222
+ text: `Verify the page title is ${queryText}`,
3223
+ _text: `Verify the page title is ${queryText}`,
3224
+ operation: "verifyPageTitle",
3225
+ log: "***** verify page title is " + queryText + " *****\n",
3226
+ };
3227
+ try {
3228
+ await _preCommand(state, this);
3229
+ state.info.text = queryText;
3230
+ for (let i = 0; i < 30; i++) {
3231
+ const foundTitle = await this.page.title();
3232
+ switch (matcher) {
3233
+ case "exact":
3234
+ if (foundTitle !== queryText) {
3235
+ if (i === 29) {
3236
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
3237
+ }
3238
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3239
+ continue;
3240
+ }
3241
+ break;
3242
+ case "contains":
3243
+ if (!foundTitle.includes(queryText)) {
3244
+ if (i === 29) {
3245
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
3246
+ }
3247
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3248
+ continue;
3249
+ }
3250
+ break;
3251
+ case "starts-with":
3252
+ if (!foundTitle.startsWith(queryText)) {
3253
+ if (i === 29) {
3254
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
3255
+ }
3256
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3257
+ continue;
3258
+ }
3259
+ break;
3260
+ case "ends-with":
3261
+ if (!foundTitle.endsWith(queryText)) {
3262
+ if (i === 29) {
3263
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
3264
+ }
3265
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3266
+ continue;
3267
+ }
3268
+ break;
3269
+ case "regex":
3270
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3271
+ if (!regex.test(foundTitle)) {
3272
+ if (i === 29) {
3273
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
3274
+ }
3275
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3276
+ continue;
3277
+ }
3278
+ break;
3279
+ default:
3280
+ console.log("Unknown matching type, defaulting to contains matching");
3281
+ if (!foundTitle.includes(title)) {
3282
+ if (i === 29) {
3283
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
3284
+ }
3285
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3286
+ continue;
3287
+ }
3288
+ }
3289
+ await _screenshot(state, this);
3290
+ return state.info;
3291
+ }
3292
+ }
3293
+ catch (e) {
3294
+ state.info.failCause.lastError = e.message;
3295
+ state.info.failCause.assertionFailed = true;
3296
+ await _commandError(state, e, this);
3297
+ }
3298
+ finally {
3299
+ await _commandFinally(state, this);
3300
+ }
3301
+ }
3302
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
3303
+ const frames = this.page.frames();
3304
+ let results = [];
3305
+ // let ignoreCase = false;
3306
+ for (let i = 0; i < frames.length; i++) {
3307
+ if (dateAlternatives.date) {
3308
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
3309
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3310
+ result.frame = frames[i];
3311
+ results.push(result);
3312
+ }
3313
+ }
3314
+ else if (numberAlternatives.number) {
3315
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
3316
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3317
+ result.frame = frames[i];
3318
+ results.push(result);
3319
+ }
3320
+ }
3321
+ else {
3322
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
3323
+ result.frame = frames[i];
3324
+ results.push(result);
3325
+ }
3326
+ }
3327
+ state.info.results = results;
3328
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3329
+ return resultWithElementsFound;
3330
+ }
3331
+ async verifyTextExistInPage(text, options = {}, world = null) {
3332
+ text = unEscapeString(text);
3333
+ const state = {
3334
+ text_search: text,
3335
+ options,
3336
+ world,
3337
+ locate: false,
3338
+ scroll: false,
3339
+ highlight: false,
3340
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
3341
+ text: `Verify the text '${maskValue(text)}' exists in page`,
3342
+ _text: `Verify the text '${text}' exists in page`,
3343
+ operation: "verifyTextExistInPage",
3344
+ log: "***** verify text " + text + " exists in page *****\n",
3345
+ };
3346
+ if (testForRegex(text)) {
3347
+ text = text.replace(/\\"/g, '"');
3348
+ }
3349
+ const timeout = this._getFindElementTimeout(options);
3350
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3351
+ const newValue = await this._replaceWithLocalData(text, world);
3352
+ if (newValue !== text) {
3353
+ this.logger.info(text + "=" + newValue);
3354
+ text = newValue;
3355
+ }
3356
+ let dateAlternatives = findDateAlternatives(text);
3357
+ let numberAlternatives = findNumberAlternatives(text);
3358
+ try {
3359
+ await _preCommand(state, this);
3360
+ state.info.text = text;
3361
+ while (true) {
3362
+ let resultWithElementsFound = {
3363
+ length: 0,
3364
+ };
3365
+ try {
3366
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3367
+ }
3368
+ catch (error) {
3369
+ // ignore
3370
+ }
3371
+ if (resultWithElementsFound.length === 0) {
3372
+ if (Date.now() - state.startTime > timeout) {
3373
+ throw new Error(`Text ${text} not found in page`);
2011
3374
  }
2012
3375
  await new Promise((resolve) => setTimeout(resolve, 1000));
2013
3376
  continue;
2014
3377
  }
2015
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2016
- return info;
3378
+ try {
3379
+ if (resultWithElementsFound[0].randomToken) {
3380
+ const frame = resultWithElementsFound[0].frame;
3381
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
3382
+ await this._highlightElements(frame, dataAttribute);
3383
+ const element = await frame.locator(dataAttribute).first();
3384
+ if (element) {
3385
+ await this.scrollIfNeeded(element, state.info);
3386
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3387
+ }
3388
+ }
3389
+ await _screenshot(state, this);
3390
+ return state.info;
3391
+ }
3392
+ catch (error) {
3393
+ console.error(error);
3394
+ }
2017
3395
  }
2018
3396
  }
2019
3397
  catch (e) {
2020
- //await this.closeUnexpectedPopups();
2021
- this.logger.error("verify page path failed " + info.log);
2022
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2023
- info.screenshotPath = screenshotPath;
2024
- Object.assign(e, { info: info });
2025
- error = e;
2026
- throw e;
3398
+ await _commandError(state, e, this);
2027
3399
  }
2028
3400
  finally {
2029
- const endTime = Date.now();
2030
- this._reportToWorld(world, {
2031
- type: Types.VERIFY_PAGE_PATH,
2032
- text: "Verify page path",
2033
- screenshotId,
2034
- result: error
2035
- ? {
2036
- status: "FAILED",
2037
- startTime,
2038
- endTime,
2039
- message: error === null || error === void 0 ? void 0 : error.message,
2040
- }
2041
- : {
2042
- status: "PASSED",
2043
- startTime,
2044
- endTime,
2045
- },
2046
- info: info,
2047
- });
3401
+ await _commandFinally(state, this);
3402
+ }
3403
+ }
3404
+ async waitForTextToDisappear(text, options = {}, world = null) {
3405
+ text = unEscapeString(text);
3406
+ const state = {
3407
+ text_search: text,
3408
+ options,
3409
+ world,
3410
+ locate: false,
3411
+ scroll: false,
3412
+ highlight: false,
3413
+ type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
3414
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
3415
+ _text: `Verify the text '${text}' does not exist in page`,
3416
+ operation: "verifyTextNotExistInPage",
3417
+ log: "***** verify text " + text + " does not exist in page *****\n",
3418
+ };
3419
+ if (testForRegex(text)) {
3420
+ text = text.replace(/\\"/g, '"');
3421
+ }
3422
+ const timeout = this._getFindElementTimeout(options);
3423
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3424
+ const newValue = await this._replaceWithLocalData(text, world);
3425
+ if (newValue !== text) {
3426
+ this.logger.info(text + "=" + newValue);
3427
+ text = newValue;
3428
+ }
3429
+ let dateAlternatives = findDateAlternatives(text);
3430
+ let numberAlternatives = findNumberAlternatives(text);
3431
+ try {
3432
+ await _preCommand(state, this);
3433
+ state.info.text = text;
3434
+ let resultWithElementsFound = {
3435
+ length: null, // initial cannot be 0
3436
+ };
3437
+ while (true) {
3438
+ try {
3439
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3440
+ }
3441
+ catch (error) {
3442
+ // ignore
3443
+ }
3444
+ if (resultWithElementsFound.length === 0) {
3445
+ await _screenshot(state, this);
3446
+ return state.info;
3447
+ }
3448
+ if (Date.now() - state.startTime > timeout) {
3449
+ throw new Error(`Text ${text} found in page`);
3450
+ }
3451
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3452
+ }
3453
+ }
3454
+ catch (e) {
3455
+ await _commandError(state, e, this);
3456
+ }
3457
+ finally {
3458
+ await _commandFinally(state, this);
2048
3459
  }
2049
3460
  }
2050
- async verifyTextExistInPage(text, options = {}, world = null) {
2051
- text = unEscapeString(text);
2052
- const startTime = Date.now();
2053
- const timeout = this._getLoadTimeout(options);
2054
- let error = null;
2055
- let screenshotId = null;
2056
- let screenshotPath = null;
3461
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
3462
+ textAnchor = unEscapeString(textAnchor);
3463
+ textToVerify = unEscapeString(textToVerify);
3464
+ const state = {
3465
+ text_search: textToVerify,
3466
+ options,
3467
+ world,
3468
+ locate: false,
3469
+ scroll: false,
3470
+ highlight: false,
3471
+ type: Types.VERIFY_TEXT_WITH_RELATION,
3472
+ text: `Verify text with relation to another text`,
3473
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
3474
+ operation: "verify_text_with_relation",
3475
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3476
+ };
3477
+ const cmdStartTime = Date.now();
3478
+ let cmdEndTime = null;
3479
+ const timeout = this._getFindElementTimeout(options);
2057
3480
  await new Promise((resolve) => setTimeout(resolve, 2000));
2058
- const info = {};
2059
- info.log = "***** verify text " + text + " exists in page *****\n";
2060
- info.operation = "verifyTextExistInPage";
2061
- const newValue = await this._replaceWithLocalData(text, world);
2062
- if (newValue !== text) {
2063
- this.logger.info(text + "=" + newValue);
2064
- text = newValue;
2065
- }
2066
- info.text = text;
2067
- let dateAlternatives = findDateAlternatives(text);
2068
- let numberAlternatives = findNumberAlternatives(text);
3481
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
3482
+ if (newValue !== textAnchor) {
3483
+ this.logger.info(textAnchor + "=" + newValue);
3484
+ textAnchor = newValue;
3485
+ }
3486
+ newValue = await this._replaceWithLocalData(textToVerify, world);
3487
+ if (newValue !== textToVerify) {
3488
+ this.logger.info(textToVerify + "=" + newValue);
3489
+ textToVerify = newValue;
3490
+ }
3491
+ let dateAlternatives = findDateAlternatives(textToVerify);
3492
+ let numberAlternatives = findNumberAlternatives(textToVerify);
3493
+ let foundAncore = false;
2069
3494
  try {
3495
+ await _preCommand(state, this);
3496
+ state.info.text = textToVerify;
3497
+ let resultWithElementsFound = {
3498
+ length: 0,
3499
+ };
2070
3500
  while (true) {
2071
- const frames = this.page.frames();
2072
- let results = [];
2073
- for (let i = 0; i < frames.length; i++) {
2074
- if (dateAlternatives.date) {
2075
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2076
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
2077
- result.frame = frames[i];
2078
- results.push(result);
2079
- }
2080
- }
2081
- else if (numberAlternatives.number) {
2082
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2083
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
2084
- result.frame = frames[i];
2085
- results.push(result);
2086
- }
2087
- }
2088
- else {
2089
- const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
2090
- result.frame = frames[i];
2091
- results.push(result);
2092
- }
3501
+ try {
3502
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
3503
+ }
3504
+ catch (error) {
3505
+ // ignore
2093
3506
  }
2094
- info.results = results;
2095
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2096
3507
  if (resultWithElementsFound.length === 0) {
2097
- if (Date.now() - startTime > timeout) {
2098
- throw new Error(`Text ${text} not found in page`);
3508
+ if (Date.now() - state.startTime > timeout) {
3509
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2099
3510
  }
2100
3511
  await new Promise((resolve) => setTimeout(resolve, 1000));
2101
3512
  continue;
2102
3513
  }
2103
- if (resultWithElementsFound[0].randomToken) {
2104
- const frame = resultWithElementsFound[0].frame;
2105
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2106
- await this._highlightElements(frame, dataAttribute);
2107
- const element = await frame.$(dataAttribute);
2108
- if (element) {
2109
- await this.scrollIfNeeded(element, info);
2110
- await element.dispatchEvent("bvt_verify_page_contains_text");
3514
+ else {
3515
+ cmdEndTime = Date.now();
3516
+ if (cmdEndTime - cmdStartTime > 55000) {
3517
+ if (foundAncore) {
3518
+ throw new Error(`Text ${textToVerify} not found in page`);
3519
+ }
3520
+ else {
3521
+ throw new Error(`Text ${textAnchor} not found in page`);
3522
+ }
3523
+ }
3524
+ }
3525
+ try {
3526
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
3527
+ foundAncore = true;
3528
+ const result = resultWithElementsFound[i];
3529
+ const token = result.randomToken;
3530
+ const frame = result.frame;
3531
+ let css = `[data-blinq-id-${token}]`;
3532
+ const climbArray1 = [];
3533
+ for (let i = 0; i < climb; i++) {
3534
+ climbArray1.push("..");
3535
+ }
3536
+ let climbXpath = "xpath=" + climbArray1.join("/");
3537
+ css = css + " >> " + climbXpath;
3538
+ const count = await frame.locator(css).count();
3539
+ for (let j = 0; j < count; j++) {
3540
+ const continer = await frame.locator(css).nth(j);
3541
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
3542
+ if (result.elementCount > 0) {
3543
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
3544
+ await this._highlightElements(frame, dataAttribute);
3545
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
3546
+ // if (world && world.screenshot && !world.screenshotPath) {
3547
+ // console.log(`Highlighting for vtrt while running from recorder`);
3548
+ // this._highlightElements(frame, dataAttribute)
3549
+ // .then(async () => {
3550
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
3551
+ // this._unhighlightElements(frame, dataAttribute).then(
3552
+ // () => {}
3553
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
3554
+ // );
3555
+ // })
3556
+ // .catch(e);
3557
+ // }
3558
+ //await this._highlightElements(frame, cssAnchor);
3559
+ const element = await frame.locator(dataAttribute).first();
3560
+ // await new Promise((resolve) => setTimeout(resolve, 100));
3561
+ // await this._unhighlightElements(frame, dataAttribute);
3562
+ if (element) {
3563
+ await this.scrollIfNeeded(element, state.info);
3564
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3565
+ }
3566
+ await _screenshot(state, this);
3567
+ return state.info;
3568
+ }
3569
+ }
2111
3570
  }
2112
3571
  }
2113
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2114
- return info;
3572
+ catch (error) {
3573
+ console.error(error);
3574
+ }
2115
3575
  }
2116
3576
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2117
3577
  }
2118
3578
  catch (e) {
2119
- //await this.closeUnexpectedPopups();
2120
- this.logger.error("verify text exist in page failed " + info.log);
2121
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2122
- info.screenshotPath = screenshotPath;
2123
- Object.assign(e, { info: info });
2124
- error = e;
2125
- throw e;
3579
+ await _commandError(state, e, this);
2126
3580
  }
2127
3581
  finally {
2128
- const endTime = Date.now();
2129
- this._reportToWorld(world, {
2130
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2131
- text: "Verify text exists in page",
2132
- screenshotId,
2133
- result: error
2134
- ? {
2135
- status: "FAILED",
2136
- startTime,
2137
- endTime,
2138
- message: error === null || error === void 0 ? void 0 : error.message,
2139
- }
2140
- : {
2141
- status: "PASSED",
2142
- startTime,
2143
- endTime,
2144
- },
2145
- info: info,
2146
- });
3582
+ await _commandFinally(state, this);
2147
3583
  }
2148
3584
  }
2149
- _getServerUrl() {
2150
- let serviceUrl = "https://api.blinq.io";
2151
- if (process.env.NODE_ENV_BLINQ === "dev") {
2152
- serviceUrl = "https://dev.api.blinq.io";
2153
- }
2154
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2155
- serviceUrl = "https://stage.api.blinq.io";
3585
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
3586
+ const frames = this.page.frames();
3587
+ let results = [];
3588
+ let ignoreCase = false;
3589
+ for (let i = 0; i < frames.length; i++) {
3590
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
3591
+ result.frame = frames[i];
3592
+ const climbArray = [];
3593
+ for (let i = 0; i < climb; i++) {
3594
+ climbArray.push("..");
3595
+ }
3596
+ let climbXpath = "xpath=" + climbArray.join("/");
3597
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
3598
+ const count = await frames[i].locator(newLocator).count();
3599
+ if (count > 0) {
3600
+ result.elementCount = count;
3601
+ result.locator = newLocator;
3602
+ results.push(result);
3603
+ }
2156
3604
  }
2157
- return serviceUrl;
3605
+ // state.info.results = results;
3606
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3607
+ return resultWithElementsFound;
2158
3608
  }
2159
3609
  async visualVerification(text, options = {}, world = null) {
2160
3610
  const startTime = Date.now();
@@ -2170,14 +3620,17 @@ class StableBrowser {
2170
3620
  throw new Error("TOKEN is not set");
2171
3621
  }
2172
3622
  try {
2173
- let serviceUrl = this._getServerUrl();
3623
+ let serviceUrl = _getServerUrl();
2174
3624
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2175
3625
  info.screenshotPath = screenshotPath;
2176
3626
  const screenshot = await this.takeScreenshot();
2177
- const request = {
2178
- method: "POST",
3627
+ let request = {
3628
+ method: "post",
3629
+ maxBodyLength: Infinity,
2179
3630
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2180
3631
  headers: {
3632
+ "x-bvt-project-id": path.basename(this.project_path),
3633
+ "x-source": "aaa",
2181
3634
  "Content-Type": "application/json",
2182
3635
  Authorization: `Bearer ${process.env.TOKEN}`,
2183
3636
  },
@@ -2186,7 +3639,7 @@ class StableBrowser {
2186
3639
  screenshot: screenshot,
2187
3640
  }),
2188
3641
  };
2189
- let result = await this.context.api.request(request);
3642
+ const result = await axios.request(request);
2190
3643
  if (result.data.status !== true) {
2191
3644
  throw new Error("Visual validation failed");
2192
3645
  }
@@ -2206,20 +3659,22 @@ class StableBrowser {
2206
3659
  info.screenshotPath = screenshotPath;
2207
3660
  Object.assign(e, { info: info });
2208
3661
  error = e;
2209
- throw e;
3662
+ // throw e;
3663
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
2210
3664
  }
2211
3665
  finally {
2212
3666
  const endTime = Date.now();
2213
- this._reportToWorld(world, {
3667
+ _reportToWorld(world, {
2214
3668
  type: Types.VERIFY_VISUAL,
2215
3669
  text: "Visual verification",
3670
+ _text: "Visual verification of " + text,
2216
3671
  screenshotId,
2217
3672
  result: error
2218
3673
  ? {
2219
3674
  status: "FAILED",
2220
3675
  startTime,
2221
3676
  endTime,
2222
- message: error === null || error === void 0 ? void 0 : error.message,
3677
+ message: error?.message,
2223
3678
  }
2224
3679
  : {
2225
3680
  status: "PASSED",
@@ -2258,6 +3713,7 @@ class StableBrowser {
2258
3713
  let screenshotPath = null;
2259
3714
  const info = {};
2260
3715
  info.log = "";
3716
+ info.locatorLog = new LocatorLog(selectors);
2261
3717
  info.operation = "getTableData";
2262
3718
  info.selectors = selectors;
2263
3719
  try {
@@ -2273,11 +3729,12 @@ class StableBrowser {
2273
3729
  info.screenshotPath = screenshotPath;
2274
3730
  Object.assign(e, { info: info });
2275
3731
  error = e;
2276
- throw e;
3732
+ // throw e;
3733
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2277
3734
  }
2278
3735
  finally {
2279
3736
  const endTime = Date.now();
2280
- this._reportToWorld(world, {
3737
+ _reportToWorld(world, {
2281
3738
  element_name: selectors.element_name,
2282
3739
  type: Types.GET_TABLE_DATA,
2283
3740
  text: "Get table data",
@@ -2287,7 +3744,7 @@ class StableBrowser {
2287
3744
  status: "FAILED",
2288
3745
  startTime,
2289
3746
  endTime,
2290
- message: error === null || error === void 0 ? void 0 : error.message,
3747
+ message: error?.message,
2291
3748
  }
2292
3749
  : {
2293
3750
  status: "PASSED",
@@ -2332,7 +3789,7 @@ class StableBrowser {
2332
3789
  info.operation = "analyzeTable";
2333
3790
  info.selectors = selectors;
2334
3791
  info.query = query;
2335
- query = this._fixUsingParams(query, _params);
3792
+ query = _fixUsingParams(query, _params);
2336
3793
  info.query_fixed = query;
2337
3794
  info.operator = operator;
2338
3795
  info.value = value;
@@ -2438,11 +3895,12 @@ class StableBrowser {
2438
3895
  info.screenshotPath = screenshotPath;
2439
3896
  Object.assign(e, { info: info });
2440
3897
  error = e;
2441
- throw e;
3898
+ // throw e;
3899
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2442
3900
  }
2443
3901
  finally {
2444
3902
  const endTime = Date.now();
2445
- this._reportToWorld(world, {
3903
+ _reportToWorld(world, {
2446
3904
  element_name: selectors.element_name,
2447
3905
  type: Types.ANALYZE_TABLE,
2448
3906
  text: "Analyze table",
@@ -2452,7 +3910,7 @@ class StableBrowser {
2452
3910
  status: "FAILED",
2453
3911
  startTime,
2454
3912
  endTime,
2455
- message: error === null || error === void 0 ? void 0 : error.message,
3913
+ message: error?.message,
2456
3914
  }
2457
3915
  : {
2458
3916
  status: "PASSED",
@@ -2463,28 +3921,51 @@ class StableBrowser {
2463
3921
  });
2464
3922
  }
2465
3923
  }
2466
- async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2467
- if (!value) {
2468
- return value;
2469
- }
2470
- // find all the accurance of {{(.*?)}} and replace with the value
2471
- let regex = /{{(.*?)}}/g;
2472
- let matches = value.match(regex);
2473
- if (matches) {
2474
- const testData = this.getTestData(world);
2475
- for (let i = 0; i < matches.length; i++) {
2476
- let match = matches[i];
2477
- let key = match.substring(2, match.length - 2);
2478
- let newValue = objectPath.get(testData, key, null);
2479
- if (newValue !== null) {
2480
- value = value.replace(match, newValue);
2481
- }
3924
+ /**
3925
+ * Explicit wait/sleep function that pauses execution for a specified duration
3926
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3927
+ * @param options - Optional configuration object
3928
+ * @param world - Optional world context
3929
+ * @returns Promise that resolves after the specified duration
3930
+ */
3931
+ async sleep(duration = 1000, options = {}, world = null) {
3932
+ const state = {
3933
+ duration,
3934
+ options,
3935
+ world,
3936
+ locate: false,
3937
+ scroll: false,
3938
+ screenshot: false,
3939
+ highlight: false,
3940
+ type: Types.SLEEP,
3941
+ text: `Sleep for ${duration} ms`,
3942
+ _text: `Sleep for ${duration} ms`,
3943
+ operation: "sleep",
3944
+ log: `***** Sleep for ${duration} ms *****\n`,
3945
+ };
3946
+ try {
3947
+ await _preCommand(state, this);
3948
+ if (duration < 0) {
3949
+ throw new Error("Sleep duration cannot be negative");
2482
3950
  }
3951
+ await new Promise((resolve) => setTimeout(resolve, duration));
3952
+ return state.info;
2483
3953
  }
2484
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2485
- return await decrypt(value, null, totpWait);
3954
+ catch (e) {
3955
+ await _commandError(state, e, this);
3956
+ }
3957
+ finally {
3958
+ await _commandFinally(state, this);
3959
+ }
3960
+ }
3961
+ async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
3962
+ try {
3963
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3964
+ }
3965
+ catch (error) {
3966
+ this.logger.debug(error);
3967
+ throw error;
2486
3968
  }
2487
- return value;
2488
3969
  }
2489
3970
  _getLoadTimeout(options) {
2490
3971
  let timeout = 15000;
@@ -2496,7 +3977,54 @@ class StableBrowser {
2496
3977
  }
2497
3978
  return timeout;
2498
3979
  }
3980
+ _getFindElementTimeout(options) {
3981
+ if (options && options.timeout) {
3982
+ return options.timeout;
3983
+ }
3984
+ if (this.configuration.find_element_timeout) {
3985
+ return this.configuration.find_element_timeout;
3986
+ }
3987
+ return 30000;
3988
+ }
3989
+ async saveStoreState(path = null, world = null) {
3990
+ const storageState = await this.page.context().storageState();
3991
+ path = await this._replaceWithLocalData(path, this.world);
3992
+ //const testDataFile = _getDataFile(world, this.context, this);
3993
+ if (path) {
3994
+ // save { storageState: storageState } into the path
3995
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3996
+ }
3997
+ else {
3998
+ await this.setTestData({ storageState: storageState }, world);
3999
+ }
4000
+ }
4001
+ async restoreSaveState(path = null, world = null) {
4002
+ path = await this._replaceWithLocalData(path, this.world);
4003
+ await refreshBrowser(this, path, world);
4004
+ this.registerEventListeners(this.context);
4005
+ registerNetworkEvents(this.world, this, this.context, this.page);
4006
+ registerDownloadEvent(this.page, this.world, this.context);
4007
+ if (this.onRestoreSaveState) {
4008
+ this.onRestoreSaveState(path);
4009
+ }
4010
+ }
2499
4011
  async waitForPageLoad(options = {}, world = null) {
4012
+ // try {
4013
+ // let currentPagePath = null;
4014
+ // currentPagePath = new URL(this.page.url()).pathname;
4015
+ // if (this.latestPagePath) {
4016
+ // // get the currect page path and compare with the latest page path
4017
+ // if (this.latestPagePath === currentPagePath) {
4018
+ // // if the page path is the same, do not wait for page load
4019
+ // console.log("No page change: " + currentPagePath);
4020
+ // return;
4021
+ // }
4022
+ // }
4023
+ // this.latestPagePath = currentPagePath;
4024
+ // } catch (e) {
4025
+ // console.debug("Error getting current page path: ", e);
4026
+ // }
4027
+ //console.log("Waiting for page load");
2500
4028
  let timeout = this._getLoadTimeout(options);
2501
4029
  const promiseArray = [];
2502
4030
  // let waitForNetworkIdle = true;
@@ -2529,13 +4057,15 @@ class StableBrowser {
2529
4057
  else if (e.label === "domcontentloaded") {
2530
4058
  console.log("waited for the domcontent loaded timeout");
2531
4059
  }
2532
- console.log(".");
2533
4060
  }
2534
4061
  finally {
2535
- await new Promise((resolve) => setTimeout(resolve, 2000));
4062
+ await new Promise((resolve) => setTimeout(resolve, 500));
4063
+ if (options && !options.noSleep) {
4064
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4065
+ }
2536
4066
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2537
4067
  const endTime = Date.now();
2538
- this._reportToWorld(world, {
4068
+ _reportToWorld(world, {
2539
4069
  type: Types.GET_PAGE_STATUS,
2540
4070
  text: "Wait for page load",
2541
4071
  screenshotId,
@@ -2544,7 +4074,7 @@ class StableBrowser {
2544
4074
  status: "FAILED",
2545
4075
  startTime,
2546
4076
  endTime,
2547
- message: error === null || error === void 0 ? void 0 : error.message,
4077
+ message: error?.message,
2548
4078
  }
2549
4079
  : {
2550
4080
  status: "PASSED",
@@ -2555,41 +4085,133 @@ class StableBrowser {
2555
4085
  }
2556
4086
  }
2557
4087
  async closePage(options = {}, world = null) {
2558
- const startTime = Date.now();
2559
- let error = null;
2560
- let screenshotId = null;
2561
- let screenshotPath = null;
2562
- const info = {};
4088
+ const state = {
4089
+ options,
4090
+ world,
4091
+ locate: false,
4092
+ scroll: false,
4093
+ highlight: false,
4094
+ type: Types.CLOSE_PAGE,
4095
+ text: `Close page`,
4096
+ _text: `Close the page`,
4097
+ operation: "closePage",
4098
+ log: "***** close page *****\n",
4099
+ throwError: false,
4100
+ };
2563
4101
  try {
4102
+ await _preCommand(state, this);
2564
4103
  await this.page.close();
2565
4104
  }
2566
4105
  catch (e) {
2567
- console.log(".");
4106
+ await _commandError(state, e, this);
2568
4107
  }
2569
4108
  finally {
2570
- await new Promise((resolve) => setTimeout(resolve, 2000));
2571
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2572
- const endTime = Date.now();
2573
- this._reportToWorld(world, {
2574
- type: Types.CLOSE_PAGE,
2575
- text: "close page",
2576
- screenshotId,
2577
- result: error
2578
- ? {
2579
- status: "FAILED",
2580
- startTime,
2581
- endTime,
2582
- message: error === null || error === void 0 ? void 0 : error.message,
4109
+ await _commandFinally(state, this);
4110
+ }
4111
+ }
4112
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
4113
+ let operation = null;
4114
+ if (!options || !options.operation) {
4115
+ throw new Error("operation is not defined");
4116
+ }
4117
+ operation = options.operation;
4118
+ // validate operation is one of the supported operations
4119
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
4120
+ throw new Error("operation is not supported");
4121
+ }
4122
+ const state = {
4123
+ options,
4124
+ world,
4125
+ locate: false,
4126
+ scroll: false,
4127
+ highlight: false,
4128
+ type: Types.TABLE_OPERATION,
4129
+ text: `Table operation`,
4130
+ _text: `Table ${operation} operation`,
4131
+ operation: operation,
4132
+ log: "***** Table operation *****\n",
4133
+ };
4134
+ const timeout = this._getFindElementTimeout(options);
4135
+ try {
4136
+ await _preCommand(state, this);
4137
+ const start = Date.now();
4138
+ let cellArea = null;
4139
+ while (true) {
4140
+ try {
4141
+ cellArea = await _findCellArea(headerText, rowText, this, state);
4142
+ if (cellArea) {
4143
+ break;
2583
4144
  }
2584
- : {
2585
- status: "PASSED",
2586
- startTime,
2587
- endTime,
2588
- },
2589
- info: info,
2590
- });
4145
+ }
4146
+ catch (e) {
4147
+ // ignore
4148
+ }
4149
+ if (Date.now() - start > timeout) {
4150
+ throw new Error(`Cell not found in table`);
4151
+ }
4152
+ await new Promise((resolve) => setTimeout(resolve, 1000));
4153
+ }
4154
+ switch (operation) {
4155
+ case "click":
4156
+ if (!options.css) {
4157
+ // will click in the center of the cell
4158
+ let xOffset = 0;
4159
+ let yOffset = 0;
4160
+ if (options.xOffset) {
4161
+ xOffset = options.xOffset;
4162
+ }
4163
+ if (options.yOffset) {
4164
+ yOffset = options.yOffset;
4165
+ }
4166
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
4167
+ }
4168
+ else {
4169
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4170
+ if (results.length === 0) {
4171
+ throw new Error(`Element not found in cell area`);
4172
+ }
4173
+ state.element = results[0];
4174
+ await performAction("click", state.element, options, this, state, _params);
4175
+ }
4176
+ break;
4177
+ case "hover+click":
4178
+ if (!options.css) {
4179
+ throw new Error("css is not defined");
4180
+ }
4181
+ const results = await findElementsInArea(options.css, cellArea, this, options);
4182
+ if (results.length === 0) {
4183
+ throw new Error(`Element not found in cell area`);
4184
+ }
4185
+ state.element = results[0];
4186
+ await performAction("hover+click", state.element, options, this, state, _params);
4187
+ break;
4188
+ case "hover":
4189
+ if (!options.css) {
4190
+ throw new Error("css is not defined");
4191
+ }
4192
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4193
+ if (result1.length === 0) {
4194
+ throw new Error(`Element not found in cell area`);
4195
+ }
4196
+ state.element = result1[0];
4197
+ await performAction("hover", state.element, options, this, state, _params);
4198
+ break;
4199
+ default:
4200
+ throw new Error("operation is not supported");
4201
+ }
4202
+ }
4203
+ catch (e) {
4204
+ await _commandError(state, e, this);
4205
+ }
4206
+ finally {
4207
+ await _commandFinally(state, this);
2591
4208
  }
2592
4209
  }
4210
+ saveTestDataAsGlobal(options, world) {
4211
+ const dataFile = _getDataFile(world, this.context, this);
4212
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
4213
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
4214
+ }
2593
4215
  async setViewportSize(width, hight, options = {}, world = null) {
2594
4216
  const startTime = Date.now();
2595
4217
  let error = null;
@@ -2606,22 +4228,23 @@ class StableBrowser {
2606
4228
  await this.page.setViewportSize({ width: width, height: hight });
2607
4229
  }
2608
4230
  catch (e) {
2609
- console.log(".");
4231
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2610
4232
  }
2611
4233
  finally {
2612
4234
  await new Promise((resolve) => setTimeout(resolve, 2000));
2613
4235
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2614
4236
  const endTime = Date.now();
2615
- this._reportToWorld(world, {
4237
+ _reportToWorld(world, {
2616
4238
  type: Types.SET_VIEWPORT,
2617
4239
  text: "set viewport size to " + width + "x" + hight,
4240
+ _text: "Set the viewport size to " + width + "x" + hight,
2618
4241
  screenshotId,
2619
4242
  result: error
2620
4243
  ? {
2621
4244
  status: "FAILED",
2622
4245
  startTime,
2623
4246
  endTime,
2624
- message: error === null || error === void 0 ? void 0 : error.message,
4247
+ message: error?.message,
2625
4248
  }
2626
4249
  : {
2627
4250
  status: "PASSED",
@@ -2642,13 +4265,13 @@ class StableBrowser {
2642
4265
  await this.page.reload();
2643
4266
  }
2644
4267
  catch (e) {
2645
- console.log(".");
4268
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2646
4269
  }
2647
4270
  finally {
2648
4271
  await new Promise((resolve) => setTimeout(resolve, 2000));
2649
4272
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2650
4273
  const endTime = Date.now();
2651
- this._reportToWorld(world, {
4274
+ _reportToWorld(world, {
2652
4275
  type: Types.GET_PAGE_STATUS,
2653
4276
  text: "page relaod",
2654
4277
  screenshotId,
@@ -2657,7 +4280,7 @@ class StableBrowser {
2657
4280
  status: "FAILED",
2658
4281
  startTime,
2659
4282
  endTime,
2660
- message: error === null || error === void 0 ? void 0 : error.message,
4283
+ message: error?.message,
2661
4284
  }
2662
4285
  : {
2663
4286
  status: "PASSED",
@@ -2684,11 +4307,239 @@ class StableBrowser {
2684
4307
  console.log("#-#");
2685
4308
  }
2686
4309
  }
2687
- _reportToWorld(world, properties) {
2688
- if (!world || !world.attach) {
2689
- return;
4310
+ async beforeScenario(world, scenario) {
4311
+ if (world && world.attach) {
4312
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4313
+ }
4314
+ this.context.loadedRoutes = null;
4315
+ this.beforeScenarioCalled = true;
4316
+ if (scenario && scenario.pickle && scenario.pickle.name) {
4317
+ this.scenarioName = scenario.pickle.name;
4318
+ }
4319
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
4320
+ this.featureName = scenario.gherkinDocument.feature.name;
4321
+ }
4322
+ if (this.context) {
4323
+ this.context.examplesRow = extractStepExampleParameters(scenario);
4324
+ }
4325
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
4326
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
4327
+ // check if @global_test_data tag is present
4328
+ if (this.tags.includes("@global_test_data")) {
4329
+ this.saveTestDataAsGlobal({}, world);
4330
+ }
4331
+ }
4332
+ // update test data based on feature/scenario
4333
+ let envName = null;
4334
+ if (this.context && this.context.environment) {
4335
+ envName = this.context.environment.name;
4336
+ }
4337
+ if (!process.env.TEMP_RUN) {
4338
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
4339
+ }
4340
+ await loadBrunoParams(this.context, this.context.environment.name);
4341
+ }
4342
+ async afterScenario(world, scenario) { }
4343
+ async beforeStep(world, step) {
4344
+ this.stepTags = [];
4345
+ if (!this.beforeScenarioCalled) {
4346
+ this.beforeScenario(world, step);
4347
+ this.context.loadedRoutes = null;
4348
+ }
4349
+ if (this.stepIndex === undefined) {
4350
+ this.stepIndex = 0;
4351
+ }
4352
+ else {
4353
+ this.stepIndex++;
4354
+ }
4355
+ if (step && step.pickleStep && step.pickleStep.text) {
4356
+ this.stepName = step.pickleStep.text;
4357
+ let printableStepName = this.stepName;
4358
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4359
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4360
+ return `\x1b[33m"${p1}"\x1b[0m`;
4361
+ });
4362
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4363
+ }
4364
+ else if (step && step.text) {
4365
+ this.stepName = step.text;
4366
+ }
4367
+ else {
4368
+ this.stepName = "step " + this.stepIndex;
4369
+ }
4370
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4371
+ if (this.context.browserObject.context) {
4372
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
4373
+ }
4374
+ }
4375
+ if (this.initSnapshotTaken === false) {
4376
+ this.initSnapshotTaken = true;
4377
+ if (world &&
4378
+ world.attach &&
4379
+ !process.env.DISABLE_SNAPSHOT &&
4380
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4381
+ const snapshot = await this.getAriaSnapshot();
4382
+ if (snapshot) {
4383
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
4384
+ }
4385
+ }
4386
+ }
4387
+ this.context.routeResults = null;
4388
+ this.context.loadedRoutes = null;
4389
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4390
+ networkBeforeStep(this.stepName, this.context);
4391
+ }
4392
+ setStepTags(tags) {
4393
+ this.stepTags = tags;
4394
+ }
4395
+ async getAriaSnapshot() {
4396
+ try {
4397
+ // find the page url
4398
+ const url = await this.page.url();
4399
+ // extract the path from the url
4400
+ const path = new URL(url).pathname;
4401
+ // get the page title
4402
+ const title = await this.page.title();
4403
+ // go over other frams
4404
+ const frames = this.page.frames();
4405
+ const snapshots = [];
4406
+ const content = [`- path: ${path}`, `- title: ${title}`];
4407
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
4408
+ for (let i = 0; i < frames.length; i++) {
4409
+ const frame = frames[i];
4410
+ try {
4411
+ // Ensure frame is attached and has body
4412
+ const body = frame.locator("body");
4413
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4414
+ const snapshot = await body.ariaSnapshot({ timeout });
4415
+ if (!snapshot) {
4416
+ continue;
4417
+ }
4418
+ content.push(`- frame: ${i}`);
4419
+ content.push(snapshot);
4420
+ }
4421
+ catch (innerErr) {
4422
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4423
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4424
+ }
4425
+ }
4426
+ return content.join("\n");
4427
+ }
4428
+ catch (e) {
4429
+ console.log("Error in getAriaSnapshot");
4430
+ //console.debug(e);
4431
+ }
4432
+ return null;
4433
+ }
4434
+ /**
4435
+ * Sends command with custom payload to report.
4436
+ * @param commandText - Title of the command to be shown in the report.
4437
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
4438
+ * @param content - Content of the command to be shown in the report.
4439
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
4440
+ * @param world - Optional world context.
4441
+ * @public
4442
+ */
4443
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
4444
+ const state = {
4445
+ options,
4446
+ world,
4447
+ locate: false,
4448
+ scroll: false,
4449
+ screenshot: options.screenshot ?? false,
4450
+ highlight: options.highlight ?? false,
4451
+ type: Types.REPORT_COMMAND,
4452
+ text: commandText,
4453
+ _text: commandText,
4454
+ operation: "report_command",
4455
+ log: "***** " + commandText + " *****\n",
4456
+ };
4457
+ try {
4458
+ await _preCommand(state, this);
4459
+ const payload = {
4460
+ type: options.type ?? "text",
4461
+ content: content,
4462
+ screenshotId: null,
4463
+ };
4464
+ state.payload = payload;
4465
+ if (commandStatus === "FAILED") {
4466
+ state.throwError = true;
4467
+ throw new Error("Command failed");
4468
+ }
4469
+ }
4470
+ catch (e) {
4471
+ await _commandError(state, e, this);
4472
+ }
4473
+ finally {
4474
+ await _commandFinally(state, this);
4475
+ }
4476
+ }
4477
+ async afterStep(world, step) {
4478
+ this.stepName = null;
4479
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4480
+ if (this.context.browserObject.context) {
4481
+ await this.context.browserObject.context.tracing.stopChunk({
4482
+ path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
4483
+ });
4484
+ if (world && world.attach) {
4485
+ await world.attach(JSON.stringify({
4486
+ type: "trace",
4487
+ traceFilePath: `trace-${this.stepIndex}.zip`,
4488
+ }), "application/json+trace");
4489
+ }
4490
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
4491
+ }
4492
+ }
4493
+ if (this.context) {
4494
+ this.context.examplesRow = null;
4495
+ }
4496
+ if (world &&
4497
+ world.attach &&
4498
+ !process.env.DISABLE_SNAPSHOT &&
4499
+ !this.fastMode &&
4500
+ !this.stepTags.includes("fast-mode")) {
4501
+ const snapshot = await this.getAriaSnapshot();
4502
+ if (snapshot) {
4503
+ const obj = {};
4504
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4505
+ }
4506
+ }
4507
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4508
+ if (this.context.routeResults) {
4509
+ if (world && world.attach) {
4510
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4511
+ }
4512
+ }
4513
+ if (!process.env.TEMP_RUN) {
4514
+ const state = {
4515
+ world,
4516
+ locate: false,
4517
+ scroll: false,
4518
+ screenshot: true,
4519
+ highlight: true,
4520
+ type: Types.STEP_COMPLETE,
4521
+ text: "end of scenario",
4522
+ _text: "end of scenario",
4523
+ operation: "step_complete",
4524
+ log: "***** " + "end of scenario" + " *****\n",
4525
+ };
4526
+ try {
4527
+ await _preCommand(state, this);
4528
+ }
4529
+ catch (e) {
4530
+ await _commandError(state, e, this);
4531
+ }
4532
+ finally {
4533
+ await _commandFinally(state, this);
4534
+ }
4535
+ }
4536
+ networkAfterStep(this.stepName, this.context);
4537
+ if (process.env.TEMP_RUN === "true") {
4538
+ // Put a sleep for some time to allow the browser to finish processing
4539
+ if (!this.stepTags.includes("fast-mode")) {
4540
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4541
+ }
2690
4542
  }
2691
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2692
4543
  }
2693
4544
  }
2694
4545
  function createTimedPromise(promise, label) {
@@ -2696,156 +4547,5 @@ function createTimedPromise(promise, label) {
2696
4547
  .then((result) => ({ status: "fulfilled", label, result }))
2697
4548
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2698
4549
  }
2699
- const KEYBOARD_EVENTS = [
2700
- "ALT",
2701
- "AltGraph",
2702
- "CapsLock",
2703
- "Control",
2704
- "Fn",
2705
- "FnLock",
2706
- "Hyper",
2707
- "Meta",
2708
- "NumLock",
2709
- "ScrollLock",
2710
- "Shift",
2711
- "Super",
2712
- "Symbol",
2713
- "SymbolLock",
2714
- "Enter",
2715
- "Tab",
2716
- "ArrowDown",
2717
- "ArrowLeft",
2718
- "ArrowRight",
2719
- "ArrowUp",
2720
- "End",
2721
- "Home",
2722
- "PageDown",
2723
- "PageUp",
2724
- "Backspace",
2725
- "Clear",
2726
- "Copy",
2727
- "CrSel",
2728
- "Cut",
2729
- "Delete",
2730
- "EraseEof",
2731
- "ExSel",
2732
- "Insert",
2733
- "Paste",
2734
- "Redo",
2735
- "Undo",
2736
- "Accept",
2737
- "Again",
2738
- "Attn",
2739
- "Cancel",
2740
- "ContextMenu",
2741
- "Escape",
2742
- "Execute",
2743
- "Find",
2744
- "Finish",
2745
- "Help",
2746
- "Pause",
2747
- "Play",
2748
- "Props",
2749
- "Select",
2750
- "ZoomIn",
2751
- "ZoomOut",
2752
- "BrightnessDown",
2753
- "BrightnessUp",
2754
- "Eject",
2755
- "LogOff",
2756
- "Power",
2757
- "PowerOff",
2758
- "PrintScreen",
2759
- "Hibernate",
2760
- "Standby",
2761
- "WakeUp",
2762
- "AllCandidates",
2763
- "Alphanumeric",
2764
- "CodeInput",
2765
- "Compose",
2766
- "Convert",
2767
- "Dead",
2768
- "FinalMode",
2769
- "GroupFirst",
2770
- "GroupLast",
2771
- "GroupNext",
2772
- "GroupPrevious",
2773
- "ModeChange",
2774
- "NextCandidate",
2775
- "NonConvert",
2776
- "PreviousCandidate",
2777
- "Process",
2778
- "SingleCandidate",
2779
- "HangulMode",
2780
- "HanjaMode",
2781
- "JunjaMode",
2782
- "Eisu",
2783
- "Hankaku",
2784
- "Hiragana",
2785
- "HiraganaKatakana",
2786
- "KanaMode",
2787
- "KanjiMode",
2788
- "Katakana",
2789
- "Romaji",
2790
- "Zenkaku",
2791
- "ZenkakuHanaku",
2792
- "F1",
2793
- "F2",
2794
- "F3",
2795
- "F4",
2796
- "F5",
2797
- "F6",
2798
- "F7",
2799
- "F8",
2800
- "F9",
2801
- "F10",
2802
- "F11",
2803
- "F12",
2804
- "Soft1",
2805
- "Soft2",
2806
- "Soft3",
2807
- "Soft4",
2808
- "ChannelDown",
2809
- "ChannelUp",
2810
- "Close",
2811
- "MailForward",
2812
- "MailReply",
2813
- "MailSend",
2814
- "MediaFastForward",
2815
- "MediaPause",
2816
- "MediaPlay",
2817
- "MediaPlayPause",
2818
- "MediaRecord",
2819
- "MediaRewind",
2820
- "MediaStop",
2821
- "MediaTrackNext",
2822
- "MediaTrackPrevious",
2823
- "AudioBalanceLeft",
2824
- "AudioBalanceRight",
2825
- "AudioBassBoostDown",
2826
- "AudioBassBoostToggle",
2827
- "AudioBassBoostUp",
2828
- "AudioFaderFront",
2829
- "AudioFaderRear",
2830
- "AudioSurroundModeNext",
2831
- "AudioTrebleDown",
2832
- "AudioTrebleUp",
2833
- "AudioVolumeDown",
2834
- "AudioVolumeMute",
2835
- "AudioVolumeUp",
2836
- "MicrophoneToggle",
2837
- "MicrophoneVolumeDown",
2838
- "MicrophoneVolumeMute",
2839
- "MicrophoneVolumeUp",
2840
- "TV",
2841
- "TV3DMode",
2842
- "TVAntennaCable",
2843
- "TVAudioDescription",
2844
- ];
2845
- function unEscapeString(str) {
2846
- const placeholder = "__NEWLINE__";
2847
- str = str.replace(new RegExp(placeholder, "g"), "\n");
2848
- return str;
2849
- }
2850
4550
  export { StableBrowser };
2851
4551
  //# sourceMappingURL=stable_browser.js.map