automation_model 1.0.450-dev → 1.0.450

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 (74) 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 +239 -49
  5. package/lib/api.js.map +1 -1
  6. package/lib/auto_page.d.ts +7 -2
  7. package/lib/auto_page.js +270 -49
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.d.ts +7 -3
  10. package/lib/browser_manager.js +176 -49
  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/command_common.d.ts +6 -0
  16. package/lib/command_common.js +206 -0
  17. package/lib/command_common.js.map +1 -0
  18. package/lib/date_time.js.map +1 -1
  19. package/lib/drawRect.js.map +1 -1
  20. package/lib/environment.d.ts +1 -0
  21. package/lib/environment.js +6 -3
  22. package/lib/environment.js.map +1 -1
  23. package/lib/error-messages.d.ts +6 -0
  24. package/lib/error-messages.js +206 -0
  25. package/lib/error-messages.js.map +1 -0
  26. package/lib/file_checker.d.ts +1 -0
  27. package/lib/file_checker.js +61 -0
  28. package/lib/file_checker.js.map +1 -0
  29. package/lib/find_function.js.map +1 -1
  30. package/lib/generation_scripts.d.ts +4 -0
  31. package/lib/generation_scripts.js +2 -0
  32. package/lib/generation_scripts.js.map +1 -0
  33. package/lib/index.d.ts +3 -0
  34. package/lib/index.js +3 -0
  35. package/lib/index.js.map +1 -1
  36. package/lib/init_browser.d.ts +4 -2
  37. package/lib/init_browser.js +117 -11
  38. package/lib/init_browser.js.map +1 -1
  39. package/lib/locate_element.d.ts +7 -0
  40. package/lib/locate_element.js +215 -0
  41. package/lib/locate_element.js.map +1 -0
  42. package/lib/locator.d.ts +37 -0
  43. package/lib/locator.js +172 -0
  44. package/lib/locator.js.map +1 -1
  45. package/lib/locator_log.d.ts +26 -0
  46. package/lib/locator_log.js +69 -0
  47. package/lib/locator_log.js.map +1 -0
  48. package/lib/network.d.ts +5 -0
  49. package/lib/network.js +331 -0
  50. package/lib/network.js.map +1 -0
  51. package/lib/route.d.ts +21 -0
  52. package/lib/route.js +390 -0
  53. package/lib/route.js.map +1 -0
  54. package/lib/scripts/axe.mini.js +12 -0
  55. package/lib/snapshot_validation.d.ts +37 -0
  56. package/lib/snapshot_validation.js +357 -0
  57. package/lib/snapshot_validation.js.map +1 -0
  58. package/lib/stable_browser.d.ts +150 -55
  59. package/lib/stable_browser.js +2560 -1302
  60. package/lib/stable_browser.js.map +1 -1
  61. package/lib/table.d.ts +15 -0
  62. package/lib/table.js +257 -0
  63. package/lib/table.js.map +1 -0
  64. package/lib/table_analyze.js.map +1 -1
  65. package/lib/table_helper.d.ts +19 -0
  66. package/lib/table_helper.js +116 -0
  67. package/lib/table_helper.js.map +1 -0
  68. package/lib/test_context.d.ts +7 -0
  69. package/lib/test_context.js +15 -10
  70. package/lib/test_context.js.map +1 -1
  71. package/lib/utils.d.ts +35 -2
  72. package/lib/utils.js +683 -11
  73. package/lib/utils.js.map +1 -1
  74. package/package.json +21 -11
@@ -2,34 +2,50 @@
2
2
  import { expect } from "@playwright/test";
3
3
  import dayjs from "dayjs";
4
4
  import fs from "fs";
5
+ import { Jimp } from "jimp";
5
6
  import path from "path";
6
7
  import reg_parser from "regex-parser";
7
- import sharp from "sharp";
8
8
  import { findDateAlternatives, findNumberAlternatives } from "./analyze_helper.js";
9
9
  import { getDateTimeValue } from "./date_time.js";
10
10
  import drawRectangle from "./drawRect.js";
11
11
  //import { closeUnexpectedPopups } from "./popups.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import objectPath from "object-path";
14
- import { decrypt } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
15
14
  import csv from "csv-parser";
16
15
  import { Readable } from "node:stream";
17
16
  import readline from "readline";
18
- import { getContext } from "./init_browser.js";
19
- const Types = {
17
+ import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
19
+ import { locate_element } from "./locate_element.js";
20
+ import { randomUUID } from "crypto";
21
+ import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
22
+ import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
23
+ import { LocatorLog } from "./locator_log.js";
24
+ import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
27
+ import { loadBrunoParams } from "./bruno.js";
28
+ import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
29
+ export const Types = {
20
30
  CLICK: "click_element",
31
+ WAIT_ELEMENT: "wait_element",
21
32
  NAVIGATE: "navigate",
33
+ GO_BACK: "go_back",
34
+ GO_FORWARD: "go_forward",
22
35
  FILL: "fill_element",
23
- EXECUTE: "execute_page_method",
24
- OPEN: "open_environment",
36
+ EXECUTE: "execute_page_method", //
37
+ OPEN: "open_environment", //
25
38
  COMPLETE: "step_complete",
26
39
  ASK: "information_needed",
27
- GET_PAGE_STATUS: "get_page_status",
28
- CLICK_ROW_ACTION: "click_row_action",
40
+ GET_PAGE_STATUS: "get_page_status", ///
41
+ CLICK_ROW_ACTION: "click_row_action", //
29
42
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
43
+ VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
44
+ VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
30
45
  ANALYZE_TABLE: "analyze_table",
31
- SELECT: "select_combobox",
46
+ SELECT: "select_combobox", //
32
47
  VERIFY_PAGE_PATH: "verify_page_path",
48
+ VERIFY_PAGE_TITLE: "verify_page_title",
33
49
  TYPE_PRESS: "type_press",
34
50
  PRESS: "press_key",
35
51
  HOVER: "hover_element",
@@ -37,25 +53,50 @@ const Types = {
37
53
  UNCHECK: "uncheck_element",
38
54
  EXTRACT: "extract_attribute",
39
55
  CLOSE_PAGE: "close_page",
56
+ TABLE_OPERATION: "table_operation",
40
57
  SET_DATE_TIME: "set_date_time",
41
58
  SET_VIEWPORT: "set_viewport",
42
59
  VERIFY_VISUAL: "verify_visual",
43
60
  LOAD_DATA: "load_data",
44
61
  SET_INPUT: "set_input",
62
+ WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
63
+ VERIFY_ATTRIBUTE: "verify_element_attribute",
64
+ VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
65
+ BRUNO: "bruno",
66
+ VERIFY_FILE_EXISTS: "verify_file_exists",
67
+ SET_INPUT_FILES: "set_input_files",
68
+ SNAPSHOT_VALIDATION: "snapshot_validation",
69
+ REPORT_COMMAND: "report_command",
70
+ STEP_COMPLETE: "step_complete",
71
+ SLEEP: "sleep",
72
+ CONDITIONAL_WAIT: "conditional_wait",
45
73
  };
46
74
  export const apps = {};
75
+ const formatElementName = (elementName) => {
76
+ return elementName ? JSON.stringify(elementName) : "element";
77
+ };
47
78
  class StableBrowser {
48
- constructor(browser, page, logger = null, context = null, world = null) {
79
+ browser;
80
+ page;
81
+ logger;
82
+ context;
83
+ world;
84
+ fastMode;
85
+ project_path = null;
86
+ webLogFile = null;
87
+ networkLogger = null;
88
+ configuration = null;
89
+ appName = "main";
90
+ tags = null;
91
+ isRecording = false;
92
+ initSnapshotTaken = false;
93
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
49
94
  this.browser = browser;
50
95
  this.page = page;
51
96
  this.logger = logger;
52
97
  this.context = context;
53
98
  this.world = world;
54
- this.project_path = null;
55
- this.webLogFile = null;
56
- this.networkLogger = null;
57
- this.configuration = null;
58
- this.appName = "main";
99
+ this.fastMode = fastMode;
59
100
  if (!this.logger) {
60
101
  this.logger = console;
61
102
  }
@@ -84,16 +125,52 @@ class StableBrowser {
84
125
  context.pages = [this.page];
85
126
  const logFolder = path.join(this.project_path, "logs", "web");
86
127
  this.world = world;
128
+ if (this.configuration && this.configuration.fastMode === true) {
129
+ this.fastMode = true;
130
+ }
131
+ if (process.env.FAST_MODE === "true") {
132
+ // console.log("Fast mode enabled from environment variable");
133
+ this.fastMode = true;
134
+ }
135
+ if (process.env.FAST_MODE === "false") {
136
+ this.fastMode = false;
137
+ }
138
+ if (this.context) {
139
+ this.context.fastMode = this.fastMode;
140
+ }
87
141
  this.registerEventListeners(this.context);
142
+ registerNetworkEvents(this.world, this, this.context, this.page);
143
+ registerDownloadEvent(this.page, this.world, this.context);
88
144
  }
89
145
  registerEventListeners(context) {
90
146
  this.registerConsoleLogListener(this.page, context);
91
- this.registerRequestListener(this.page, context, this.webLogFile);
147
+ // this.registerRequestListener(this.page, context, this.webLogFile);
148
+ if (!context.pageLoading) {
149
+ context.pageLoading = { status: false };
150
+ }
151
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
152
+ this.page.on("dialog", (dialog) => dialog.accept());
153
+ }
92
154
  context.playContext.on("page", async function (page) {
155
+ if (this.configuration && this.configuration.closePopups === true) {
156
+ console.log("close unexpected popups");
157
+ await page.close();
158
+ return;
159
+ }
93
160
  context.pageLoading.status = true;
94
161
  this.page = page;
162
+ try {
163
+ if (this.configuration && this.configuration.acceptDialog) {
164
+ await page.on("dialog", (dialog) => dialog.accept());
165
+ }
166
+ }
167
+ catch (error) {
168
+ console.error("Error on dialog accept registration", error);
169
+ }
95
170
  context.page = page;
96
171
  context.pages.push(page);
172
+ registerNetworkEvents(this.world, this, context, this.page);
173
+ registerDownloadEvent(this.page, this.world, context);
97
174
  page.on("close", async () => {
98
175
  if (this.context && this.context.pages && this.context.pages.length > 1) {
99
176
  this.context.pages.pop();
@@ -125,7 +202,7 @@ class StableBrowser {
125
202
  }
126
203
  let newContextCreated = false;
127
204
  if (!apps[appName]) {
128
- let newContext = await getContext(null, false, this.logger, appName, false, this);
205
+ let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
129
206
  newContextCreated = true;
130
207
  apps[appName] = {
131
208
  context: newContext,
@@ -134,38 +211,47 @@ class StableBrowser {
134
211
  };
135
212
  }
136
213
  const tempContext = {};
137
- this._copyContext(this, tempContext);
138
- this._copyContext(apps[appName], this);
214
+ _copyContext(this, tempContext);
215
+ _copyContext(apps[appName], this);
139
216
  apps[this.appName] = tempContext;
140
217
  this.appName = appName;
141
218
  if (newContextCreated) {
142
219
  this.registerEventListeners(this.context);
143
220
  await this.goto(this.context.environment.baseUrl);
144
- await this.waitForPageLoad();
221
+ if (!this.fastMode) {
222
+ await this.waitForPageLoad();
223
+ }
145
224
  }
146
225
  }
147
- _copyContext(from, to) {
148
- to.browser = from.browser;
149
- to.page = from.page;
150
- to.context = from.context;
151
- }
152
- getWebLogFile(logFolder) {
153
- if (!fs.existsSync(logFolder)) {
154
- fs.mkdirSync(logFolder, { recursive: true });
226
+ async switchTab(tabTitleOrIndex) {
227
+ // first check if the tabNameOrIndex is a number
228
+ let index = parseInt(tabTitleOrIndex);
229
+ if (!isNaN(index)) {
230
+ if (index >= 0 && index < this.context.pages.length) {
231
+ this.page = this.context.pages[index];
232
+ this.context.page = this.page;
233
+ await this.page.bringToFront();
234
+ return;
235
+ }
155
236
  }
156
- let nextIndex = 1;
157
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
158
- nextIndex++;
237
+ // if the tabNameOrIndex is a string, find the tab by name
238
+ for (let i = 0; i < this.context.pages.length; i++) {
239
+ let page = this.context.pages[i];
240
+ let title = await page.title();
241
+ if (title.includes(tabTitleOrIndex)) {
242
+ this.page = page;
243
+ this.context.page = this.page;
244
+ await this.page.bringToFront();
245
+ return;
246
+ }
159
247
  }
160
- const fileName = nextIndex + ".json";
161
- return path.join(logFolder, fileName);
248
+ throw new Error("Tab not found: " + tabTitleOrIndex);
162
249
  }
163
250
  registerConsoleLogListener(page, context) {
164
251
  if (!this.context.webLogger) {
165
252
  this.context.webLogger = [];
166
253
  }
167
254
  page.on("console", async (msg) => {
168
- var _a;
169
255
  const obj = {
170
256
  type: msg.type(),
171
257
  text: msg.text(),
@@ -173,7 +259,9 @@ class StableBrowser {
173
259
  time: new Date().toISOString(),
174
260
  };
175
261
  this.context.webLogger.push(obj);
176
- (_a = this.world) === null || _a === void 0 ? void 0 : _a.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
262
+ if (msg.type() === "error") {
263
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
264
+ }
177
265
  });
178
266
  }
179
267
  registerRequestListener(page, context, logFile) {
@@ -181,7 +269,6 @@ class StableBrowser {
181
269
  this.context.networkLogger = [];
182
270
  }
183
271
  page.on("request", async (data) => {
184
- var _a;
185
272
  const startTime = new Date().getTime();
186
273
  try {
187
274
  const pageUrl = new URL(page.url());
@@ -206,10 +293,10 @@ class StableBrowser {
206
293
  startTime,
207
294
  };
208
295
  context.networkLogger.push(obj);
209
- (_a = this.world) === null || _a === void 0 ? void 0 : _a.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
296
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
210
297
  }
211
298
  catch (error) {
212
- console.error("Error in request listener", error);
299
+ // console.error("Error in request listener", error);
213
300
  context.networkLogger.push({
214
301
  error: "not able to listen",
215
302
  message: error.message,
@@ -223,69 +310,110 @@ class StableBrowser {
223
310
  // async closeUnexpectedPopups() {
224
311
  // await closeUnexpectedPopups(this.page);
225
312
  // }
226
- async goto(url) {
313
+ async goto(url, world = null) {
314
+ if (!url) {
315
+ throw new Error("url is null, verify that the environment file is correct");
316
+ }
317
+ url = await this._replaceWithLocalData(url, this.world);
227
318
  if (!url.startsWith("http")) {
228
319
  url = "https://" + url;
229
320
  }
230
- await this.page.goto(url, {
231
- timeout: 60000,
232
- });
233
- }
234
- _validateSelectors(selectors) {
235
- if (!selectors) {
236
- throw new Error("selectors is null");
237
- }
238
- if (!selectors.locators) {
239
- throw new Error("selectors.locators is null");
321
+ const state = {
322
+ value: url,
323
+ world: world,
324
+ type: Types.NAVIGATE,
325
+ text: `Navigate Page to: ${url}`,
326
+ operation: "goto",
327
+ log: "***** navigate page to " + url + " *****\n",
328
+ info: {},
329
+ locate: false,
330
+ scroll: false,
331
+ screenshot: false,
332
+ highlight: false,
333
+ };
334
+ try {
335
+ await _preCommand(state, this);
336
+ await this.page.goto(url, {
337
+ timeout: 60000,
338
+ });
339
+ await _screenshot(state, this);
240
340
  }
241
- if (!Array.isArray(selectors.locators)) {
242
- throw new Error("selectors.locators expected to be array");
341
+ catch (error) {
342
+ console.error("Error on goto", error);
343
+ _commandError(state, error, this);
243
344
  }
244
- if (selectors.locators.length === 0) {
245
- throw new Error("selectors.locators expected to be non empty array");
345
+ finally {
346
+ await _commandFinally(state, this);
246
347
  }
247
348
  }
248
- _fixUsingParams(text, _params) {
249
- if (!_params || typeof text !== "string") {
250
- return text;
349
+ async goBack(options, world = null) {
350
+ const state = {
351
+ value: "",
352
+ world: world,
353
+ type: Types.GO_BACK,
354
+ text: `Browser navigate back`,
355
+ operation: "goBack",
356
+ log: "***** navigate back *****\n",
357
+ info: {},
358
+ locate: false,
359
+ scroll: false,
360
+ screenshot: false,
361
+ highlight: false,
362
+ };
363
+ try {
364
+ await _preCommand(state, this);
365
+ await this.page.goBack({
366
+ waitUntil: "load",
367
+ });
368
+ await _screenshot(state, this);
251
369
  }
252
- for (let key in _params) {
253
- let regValue = key;
254
- if (key.startsWith("_")) {
255
- // remove the _ prefix
256
- regValue = key.substring(1);
257
- }
258
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
370
+ catch (error) {
371
+ console.error("Error on goBack", error);
372
+ _commandError(state, error, this);
259
373
  }
260
- return text;
261
- }
262
- _fixLocatorUsingParams(locator, _params) {
263
- // check if not null
264
- if (!locator) {
265
- return locator;
374
+ finally {
375
+ await _commandFinally(state, this);
266
376
  }
267
- // clone the locator
268
- locator = JSON.parse(JSON.stringify(locator));
269
- this.scanAndManipulate(locator, _params);
270
- return locator;
271
377
  }
272
- _isObject(value) {
273
- return value && typeof value === "object" && value.constructor === Object;
378
+ async goForward(options, world = null) {
379
+ const state = {
380
+ value: "",
381
+ world: world,
382
+ type: Types.GO_FORWARD,
383
+ text: `Browser navigate forward`,
384
+ operation: "goForward",
385
+ log: "***** navigate forward *****\n",
386
+ info: {},
387
+ locate: false,
388
+ scroll: false,
389
+ screenshot: false,
390
+ highlight: false,
391
+ };
392
+ try {
393
+ await _preCommand(state, this);
394
+ await this.page.goForward({
395
+ waitUntil: "load",
396
+ });
397
+ await _screenshot(state, this);
398
+ }
399
+ catch (error) {
400
+ console.error("Error on goForward", error);
401
+ _commandError(state, error, this);
402
+ }
403
+ finally {
404
+ await _commandFinally(state, this);
405
+ }
274
406
  }
275
- scanAndManipulate(currentObj, _params) {
276
- for (const key in currentObj) {
277
- if (typeof currentObj[key] === "string") {
278
- // Perform string manipulation
279
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
280
- }
281
- else if (this._isObject(currentObj[key])) {
282
- // Recursively scan nested objects
283
- this.scanAndManipulate(currentObj[key], _params);
407
+ async _getLocator(locator, scope, _params) {
408
+ locator = _fixLocatorUsingParams(locator, _params);
409
+ // locator = await this._replaceWithLocalData(locator);
410
+ for (let key in locator) {
411
+ if (typeof locator[key] !== "string")
412
+ continue;
413
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
414
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
284
415
  }
285
416
  }
286
- }
287
- _getLocator(locator, scope, _params) {
288
- locator = this._fixLocatorUsingParams(locator, _params);
289
417
  let locatorReturn;
290
418
  if (locator.role) {
291
419
  if (locator.role[1].nameReg) {
@@ -293,7 +421,7 @@ class StableBrowser {
293
421
  delete locator.role[1].nameReg;
294
422
  }
295
423
  // if (locator.role[1].name) {
296
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
424
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
297
425
  // }
298
426
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
299
427
  }
@@ -312,7 +440,7 @@ class StableBrowser {
312
440
  locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
313
441
  }
314
442
  }
315
- if (locator === null || locator === void 0 ? void 0 : locator.engine) {
443
+ if (locator?.engine) {
316
444
  if (locator.engine === "css") {
317
445
  locatorReturn = scope.locator(locator.selector);
318
446
  }
@@ -333,192 +461,180 @@ class StableBrowser {
333
461
  return locatorReturn;
334
462
  }
335
463
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
336
- let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
464
+ if (css && css.locator) {
465
+ css = css.locator;
466
+ }
467
+ let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
337
468
  if (result.elementCount === 0) {
338
469
  return;
339
470
  }
340
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
471
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
341
472
  // css climb to parent element
342
473
  const climbArray = [];
343
474
  for (let i = 0; i < climb; i++) {
344
475
  climbArray.push("..");
345
476
  }
346
477
  let climbXpath = "xpath=" + climbArray.join("/");
347
- return textElementCss + " >> " + climbXpath + " >> " + css;
348
- }
349
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
350
- //const stringifyText = JSON.stringify(text);
351
- return await scope.evaluate(([text, tag, regex, partial]) => {
352
- function isParent(parent, child) {
353
- let currentNode = child.parentNode;
354
- while (currentNode !== null) {
355
- if (currentNode === parent) {
356
- return true;
357
- }
358
- currentNode = currentNode.parentNode;
359
- }
360
- return false;
361
- }
362
- document.isParent = isParent;
363
- function collectAllShadowDomElements(element, result = []) {
364
- // Check and add the element if it has a shadow root
365
- if (element.shadowRoot) {
366
- result.push(element);
367
- // Also search within the shadow root
368
- document.collectAllShadowDomElements(element.shadowRoot, result);
369
- }
370
- // Iterate over child nodes
371
- element.childNodes.forEach((child) => {
372
- // Recursively call the function for each child node
373
- document.collectAllShadowDomElements(child, result);
374
- });
375
- return result;
376
- }
377
- document.collectAllShadowDomElements = collectAllShadowDomElements;
378
- if (!tag) {
379
- tag = "*";
380
- }
381
- let elements = Array.from(document.querySelectorAll(tag));
382
- let shadowHosts = [];
383
- document.collectAllShadowDomElements(document, shadowHosts);
384
- for (let i = 0; i < shadowHosts.length; i++) {
385
- let shadowElement = shadowHosts[i].shadowRoot;
386
- if (!shadowElement) {
387
- console.log("shadowElement is null, for host " + shadowHosts[i]);
388
- continue;
389
- }
390
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
391
- elements = elements.concat(shadowElements);
392
- }
393
- let randomToken = null;
394
- const foundElements = [];
395
- if (regex) {
396
- let regexpSearch = new RegExp(text, "im");
397
- for (let i = 0; i < elements.length; i++) {
398
- const element = elements[i];
399
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
400
- (element.value && regexpSearch.test(element.value))) {
401
- foundElements.push(element);
402
- }
403
- }
404
- }
405
- else {
406
- text = text.trim();
407
- for (let i = 0; i < elements.length; i++) {
408
- const element = elements[i];
409
- if (partial) {
410
- if ((element.innerText && element.innerText.trim().includes(text)) ||
411
- (element.value && element.value.includes(text))) {
412
- foundElements.push(element);
413
- }
414
- }
415
- else {
416
- if ((element.innerText && element.innerText.trim() === text) ||
417
- (element.value && element.value === text)) {
418
- foundElements.push(element);
419
- }
420
- }
421
- }
422
- }
423
- let noChildElements = [];
424
- for (let i = 0; i < foundElements.length; i++) {
425
- let element = foundElements[i];
426
- let hasChild = false;
427
- for (let j = 0; j < foundElements.length; j++) {
428
- if (i === j) {
429
- continue;
430
- }
431
- if (isParent(element, foundElements[j])) {
432
- hasChild = true;
433
- break;
478
+ let resultCss = textElementCss + " >> " + climbXpath;
479
+ if (css) {
480
+ resultCss = resultCss + " >> " + css;
481
+ }
482
+ return resultCss;
483
+ }
484
+ async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
485
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
486
+ const locator = scope.locator(query);
487
+ const count = await locator.count();
488
+ if (!tag1) {
489
+ tag1 = "*";
490
+ }
491
+ const randomToken = Math.random().toString(36).substring(7);
492
+ let tagCount = 0;
493
+ for (let i = 0; i < count; i++) {
494
+ const element = locator.nth(i);
495
+ // check if the tag matches
496
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
497
+ if (!tag.startsWith("*")) {
498
+ if (el.tagName.toLowerCase() !== tag) {
499
+ return false;
434
500
  }
435
501
  }
436
- if (!hasChild) {
437
- noChildElements.push(element);
438
- }
439
- }
440
- let elementCount = 0;
441
- if (noChildElements.length > 0) {
442
- for (let i = 0; i < noChildElements.length; i++) {
443
- if (randomToken === null) {
444
- randomToken = Math.random().toString(36).substring(7);
445
- }
446
- let element = noChildElements[i];
447
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
448
- elementCount++;
502
+ if (!el.setAttribute) {
503
+ el = el.parentElement;
449
504
  }
505
+ // remove any attributes start with data-blinq-id
506
+ // for (let i = 0; i < el.attributes.length; i++) {
507
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
508
+ // el.removeAttribute(el.attributes[i].name);
509
+ // }
510
+ // }
511
+ el.setAttribute("data-blinq-id-" + randomToken, "");
512
+ return true;
513
+ }, [tag1, randomToken]))) {
514
+ continue;
450
515
  }
451
- return { elementCount: elementCount, randomToken: randomToken };
452
- }, [text1, tag1, regex1, partial1]);
516
+ tagCount++;
517
+ }
518
+ return { elementCount: tagCount, randomToken };
453
519
  }
454
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
520
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
521
+ if (!info) {
522
+ info = {};
523
+ }
524
+ if (!info.failCause) {
525
+ info.failCause = {};
526
+ }
527
+ if (!info.log) {
528
+ info.log = "";
529
+ info.locatorLog = new LocatorLog(selectorHierarchy);
530
+ }
455
531
  let locatorSearch = selectorHierarchy[index];
532
+ try {
533
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
534
+ }
535
+ catch (e) {
536
+ console.error(e);
537
+ }
538
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
456
539
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
457
540
  let locator = null;
458
541
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
459
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
542
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
543
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
460
544
  if (!locatorString) {
545
+ info.failCause.textNotFound = true;
546
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
461
547
  return;
462
548
  }
463
- locator = this._getLocator({ css: locatorString }, scope, _params);
549
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
464
550
  }
465
551
  else if (locatorSearch.text) {
466
- let result = await this._locateElementByText(scope, this._fixUsingParams(locatorSearch.text, _params), locatorSearch.tag, false, locatorSearch.partial === true, _params);
552
+ let text = _fixUsingParams(locatorSearch.text, _params);
553
+ let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
467
554
  if (result.elementCount === 0) {
555
+ info.failCause.textNotFound = true;
556
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
468
557
  return;
469
558
  }
470
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
559
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
471
560
  if (locatorSearch.childCss) {
472
561
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
473
562
  }
474
- locator = this._getLocator(locatorSearch, scope, _params);
563
+ locator = await this._getLocator(locatorSearch, scope, _params);
475
564
  }
476
565
  else {
477
- locator = this._getLocator(locatorSearch, scope, _params);
566
+ locator = await this._getLocator(locatorSearch, scope, _params);
478
567
  }
479
568
  // let cssHref = false;
480
569
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
481
570
  // cssHref = true;
482
571
  // }
483
572
  let count = await locator.count();
573
+ if (count > 0 && !info.failCause.count) {
574
+ info.failCause.count = count;
575
+ }
484
576
  //info.log += "total elements found " + count + "\n";
485
577
  //let visibleCount = 0;
486
578
  let visibleLocator = null;
487
- if (locatorSearch.index && locatorSearch.index < count) {
579
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
488
580
  foundLocators.push(locator.nth(locatorSearch.index));
581
+ if (info.locatorLog) {
582
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
583
+ }
489
584
  return;
490
585
  }
586
+ if (info.locatorLog && count === 0 && logErrors) {
587
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
588
+ }
491
589
  for (let j = 0; j < count; j++) {
492
590
  let visible = await locator.nth(j).isVisible();
493
591
  const enabled = await locator.nth(j).isEnabled();
494
592
  if (!visibleOnly) {
495
593
  visible = true;
496
594
  }
497
- if (visible && enabled) {
595
+ if (visible && (allowDisabled || enabled)) {
498
596
  foundLocators.push(locator.nth(j));
597
+ if (info.locatorLog) {
598
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
599
+ }
499
600
  }
500
- else {
601
+ else if (logErrors) {
602
+ info.failCause.visible = visible;
603
+ info.failCause.enabled = enabled;
501
604
  if (!info.printMessages) {
502
605
  info.printMessages = {};
503
606
  }
607
+ if (info.locatorLog && !visible) {
608
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
609
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
610
+ }
611
+ if (info.locatorLog && !enabled) {
612
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
613
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
614
+ }
504
615
  if (!info.printMessages[j.toString()]) {
505
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
616
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
506
617
  info.printMessages[j.toString()] = true;
507
618
  }
508
619
  }
509
620
  }
510
621
  }
511
622
  async closeUnexpectedPopups(info, _params) {
623
+ if (!info) {
624
+ info = {};
625
+ info.failCause = {};
626
+ info.log = "";
627
+ }
512
628
  if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
513
629
  if (!info) {
514
630
  info = {};
515
631
  }
516
- info.log += "scan for popup handlers" + "\n";
632
+ //info.log += "scan for popup handlers" + "\n";
517
633
  const handlerGroup = [];
518
634
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
519
635
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
520
636
  }
521
- const scopes = [this.page, ...this.page.frames()];
637
+ const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
522
638
  let result = null;
523
639
  let scope = null;
524
640
  for (let i = 0; i < scopes.length; i++) {
@@ -540,55 +656,108 @@ class StableBrowser {
540
656
  }
541
657
  if (result.foundElements.length > 0) {
542
658
  let dialogCloseLocator = result.foundElements[0].locator;
543
- await dialogCloseLocator.click();
544
- // wait for the dialog to close
545
- await dialogCloseLocator.waitFor({ state: "hidden" });
659
+ try {
660
+ await scope?.evaluate(() => {
661
+ window.__isClosingPopups = true;
662
+ });
663
+ await dialogCloseLocator.click();
664
+ // wait for the dialog to close
665
+ await dialogCloseLocator.waitFor({ state: "hidden" });
666
+ }
667
+ catch (e) {
668
+ }
669
+ finally {
670
+ await scope?.evaluate(() => {
671
+ window.__isClosingPopups = false;
672
+ });
673
+ }
546
674
  return { rerun: true };
547
675
  }
548
676
  }
549
677
  }
550
678
  return { rerun: false };
551
679
  }
552
- async _locate(selectors, info, _params, timeout = 30000) {
680
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
681
+ if (!timeout) {
682
+ timeout = 30000;
683
+ }
553
684
  for (let i = 0; i < 3; i++) {
554
685
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
555
686
  for (let j = 0; j < selectors.locators.length; j++) {
556
687
  let selector = selectors.locators[j];
557
688
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
558
689
  }
559
- let element = await this._locate_internal(selectors, info, _params, timeout);
690
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
560
691
  if (!element.rerun) {
561
- return element;
692
+ const randomToken = Math.random().toString(36).substring(7);
693
+ await element.evaluate((el, randomToken) => {
694
+ el.setAttribute("data-blinq-id-" + randomToken, "");
695
+ }, randomToken);
696
+ // if (element._frame) {
697
+ // return element;
698
+ // }
699
+ const scope = element._frame ?? element.page();
700
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
701
+ let prefixSelector = "";
702
+ const frameControlSelector = " >> internal:control=enter-frame";
703
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
704
+ if (frameSelectorIndex !== -1) {
705
+ // remove everything after the >> internal:control=enter-frame
706
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
707
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
708
+ }
709
+ // if (element?._frame?._selector) {
710
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
711
+ // }
712
+ const newSelector = prefixSelector + newElementSelector;
713
+ return scope.locator(newSelector);
562
714
  }
563
715
  }
564
716
  throw new Error("unable to locate element " + JSON.stringify(selectors));
565
717
  }
566
- async _locate_internal(selectors, info, _params, timeout = 30000) {
567
- let highPriorityTimeout = 5000;
568
- let visibleOnlyTimeout = 6000;
569
- let startTime = performance.now();
570
- let locatorsCount = 0;
571
- //let arrayMode = Array.isArray(selectors);
718
+ async _findFrameScope(selectors, timeout = 30000, info) {
719
+ if (!info) {
720
+ info = {};
721
+ info.failCause = {};
722
+ info.log = "";
723
+ }
724
+ let startTime = Date.now();
572
725
  let scope = this.page;
726
+ if (selectors.frame) {
727
+ return selectors.frame;
728
+ }
573
729
  if (selectors.iframe_src || selectors.frameLocators) {
574
- const findFrame = (frame, framescope) => {
730
+ const findFrame = async (frame, framescope) => {
575
731
  for (let i = 0; i < frame.selectors.length; i++) {
576
732
  let frameLocator = frame.selectors[i];
577
733
  if (frameLocator.css) {
578
- framescope = framescope.frameLocator(frameLocator.css);
579
- break;
734
+ let testframescope = framescope.frameLocator(frameLocator.css);
735
+ if (frameLocator.index) {
736
+ testframescope = framescope.nth(frameLocator.index);
737
+ }
738
+ try {
739
+ await testframescope.owner().evaluateHandle(() => true, null, {
740
+ timeout: 5000,
741
+ });
742
+ framescope = testframescope;
743
+ break;
744
+ }
745
+ catch (error) {
746
+ // console.error("frame not found " + frameLocator.css);
747
+ }
580
748
  }
581
749
  }
582
750
  if (frame.children) {
583
- return findFrame(frame.children, framescope);
751
+ return await findFrame(frame.children, framescope);
584
752
  }
585
753
  return framescope;
586
754
  };
587
- info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
755
+ let fLocator = null;
588
756
  while (true) {
589
757
  let frameFound = false;
590
758
  if (selectors.nestFrmLoc) {
591
- scope = findFrame(selectors.nestFrmLoc, scope);
759
+ fLocator = selectors.nestFrmLoc;
760
+ scope = await findFrame(selectors.nestFrmLoc, scope);
592
761
  frameFound = true;
593
762
  break;
594
763
  }
@@ -596,6 +765,7 @@ class StableBrowser {
596
765
  for (let i = 0; i < selectors.frameLocators.length; i++) {
597
766
  let frameLocator = selectors.frameLocators[i];
598
767
  if (frameLocator.css) {
768
+ fLocator = frameLocator.css;
599
769
  scope = scope.frameLocator(frameLocator.css);
600
770
  frameFound = true;
601
771
  break;
@@ -603,20 +773,55 @@ class StableBrowser {
603
773
  }
604
774
  }
605
775
  if (!frameFound && selectors.iframe_src) {
776
+ fLocator = selectors.iframe_src;
606
777
  scope = this.page.frame({ url: selectors.iframe_src });
607
778
  }
608
779
  if (!scope) {
609
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
610
- if (performance.now() - startTime > timeout) {
780
+ if (info && info.locatorLog) {
781
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
782
+ }
783
+ //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
784
+ if (Date.now() - startTime > timeout) {
785
+ info.failCause.iframeNotFound = true;
786
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
611
787
  throw new Error("unable to locate iframe " + selectors.iframe_src);
612
788
  }
613
789
  await new Promise((resolve) => setTimeout(resolve, 1000));
614
790
  }
615
791
  else {
792
+ if (info && info.locatorLog) {
793
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
794
+ }
616
795
  break;
617
796
  }
618
797
  }
619
798
  }
799
+ if (!scope) {
800
+ scope = this.page;
801
+ }
802
+ return scope;
803
+ }
804
+ async _getDocumentBody(selectors, timeout = 30000, info) {
805
+ let scope = await this._findFrameScope(selectors, timeout, info);
806
+ return scope.evaluate(() => {
807
+ var bodyContent = document.body.innerHTML;
808
+ return bodyContent;
809
+ });
810
+ }
811
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
812
+ if (!info) {
813
+ info = {};
814
+ info.failCause = {};
815
+ info.log = "";
816
+ info.locatorLog = new LocatorLog(selectors);
817
+ }
818
+ let highPriorityTimeout = 5000;
819
+ let visibleOnlyTimeout = 6000;
820
+ let startTime = Date.now();
821
+ let locatorsCount = 0;
822
+ let lazy_scroll = false;
823
+ //let arrayMode = Array.isArray(selectors);
824
+ let scope = await this._findFrameScope(selectors, timeout, info);
620
825
  let selectorsLocators = null;
621
826
  selectorsLocators = selectors.locators;
622
827
  // group selectors by priority
@@ -652,18 +857,13 @@ class StableBrowser {
652
857
  }
653
858
  // info.log += "scanning locators in priority 1" + "\n";
654
859
  let onlyPriority3 = selectorsLocators[0].priority === 3;
655
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
860
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
656
861
  if (result.foundElements.length === 0) {
657
862
  // info.log += "scanning locators in priority 2" + "\n";
658
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
659
- }
660
- if (result.foundElements.length === 0 && onlyPriority3) {
661
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
863
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
662
864
  }
663
- else {
664
- if (result.foundElements.length === 0 && !highPriorityOnly) {
665
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
666
- }
865
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
866
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
667
867
  }
668
868
  let foundElements = result.foundElements;
669
869
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -703,24 +903,43 @@ class StableBrowser {
703
903
  return maxCountElement.locator;
704
904
  }
705
905
  }
706
- if (performance.now() - startTime > timeout) {
906
+ if (Date.now() - startTime > timeout) {
707
907
  break;
708
908
  }
709
- if (performance.now() - startTime > highPriorityTimeout) {
710
- info.log += "high priority timeout, will try all elements" + "\n";
909
+ if (Date.now() - startTime > highPriorityTimeout) {
910
+ //info.log += "high priority timeout, will try all elements" + "\n";
711
911
  highPriorityOnly = false;
912
+ if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
913
+ lazy_scroll = true;
914
+ await scrollPageToLoadLazyElements(this.page);
915
+ }
712
916
  }
713
- if (performance.now() - startTime > visibleOnlyTimeout) {
714
- info.log += "visible only timeout, will try all elements" + "\n";
917
+ if (Date.now() - startTime > visibleOnlyTimeout) {
918
+ //info.log += "visible only timeout, will try all elements" + "\n";
715
919
  visibleOnly = false;
716
920
  }
717
921
  await new Promise((resolve) => setTimeout(resolve, 1000));
922
+ // sheck of more of half of the timeout has passed
923
+ if (Date.now() - startTime > timeout / 2) {
924
+ highPriorityOnly = false;
925
+ visibleOnly = false;
926
+ }
718
927
  }
719
928
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
720
- info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
929
+ // if (info.locatorLog) {
930
+ // const lines = info.locatorLog.toString().split("\n");
931
+ // for (let line of lines) {
932
+ // this.logger.debug(line);
933
+ // }
934
+ // }
935
+ //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
936
+ info.failCause.locatorNotFound = true;
937
+ if (!info?.failCause?.lastError) {
938
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
939
+ }
721
940
  throw new Error("failed to locate first element no elements found, " + info.log);
722
941
  }
723
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
942
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
724
943
  let foundElements = [];
725
944
  const result = {
726
945
  foundElements: foundElements,
@@ -728,17 +947,20 @@ class StableBrowser {
728
947
  for (let i = 0; i < locatorsGroup.length; i++) {
729
948
  let foundLocators = [];
730
949
  try {
731
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
950
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
732
951
  }
733
952
  catch (e) {
734
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
735
- this.logger.debug(e);
953
+ // this call can fail it the browser is navigating
954
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
955
+ // this.logger.debug(e);
736
956
  foundLocators = [];
737
957
  try {
738
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
958
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
739
959
  }
740
960
  catch (e) {
741
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
961
+ if (logErrors) {
962
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
963
+ }
742
964
  }
743
965
  }
744
966
  if (foundLocators.length === 1) {
@@ -749,270 +971,350 @@ class StableBrowser {
749
971
  });
750
972
  result.locatorIndex = i;
751
973
  }
974
+ if (foundLocators.length > 1) {
975
+ // remove elements that consume the same space with 10 pixels tolerance
976
+ const boxes = [];
977
+ for (let j = 0; j < foundLocators.length; j++) {
978
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
979
+ }
980
+ for (let j = 0; j < boxes.length; j++) {
981
+ for (let k = 0; k < boxes.length; k++) {
982
+ if (j === k) {
983
+ continue;
984
+ }
985
+ // check if x, y, width, height are the same with 10 pixels tolerance
986
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
987
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
988
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
989
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
990
+ // as the element is not unique, will remove it
991
+ boxes.splice(k, 1);
992
+ k--;
993
+ }
994
+ }
995
+ }
996
+ if (boxes.length === 1) {
997
+ result.foundElements.push({
998
+ locator: boxes[0].locator.first(),
999
+ box: boxes[0].box,
1000
+ unique: true,
1001
+ });
1002
+ result.locatorIndex = i;
1003
+ }
1004
+ else if (logErrors) {
1005
+ info.failCause.foundMultiple = true;
1006
+ if (info.locatorLog) {
1007
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
1008
+ }
1009
+ }
1010
+ }
752
1011
  }
753
1012
  return result;
754
1013
  }
755
- async click(selectors, _params, options = {}, world = null) {
756
- this._validateSelectors(selectors);
1014
+ async simpleClick(elementDescription, _params, options = {}, world = null) {
1015
+ const state = {
1016
+ locate: false,
1017
+ scroll: false,
1018
+ highlight: false,
1019
+ _params,
1020
+ options,
1021
+ world,
1022
+ type: Types.CLICK,
1023
+ text: "Click element",
1024
+ operation: "simpleClick",
1025
+ log: "***** click on " + elementDescription + " *****\n",
1026
+ };
1027
+ _preCommand(state, this);
757
1028
  const startTime = Date.now();
758
- if (options && options.context) {
759
- selectors.locators[0].text = options.context;
1029
+ let timeout = 30000;
1030
+ if (options && options.timeout) {
1031
+ timeout = options.timeout;
760
1032
  }
761
- const info = {};
762
- info.log = "***** click on " + selectors.element_name + " *****\n";
763
- info.operation = "click";
764
- info.selectors = selectors;
765
- let error = null;
766
- let screenshotId = null;
767
- let screenshotPath = null;
768
- try {
769
- let element = await this._locate(selectors, info, _params);
770
- await this.scrollIfNeeded(element, info);
771
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1033
+ while (true) {
772
1034
  try {
773
- await this._highlightElements(element);
774
- await element.click();
775
- await new Promise((resolve) => setTimeout(resolve, 1000));
1035
+ const result = await locate_element(this.context, elementDescription, "click");
1036
+ if (result?.elementNumber >= 0) {
1037
+ const selectors = {
1038
+ frame: result?.frame,
1039
+ locators: [
1040
+ {
1041
+ css: result?.css,
1042
+ },
1043
+ ],
1044
+ };
1045
+ await this.click(selectors, _params, options, world);
1046
+ return;
1047
+ }
776
1048
  }
777
1049
  catch (e) {
778
- // await this.closeUnexpectedPopups();
779
- info.log += "click failed, will try again" + "\n";
780
- element = await this._locate(selectors, info, _params);
781
- await element.dispatchEvent("click");
782
- await new Promise((resolve) => setTimeout(resolve, 1000));
1050
+ if (performance.now() - startTime > timeout) {
1051
+ // throw e;
1052
+ try {
1053
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1054
+ }
1055
+ finally {
1056
+ await _commandFinally(state, this);
1057
+ }
1058
+ }
783
1059
  }
784
- await this.waitForPageLoad();
785
- return info;
1060
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1061
+ }
1062
+ }
1063
+ async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
1064
+ const state = {
1065
+ locate: false,
1066
+ scroll: false,
1067
+ highlight: false,
1068
+ _params,
1069
+ options,
1070
+ world,
1071
+ type: Types.FILL,
1072
+ text: "Fill element",
1073
+ operation: "simpleClickType",
1074
+ log: "***** click type on " + elementDescription + " *****\n",
1075
+ };
1076
+ _preCommand(state, this);
1077
+ const startTime = Date.now();
1078
+ let timeout = 30000;
1079
+ if (options && options.timeout) {
1080
+ timeout = options.timeout;
1081
+ }
1082
+ while (true) {
1083
+ try {
1084
+ const result = await locate_element(this.context, elementDescription, "fill", value);
1085
+ if (result?.elementNumber >= 0) {
1086
+ const selectors = {
1087
+ frame: result?.frame,
1088
+ locators: [
1089
+ {
1090
+ css: result?.css,
1091
+ },
1092
+ ],
1093
+ };
1094
+ await this.clickType(selectors, value, false, _params, options, world);
1095
+ return;
1096
+ }
1097
+ }
1098
+ catch (e) {
1099
+ if (performance.now() - startTime > timeout) {
1100
+ // throw e;
1101
+ try {
1102
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1103
+ }
1104
+ finally {
1105
+ await _commandFinally(state, this);
1106
+ }
1107
+ }
1108
+ }
1109
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1110
+ }
1111
+ }
1112
+ async click(selectors, _params, options = {}, world = null) {
1113
+ const state = {
1114
+ selectors,
1115
+ _params,
1116
+ options,
1117
+ world,
1118
+ text: "Click element",
1119
+ _text: "Click on " + selectors.element_name,
1120
+ type: Types.CLICK,
1121
+ operation: "click",
1122
+ log: "***** click on " + selectors.element_name + " *****\n",
1123
+ };
1124
+ try {
1125
+ await _preCommand(state, this);
1126
+ await performAction("click", state.element, options, this, state, _params);
1127
+ if (!this.fastMode) {
1128
+ await this.waitForPageLoad();
1129
+ }
1130
+ return state.info;
786
1131
  }
787
1132
  catch (e) {
788
- this.logger.error("click failed " + info.log);
789
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
790
- info.screenshotPath = screenshotPath;
791
- Object.assign(e, { info: info });
792
- error = e;
793
- throw e;
1133
+ await _commandError(state, e, this);
794
1134
  }
795
1135
  finally {
796
- const endTime = Date.now();
797
- this._reportToWorld(world, {
798
- element_name: selectors.element_name,
799
- type: Types.CLICK,
800
- text: `Click element`,
801
- screenshotId,
802
- result: error
803
- ? {
804
- status: "FAILED",
805
- startTime,
806
- endTime,
807
- message: error === null || error === void 0 ? void 0 : error.message,
808
- }
809
- : {
810
- status: "PASSED",
811
- startTime,
812
- endTime,
813
- },
814
- info: info,
815
- });
1136
+ await _commandFinally(state, this);
1137
+ }
1138
+ }
1139
+ async waitForElement(selectors, _params, options = {}, world = null) {
1140
+ const timeout = this._getFindElementTimeout(options);
1141
+ const state = {
1142
+ selectors,
1143
+ _params,
1144
+ options,
1145
+ world,
1146
+ text: "Wait for element",
1147
+ _text: "Wait for " + selectors.element_name,
1148
+ type: Types.WAIT_ELEMENT,
1149
+ operation: "waitForElement",
1150
+ log: "***** wait for " + selectors.element_name + " *****\n",
1151
+ };
1152
+ let found = false;
1153
+ try {
1154
+ await _preCommand(state, this);
1155
+ // if (state.options && state.options.context) {
1156
+ // state.selectors.locators[0].text = state.options.context;
1157
+ // }
1158
+ await state.element.waitFor({ timeout: timeout });
1159
+ found = true;
1160
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1161
+ }
1162
+ catch (e) {
1163
+ console.error("Error on waitForElement", e);
1164
+ // await _commandError(state, e, this);
1165
+ }
1166
+ finally {
1167
+ await _commandFinally(state, this);
816
1168
  }
1169
+ return found;
817
1170
  }
818
1171
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
819
- this._validateSelectors(selectors);
820
- const startTime = Date.now();
821
- const info = {};
822
- info.log = "";
823
- info.operation = "setCheck";
824
- info.checked = checked;
825
- info.selectors = selectors;
826
- let error = null;
827
- let screenshotId = null;
828
- let screenshotPath = null;
1172
+ const state = {
1173
+ selectors,
1174
+ _params,
1175
+ options,
1176
+ world,
1177
+ type: checked ? Types.CHECK : Types.UNCHECK,
1178
+ text: checked ? `Check element` : `Uncheck element`,
1179
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
1180
+ operation: "setCheck",
1181
+ log: "***** check " + selectors.element_name + " *****\n",
1182
+ };
829
1183
  try {
830
- let element = await this._locate(selectors, info, _params);
831
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1184
+ await _preCommand(state, this);
1185
+ state.info.checked = checked;
1186
+ // let element = await this._locate(selectors, info, _params);
1187
+ // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
832
1188
  try {
833
- await this._highlightElements(element);
834
- await element.setChecked(checked);
1189
+ // if (world && world.screenshot && !world.screenshotPath) {
1190
+ // console.log(`Highlighting while running from recorder`);
1191
+ await this._highlightElements(state.element);
1192
+ await state.element.setChecked(checked, { timeout: 2000 });
835
1193
  await new Promise((resolve) => setTimeout(resolve, 1000));
1194
+ // await this._unHighlightElements(element);
1195
+ // }
1196
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1197
+ // await this._unHighlightElements(element);
836
1198
  }
837
1199
  catch (e) {
838
1200
  if (e.message && e.message.includes("did not change its state")) {
839
1201
  this.logger.info("element did not change its state, ignoring...");
840
1202
  }
841
1203
  else {
842
- //await this.closeUnexpectedPopups();
843
- info.log += "setCheck failed, will try again" + "\n";
844
- element = await this._locate(selectors, info, _params);
845
- await element.setChecked(checked, { timeout: 5000, force: true });
846
1204
  await new Promise((resolve) => setTimeout(resolve, 1000));
1205
+ //await this.closeUnexpectedPopups();
1206
+ state.info.log += "setCheck failed, will try again" + "\n";
1207
+ state.element_found = false;
1208
+ try {
1209
+ state.element = await this._locate(selectors, state.info, _params, 100);
1210
+ state.element_found = true;
1211
+ // check the check state
1212
+ }
1213
+ catch (error) {
1214
+ // element dismissed
1215
+ }
1216
+ if (state.element_found) {
1217
+ const isChecked = await state.element.isChecked();
1218
+ if (isChecked !== checked) {
1219
+ // perform click
1220
+ await state.element.click({ timeout: 2000, force: true });
1221
+ }
1222
+ else {
1223
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1224
+ }
1225
+ }
847
1226
  }
848
1227
  }
849
1228
  await this.waitForPageLoad();
850
- return info;
1229
+ return state.info;
851
1230
  }
852
1231
  catch (e) {
853
- this.logger.error("setCheck failed " + info.log);
854
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
855
- info.screenshotPath = screenshotPath;
856
- Object.assign(e, { info: info });
857
- error = e;
858
- throw e;
1232
+ await _commandError(state, e, this);
859
1233
  }
860
1234
  finally {
861
- const endTime = Date.now();
862
- this._reportToWorld(world, {
863
- element_name: selectors.element_name,
864
- type: checked ? Types.CHECK : Types.UNCHECK,
865
- text: checked ? `Check element` : `Uncheck element`,
866
- screenshotId,
867
- result: error
868
- ? {
869
- status: "FAILED",
870
- startTime,
871
- endTime,
872
- message: error === null || error === void 0 ? void 0 : error.message,
873
- }
874
- : {
875
- status: "PASSED",
876
- startTime,
877
- endTime,
878
- },
879
- info: info,
880
- });
1235
+ await _commandFinally(state, this);
881
1236
  }
882
1237
  }
883
1238
  async hover(selectors, _params, options = {}, world = null) {
884
- this._validateSelectors(selectors);
885
- const startTime = Date.now();
886
- const info = {};
887
- info.log = "";
888
- info.operation = "hover";
889
- info.selectors = selectors;
890
- let error = null;
891
- let screenshotId = null;
892
- let screenshotPath = null;
1239
+ const state = {
1240
+ selectors,
1241
+ _params,
1242
+ options,
1243
+ world,
1244
+ type: Types.HOVER,
1245
+ text: `Hover element`,
1246
+ _text: `Hover on ${selectors.element_name}`,
1247
+ operation: "hover",
1248
+ log: "***** hover " + selectors.element_name + " *****\n",
1249
+ };
893
1250
  try {
894
- let element = await this._locate(selectors, info, _params);
895
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
896
- try {
897
- await this._highlightElements(element);
898
- await element.hover();
899
- await new Promise((resolve) => setTimeout(resolve, 1000));
900
- }
901
- catch (e) {
902
- //await this.closeUnexpectedPopups();
903
- info.log += "hover failed, will try again" + "\n";
904
- element = await this._locate(selectors, info, _params);
905
- await element.hover({ timeout: 10000 });
906
- await new Promise((resolve) => setTimeout(resolve, 1000));
907
- }
1251
+ await _preCommand(state, this);
1252
+ await performAction("hover", state.element, options, this, state, _params);
1253
+ await _screenshot(state, this);
908
1254
  await this.waitForPageLoad();
909
- return info;
1255
+ return state.info;
910
1256
  }
911
1257
  catch (e) {
912
- this.logger.error("hover failed " + info.log);
913
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
914
- info.screenshotPath = screenshotPath;
915
- Object.assign(e, { info: info });
916
- error = e;
917
- throw e;
1258
+ await _commandError(state, e, this);
918
1259
  }
919
1260
  finally {
920
- const endTime = Date.now();
921
- this._reportToWorld(world, {
922
- element_name: selectors.element_name,
923
- type: Types.HOVER,
924
- text: `Hover element`,
925
- screenshotId,
926
- result: error
927
- ? {
928
- status: "FAILED",
929
- startTime,
930
- endTime,
931
- message: error === null || error === void 0 ? void 0 : error.message,
932
- }
933
- : {
934
- status: "PASSED",
935
- startTime,
936
- endTime,
937
- },
938
- info: info,
939
- });
1261
+ await _commandFinally(state, this);
940
1262
  }
941
1263
  }
942
1264
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
943
- this._validateSelectors(selectors);
944
1265
  if (!values) {
945
1266
  throw new Error("values is null");
946
1267
  }
947
- const startTime = Date.now();
948
- let error = null;
949
- let screenshotId = null;
950
- let screenshotPath = null;
951
- const info = {};
952
- info.log = "";
953
- info.operation = "selectOptions";
954
- info.selectors = selectors;
1268
+ const state = {
1269
+ selectors,
1270
+ _params,
1271
+ options,
1272
+ world,
1273
+ value: values.toString(),
1274
+ type: Types.SELECT,
1275
+ text: `Select option: ${values}`,
1276
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1277
+ operation: "selectOption",
1278
+ log: "***** select option " + selectors.element_name + " *****\n",
1279
+ };
955
1280
  try {
956
- let element = await this._locate(selectors, info, _params);
957
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1281
+ await _preCommand(state, this);
958
1282
  try {
959
- await this._highlightElements(element);
960
- await element.selectOption(values);
1283
+ await state.element.selectOption(values);
961
1284
  }
962
1285
  catch (e) {
963
1286
  //await this.closeUnexpectedPopups();
964
- info.log += "selectOption failed, will try force" + "\n";
965
- await element.selectOption(values, { timeout: 10000, force: true });
1287
+ state.info.log += "selectOption failed, will try force" + "\n";
1288
+ await state.element.selectOption(values, { timeout: 10000, force: true });
966
1289
  }
967
1290
  await this.waitForPageLoad();
968
- return info;
1291
+ return state.info;
969
1292
  }
970
1293
  catch (e) {
971
- this.logger.error("selectOption failed " + info.log);
972
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
973
- info.screenshotPath = screenshotPath;
974
- Object.assign(e, { info: info });
975
- this.logger.info("click failed, will try next selector");
976
- error = e;
977
- throw e;
1294
+ await _commandError(state, e, this);
978
1295
  }
979
1296
  finally {
980
- const endTime = Date.now();
981
- this._reportToWorld(world, {
982
- element_name: selectors.element_name,
983
- type: Types.SELECT,
984
- text: `Select option: ${values}`,
985
- value: values.toString(),
986
- screenshotId,
987
- result: error
988
- ? {
989
- status: "FAILED",
990
- startTime,
991
- endTime,
992
- message: error === null || error === void 0 ? void 0 : error.message,
993
- }
994
- : {
995
- status: "PASSED",
996
- startTime,
997
- endTime,
998
- },
999
- info: info,
1000
- });
1297
+ await _commandFinally(state, this);
1001
1298
  }
1002
1299
  }
1003
1300
  async type(_value, _params = null, options = {}, world = null) {
1004
- const startTime = Date.now();
1005
- let error = null;
1006
- let screenshotId = null;
1007
- let screenshotPath = null;
1008
- const info = {};
1009
- info.log = "";
1010
- info.operation = "type";
1011
- _value = this._fixUsingParams(_value, _params);
1012
- info.value = _value;
1301
+ const state = {
1302
+ value: _value,
1303
+ _params,
1304
+ options,
1305
+ world,
1306
+ locate: false,
1307
+ scroll: false,
1308
+ highlight: false,
1309
+ type: Types.TYPE_PRESS,
1310
+ text: `Type value: ${_value}`,
1311
+ _text: `Type value: ${_value}`,
1312
+ operation: "type",
1313
+ log: "",
1314
+ };
1013
1315
  try {
1014
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1015
- const valueSegment = _value.split("&&");
1316
+ await _preCommand(state, this);
1317
+ const valueSegment = state.value.split("&&");
1016
1318
  for (let i = 0; i < valueSegment.length; i++) {
1017
1319
  if (i > 0) {
1018
1320
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1032,134 +1334,77 @@ class StableBrowser {
1032
1334
  await this.page.keyboard.type(value);
1033
1335
  }
1034
1336
  }
1035
- return info;
1337
+ return state.info;
1036
1338
  }
1037
1339
  catch (e) {
1038
- //await this.closeUnexpectedPopups();
1039
- this.logger.error("type failed " + info.log);
1040
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1041
- info.screenshotPath = screenshotPath;
1042
- Object.assign(e, { info: info });
1043
- error = e;
1044
- throw e;
1340
+ await _commandError(state, e, this);
1045
1341
  }
1046
1342
  finally {
1047
- const endTime = Date.now();
1048
- this._reportToWorld(world, {
1049
- type: Types.TYPE_PRESS,
1050
- screenshotId,
1051
- value: _value,
1052
- text: `type value: ${_value}`,
1053
- result: error
1054
- ? {
1055
- status: "FAILED",
1056
- startTime,
1057
- endTime,
1058
- message: error === null || error === void 0 ? void 0 : error.message,
1059
- }
1060
- : {
1061
- status: "PASSED",
1062
- startTime,
1063
- endTime,
1064
- },
1065
- info: info,
1066
- });
1343
+ await _commandFinally(state, this);
1067
1344
  }
1068
1345
  }
1069
1346
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
1070
- // set input value for non fillable inputs like date, time, range, color, etc.
1071
- this._validateSelectors(selectors);
1072
- const startTime = Date.now();
1073
- const info = {};
1074
- info.log = "***** set input value " + selectors.element_name + " *****\n";
1075
- info.operation = "setInputValue";
1076
- info.selectors = selectors;
1077
- value = this._fixUsingParams(value, _params);
1078
- info.value = value;
1079
- let error = null;
1080
- let screenshotId = null;
1081
- let screenshotPath = null;
1347
+ const state = {
1348
+ selectors,
1349
+ _params,
1350
+ value,
1351
+ options,
1352
+ world,
1353
+ type: Types.SET_INPUT,
1354
+ text: `Set input value`,
1355
+ operation: "setInputValue",
1356
+ log: "***** set input value " + selectors.element_name + " *****\n",
1357
+ };
1082
1358
  try {
1083
- value = await this._replaceWithLocalData(value, this);
1084
- let element = await this._locate(selectors, info, _params);
1085
- await this.scrollIfNeeded(element, info);
1086
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1087
- await this._highlightElements(element);
1359
+ await _preCommand(state, this);
1360
+ let value = await this._replaceWithLocalData(state.value, this);
1088
1361
  try {
1089
- await element.evaluateHandle((el, value) => {
1362
+ await state.element.evaluateHandle((el, value) => {
1090
1363
  el.value = value;
1091
1364
  }, value);
1092
1365
  }
1093
1366
  catch (error) {
1094
1367
  this.logger.error("setInputValue failed, will try again");
1095
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1096
- info.screenshotPath = screenshotPath;
1097
- Object.assign(error, { info: info });
1098
- await element.evaluateHandle((el, value) => {
1368
+ await _screenshot(state, this);
1369
+ Object.assign(error, { info: state.info });
1370
+ await state.element.evaluateHandle((el, value) => {
1099
1371
  el.value = value;
1100
1372
  });
1101
1373
  }
1102
1374
  }
1103
1375
  catch (e) {
1104
- this.logger.error("setInputValue failed " + info.log);
1105
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1106
- info.screenshotPath = screenshotPath;
1107
- Object.assign(e, { info: info });
1108
- error = e;
1109
- throw e;
1376
+ await _commandError(state, e, this);
1110
1377
  }
1111
1378
  finally {
1112
- const endTime = Date.now();
1113
- this._reportToWorld(world, {
1114
- element_name: selectors.element_name,
1115
- type: Types.SET_INPUT,
1116
- text: `Set input value`,
1117
- value: value,
1118
- screenshotId,
1119
- result: error
1120
- ? {
1121
- status: "FAILED",
1122
- startTime,
1123
- endTime,
1124
- message: error === null || error === void 0 ? void 0 : error.message,
1125
- }
1126
- : {
1127
- status: "PASSED",
1128
- startTime,
1129
- endTime,
1130
- },
1131
- info: info,
1132
- });
1379
+ await _commandFinally(state, this);
1133
1380
  }
1134
1381
  }
1135
1382
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1136
- this._validateSelectors(selectors);
1137
- const startTime = Date.now();
1138
- let error = null;
1139
- let screenshotId = null;
1140
- let screenshotPath = null;
1141
- const info = {};
1142
- info.log = "";
1143
- info.operation = Types.SET_DATE_TIME;
1144
- info.selectors = selectors;
1145
- info.value = value;
1383
+ const state = {
1384
+ selectors,
1385
+ _params,
1386
+ value: await this._replaceWithLocalData(value, this),
1387
+ options,
1388
+ world,
1389
+ type: Types.SET_DATE_TIME,
1390
+ text: `Set date time value: ${value}`,
1391
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1392
+ operation: "setDateTime",
1393
+ log: "***** set date time value " + selectors.element_name + " *****\n",
1394
+ throwError: false,
1395
+ };
1146
1396
  try {
1147
- value = await this._replaceWithLocalData(value, this);
1148
- let element = await this._locate(selectors, info, _params);
1149
- //insert red border around the element
1150
- await this.scrollIfNeeded(element, info);
1151
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1152
- await this._highlightElements(element);
1397
+ await _preCommand(state, this);
1153
1398
  try {
1154
- await element.click();
1399
+ await performAction("click", state.element, options, this, state, _params);
1155
1400
  await new Promise((resolve) => setTimeout(resolve, 500));
1156
1401
  if (format) {
1157
- value = dayjs(value).format(format);
1158
- await element.fill(value);
1402
+ state.value = dayjs(state.value).format(format);
1403
+ await state.element.fill(state.value);
1159
1404
  }
1160
1405
  else {
1161
- const dateTimeValue = await getDateTimeValue({ value, element });
1162
- await element.evaluateHandle((el, dateTimeValue) => {
1406
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1407
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1163
1408
  el.value = ""; // clear input
1164
1409
  el.value = dateTimeValue;
1165
1410
  }, dateTimeValue);
@@ -1172,20 +1417,19 @@ class StableBrowser {
1172
1417
  }
1173
1418
  catch (err) {
1174
1419
  //await this.closeUnexpectedPopups();
1175
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1420
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1176
1421
  this.logger.info("Trying again");
1177
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1178
- info.screenshotPath = screenshotPath;
1179
- Object.assign(err, { info: info });
1422
+ await _screenshot(state, this);
1423
+ Object.assign(err, { info: state.info });
1180
1424
  await element.click();
1181
1425
  await new Promise((resolve) => setTimeout(resolve, 500));
1182
1426
  if (format) {
1183
- value = dayjs(value).format(format);
1184
- await element.fill(value);
1427
+ state.value = dayjs(state.value).format(format);
1428
+ await state.element.fill(state.value);
1185
1429
  }
1186
1430
  else {
1187
- const dateTimeValue = await getDateTimeValue({ value, element });
1188
- await element.evaluateHandle((el, dateTimeValue) => {
1431
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1432
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1189
1433
  el.value = ""; // clear input
1190
1434
  el.value = dateTimeValue;
1191
1435
  }, dateTimeValue);
@@ -1198,85 +1442,63 @@ class StableBrowser {
1198
1442
  }
1199
1443
  }
1200
1444
  catch (e) {
1201
- error = e;
1202
- throw e;
1445
+ await _commandError(state, e, this);
1203
1446
  }
1204
1447
  finally {
1205
- const endTime = Date.now();
1206
- this._reportToWorld(world, {
1207
- element_name: selectors.element_name,
1208
- type: Types.SET_DATE_TIME,
1209
- screenshotId,
1210
- value: value,
1211
- text: `setDateTime input with value: ${value}`,
1212
- result: error
1213
- ? {
1214
- status: "FAILED",
1215
- startTime,
1216
- endTime,
1217
- message: error === null || error === void 0 ? void 0 : error.message,
1218
- }
1219
- : {
1220
- status: "PASSED",
1221
- startTime,
1222
- endTime,
1223
- },
1224
- info: info,
1225
- });
1448
+ await _commandFinally(state, this);
1226
1449
  }
1227
1450
  }
1228
1451
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
1229
1452
  _value = unEscapeString(_value);
1230
- this._validateSelectors(selectors);
1231
- const startTime = Date.now();
1232
- let error = null;
1233
- let screenshotId = null;
1234
- let screenshotPath = null;
1235
- const info = {};
1236
- info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1237
- info.operation = "clickType";
1238
- info.selectors = selectors;
1239
1453
  const newValue = await this._replaceWithLocalData(_value, world);
1454
+ const state = {
1455
+ selectors,
1456
+ _params,
1457
+ value: newValue,
1458
+ originalValue: _value,
1459
+ options,
1460
+ world,
1461
+ type: Types.FILL,
1462
+ text: `Click type input with value: ${_value}`,
1463
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1464
+ operation: "clickType",
1465
+ log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1466
+ };
1467
+ if (!options) {
1468
+ options = {};
1469
+ }
1240
1470
  if (newValue !== _value) {
1241
1471
  //this.logger.info(_value + "=" + newValue);
1242
1472
  _value = newValue;
1243
1473
  }
1244
- info.value = _value;
1245
1474
  try {
1246
- let element = await this._locate(selectors, info, _params);
1247
- //insert red border around the element
1248
- await this.scrollIfNeeded(element, info);
1249
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1250
- await this._highlightElements(element);
1251
- if (options === null || options === undefined || !options.press) {
1475
+ await _preCommand(state, this);
1476
+ state.info.value = _value;
1477
+ if (!options.press) {
1252
1478
  try {
1253
- let currentValue = await element.inputValue();
1479
+ let currentValue = await state.element.inputValue();
1254
1480
  if (currentValue) {
1255
- await element.fill("");
1481
+ await state.element.fill("");
1256
1482
  }
1257
1483
  }
1258
1484
  catch (e) {
1259
1485
  this.logger.info("unable to clear input value");
1260
1486
  }
1261
1487
  }
1262
- if (options === null || options === undefined || options.press) {
1263
- try {
1264
- await element.click({ timeout: 5000 });
1265
- }
1266
- catch (e) {
1267
- await element.dispatchEvent("click");
1268
- }
1488
+ if (options.press) {
1489
+ options.timeout = 5000;
1490
+ await performAction("click", state.element, options, this, state, _params);
1269
1491
  }
1270
1492
  else {
1271
1493
  try {
1272
- await element.focus();
1494
+ await state.element.focus();
1273
1495
  }
1274
1496
  catch (e) {
1275
- await element.dispatchEvent("focus");
1497
+ await state.element.dispatchEvent("focus");
1276
1498
  }
1277
1499
  }
1278
1500
  await new Promise((resolve) => setTimeout(resolve, 500));
1279
- const valueSegment = _value.split("&&");
1501
+ const valueSegment = state.value.split("&&");
1280
1502
  for (let i = 0; i < valueSegment.length; i++) {
1281
1503
  if (i > 0) {
1282
1504
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1296,13 +1518,21 @@ class StableBrowser {
1296
1518
  await new Promise((resolve) => setTimeout(resolve, 500));
1297
1519
  }
1298
1520
  }
1521
+ //if (!this.fastMode) {
1522
+ await _screenshot(state, this);
1523
+ //}
1299
1524
  if (enter === true) {
1300
1525
  await new Promise((resolve) => setTimeout(resolve, 2000));
1301
1526
  await this.page.keyboard.press("Enter");
1302
1527
  await this.waitForPageLoad();
1303
1528
  }
1304
1529
  else if (enter === false) {
1305
- await element.dispatchEvent("change");
1530
+ try {
1531
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1532
+ }
1533
+ catch (e) {
1534
+ // ignore
1535
+ }
1306
1536
  //await this.page.keyboard.press("Tab");
1307
1537
  }
1308
1538
  else {
@@ -1311,112 +1541,95 @@ class StableBrowser {
1311
1541
  await this.waitForPageLoad();
1312
1542
  }
1313
1543
  }
1314
- return info;
1544
+ return state.info;
1315
1545
  }
1316
1546
  catch (e) {
1317
- //await this.closeUnexpectedPopups();
1318
- this.logger.error("fill failed " + JSON.stringify(info));
1319
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1320
- info.screenshotPath = screenshotPath;
1321
- Object.assign(e, { info: info });
1322
- error = e;
1323
- throw e;
1547
+ await _commandError(state, e, this);
1324
1548
  }
1325
1549
  finally {
1326
- const endTime = Date.now();
1327
- this._reportToWorld(world, {
1328
- element_name: selectors.element_name,
1329
- type: Types.FILL,
1330
- screenshotId,
1331
- value: _value,
1332
- text: `clickType input with value: ${_value}`,
1333
- result: error
1334
- ? {
1335
- status: "FAILED",
1336
- startTime,
1337
- endTime,
1338
- message: error === null || error === void 0 ? void 0 : error.message,
1339
- }
1340
- : {
1341
- status: "PASSED",
1342
- startTime,
1343
- endTime,
1344
- },
1345
- info: info,
1346
- });
1550
+ await _commandFinally(state, this);
1347
1551
  }
1348
1552
  }
1349
1553
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
1350
- this._validateSelectors(selectors);
1351
- value = unEscapeString(value);
1352
- const startTime = Date.now();
1353
- let error = null;
1354
- let screenshotId = null;
1355
- let screenshotPath = null;
1356
- const info = {};
1357
- info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1358
- info.operation = "fill";
1359
- info.selectors = selectors;
1360
- info.value = value;
1554
+ const state = {
1555
+ selectors,
1556
+ _params,
1557
+ value: unEscapeString(value),
1558
+ options,
1559
+ world,
1560
+ type: Types.FILL,
1561
+ text: `Fill input with value: ${value}`,
1562
+ operation: "fill",
1563
+ log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
1564
+ };
1361
1565
  try {
1362
- let element = await this._locate(selectors, info, _params);
1363
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1364
- await this._highlightElements(element);
1365
- await element.fill(value);
1366
- await element.dispatchEvent("change");
1566
+ await _preCommand(state, this);
1567
+ await state.element.fill(value);
1568
+ await state.element.dispatchEvent("change");
1367
1569
  if (enter) {
1368
1570
  await new Promise((resolve) => setTimeout(resolve, 2000));
1369
1571
  await this.page.keyboard.press("Enter");
1370
1572
  }
1371
1573
  await this.waitForPageLoad();
1372
- return info;
1574
+ return state.info;
1373
1575
  }
1374
1576
  catch (e) {
1375
- //await this.closeUnexpectedPopups();
1376
- this.logger.error("fill failed " + info.log);
1377
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1378
- info.screenshotPath = screenshotPath;
1379
- Object.assign(e, { info: info });
1380
- error = e;
1381
- throw e;
1577
+ await _commandError(state, e, this);
1382
1578
  }
1383
1579
  finally {
1384
- const endTime = Date.now();
1385
- this._reportToWorld(world, {
1386
- element_name: selectors.element_name,
1387
- type: Types.FILL,
1388
- screenshotId,
1389
- value,
1390
- text: `Fill input with value: ${value}`,
1391
- result: error
1392
- ? {
1393
- status: "FAILED",
1394
- startTime,
1395
- endTime,
1396
- message: error === null || error === void 0 ? void 0 : error.message,
1397
- }
1398
- : {
1399
- status: "PASSED",
1400
- startTime,
1401
- endTime,
1402
- },
1403
- info: info,
1404
- });
1580
+ await _commandFinally(state, this);
1581
+ }
1582
+ }
1583
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1584
+ const state = {
1585
+ selectors,
1586
+ _params,
1587
+ files,
1588
+ value: '"' + files.join('", "') + '"',
1589
+ options,
1590
+ world,
1591
+ type: Types.SET_INPUT_FILES,
1592
+ text: `Set input files`,
1593
+ _text: `Set input files on ${selectors.element_name}`,
1594
+ operation: "setInputFiles",
1595
+ log: "***** set input files " + selectors.element_name + " *****\n",
1596
+ };
1597
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1598
+ try {
1599
+ await _preCommand(state, this);
1600
+ for (let i = 0; i < files.length; i++) {
1601
+ const file = files[i];
1602
+ const filePath = path.join(uploadsFolder, file);
1603
+ if (!fs.existsSync(filePath)) {
1604
+ throw new Error(`File not found: ${filePath}`);
1605
+ }
1606
+ state.files[i] = filePath;
1607
+ }
1608
+ await state.element.setInputFiles(files);
1609
+ return state.info;
1610
+ }
1611
+ catch (e) {
1612
+ await _commandError(state, e, this);
1613
+ }
1614
+ finally {
1615
+ await _commandFinally(state, this);
1405
1616
  }
1406
1617
  }
1407
1618
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1408
1619
  return await this._getText(selectors, 0, _params, options, info, world);
1409
1620
  }
1410
1621
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1411
- this._validateSelectors(selectors);
1622
+ const timeout = this._getFindElementTimeout(options);
1623
+ _validateSelectors(selectors);
1412
1624
  let screenshotId = null;
1413
1625
  let screenshotPath = null;
1414
1626
  if (!info.log) {
1415
1627
  info.log = "";
1628
+ info.locatorLog = new LocatorLog(selectors);
1416
1629
  }
1417
1630
  info.operation = "getText";
1418
1631
  info.selectors = selectors;
1419
- let element = await this._locate(selectors, info, _params);
1632
+ let element = await this._locate(selectors, info, _params, timeout);
1420
1633
  if (climb > 0) {
1421
1634
  const climbArray = [];
1422
1635
  for (let i = 0; i < climb; i++) {
@@ -1435,6 +1648,18 @@ class StableBrowser {
1435
1648
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1436
1649
  try {
1437
1650
  await this._highlightElements(element);
1651
+ // if (world && world.screenshot && !world.screenshotPath) {
1652
+ // // console.log(`Highlighting for get text while running from recorder`);
1653
+ // this._highlightElements(element)
1654
+ // .then(async () => {
1655
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1656
+ // this._unhighlightElements(element).then(
1657
+ // () => {}
1658
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1659
+ // );
1660
+ // })
1661
+ // .catch(e);
1662
+ // }
1438
1663
  const elementText = await element.innerText();
1439
1664
  return {
1440
1665
  text: elementText,
@@ -1446,189 +1671,219 @@ class StableBrowser {
1446
1671
  }
1447
1672
  catch (e) {
1448
1673
  //await this.closeUnexpectedPopups();
1449
- this.logger.info("no innerText will use textContent");
1674
+ this.logger.info("no innerText, will use textContent");
1450
1675
  const elementText = await element.textContent();
1451
1676
  return { text: elementText, screenshotId, screenshotPath, value: value };
1452
1677
  }
1453
1678
  }
1454
1679
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1455
- var _a;
1456
- this._validateSelectors(selectors);
1457
1680
  if (!pattern) {
1458
1681
  throw new Error("pattern is null");
1459
1682
  }
1460
1683
  if (!text) {
1461
1684
  throw new Error("text is null");
1462
1685
  }
1686
+ const state = {
1687
+ selectors,
1688
+ _params,
1689
+ pattern,
1690
+ value: pattern,
1691
+ options,
1692
+ world,
1693
+ locate: false,
1694
+ scroll: false,
1695
+ screenshot: false,
1696
+ highlight: false,
1697
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1698
+ text: `Verify element contains pattern: ${pattern}`,
1699
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1700
+ operation: "containsPattern",
1701
+ log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1702
+ };
1463
1703
  const newValue = await this._replaceWithLocalData(text, world);
1464
1704
  if (newValue !== text) {
1465
1705
  this.logger.info(text + "=" + newValue);
1466
1706
  text = newValue;
1467
1707
  }
1468
- const startTime = Date.now();
1469
- let error = null;
1470
- let screenshotId = null;
1471
- let screenshotPath = null;
1472
- const info = {};
1473
- info.log =
1474
- "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1475
- info.operation = "containsPattern";
1476
- info.selectors = selectors;
1477
- info.value = text;
1478
- info.pattern = pattern;
1479
1708
  let foundObj = null;
1480
1709
  try {
1481
- foundObj = await this._getText(selectors, 0, _params, options, info, world);
1710
+ await _preCommand(state, this);
1711
+ state.info.pattern = pattern;
1712
+ foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
1482
1713
  if (foundObj && foundObj.element) {
1483
- await this.scrollIfNeeded(foundObj.element, info);
1714
+ await this.scrollIfNeeded(foundObj.element, state.info);
1484
1715
  }
1485
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1716
+ await _screenshot(state, this);
1486
1717
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1487
1718
  pattern = pattern.replace("{text}", escapedText);
1488
1719
  let regex = new RegExp(pattern, "im");
1489
- 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))) {
1490
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1720
+ if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
1721
+ state.info.foundText = foundObj?.text;
1491
1722
  throw new Error("element doesn't contain text " + text);
1492
1723
  }
1493
- return info;
1724
+ return state.info;
1494
1725
  }
1495
1726
  catch (e) {
1496
- //await this.closeUnexpectedPopups();
1497
- this.logger.error("verify element contains text failed " + info.log);
1498
- this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
1499
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1500
- info.screenshotPath = screenshotPath;
1501
- Object.assign(e, { info: info });
1502
- error = e;
1503
- throw e;
1727
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1728
+ await _commandError(state, e, this);
1504
1729
  }
1505
1730
  finally {
1506
- const endTime = Date.now();
1507
- this._reportToWorld(world, {
1508
- element_name: selectors.element_name,
1509
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1510
- value: pattern,
1511
- text: `Verify element contains pattern: ${pattern}`,
1512
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1513
- result: error
1514
- ? {
1515
- status: "FAILED",
1516
- startTime,
1517
- endTime,
1518
- message: error === null || error === void 0 ? void 0 : error.message,
1519
- }
1520
- : {
1521
- status: "PASSED",
1522
- startTime,
1523
- endTime,
1524
- },
1525
- info: info,
1526
- });
1731
+ await _commandFinally(state, this);
1527
1732
  }
1528
1733
  }
1529
1734
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1530
- var _a, _b, _c;
1531
- this._validateSelectors(selectors);
1532
- text = unEscapeString(text);
1735
+ const timeout = this._getFindElementTimeout(options);
1736
+ const startTime = Date.now();
1737
+ const state = {
1738
+ selectors,
1739
+ _params,
1740
+ value: text,
1741
+ options,
1742
+ world,
1743
+ locate: false,
1744
+ scroll: false,
1745
+ screenshot: false,
1746
+ highlight: false,
1747
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1748
+ text: `Verify element contains text: ${text}`,
1749
+ operation: "containsText",
1750
+ log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
1751
+ };
1533
1752
  if (!text) {
1534
1753
  throw new Error("text is null");
1535
1754
  }
1536
- const startTime = Date.now();
1537
- let error = null;
1538
- let screenshotId = null;
1539
- let screenshotPath = null;
1540
- const info = {};
1541
- info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1542
- info.operation = "containsText";
1543
- info.selectors = selectors;
1755
+ text = unEscapeString(text);
1544
1756
  const newValue = await this._replaceWithLocalData(text, world);
1545
1757
  if (newValue !== text) {
1546
1758
  this.logger.info(text + "=" + newValue);
1547
1759
  text = newValue;
1548
1760
  }
1549
- info.value = text;
1550
1761
  let foundObj = null;
1551
1762
  try {
1552
- foundObj = await this._getText(selectors, climb, _params, options, info, world);
1553
- if (foundObj && foundObj.element) {
1554
- await this.scrollIfNeeded(foundObj.element, info);
1555
- }
1556
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1557
- const dateAlternatives = findDateAlternatives(text);
1558
- const numberAlternatives = findNumberAlternatives(text);
1559
- if (dateAlternatives.date) {
1560
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1561
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
1562
- ((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
1563
- return info;
1763
+ while (Date.now() - startTime < timeout) {
1764
+ try {
1765
+ await _preCommand(state, this);
1766
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1767
+ if (foundObj && foundObj.element) {
1768
+ await this.scrollIfNeeded(foundObj.element, state.info);
1564
1769
  }
1565
- }
1566
- throw new Error("element doesn't contain text " + text);
1567
- }
1568
- else if (numberAlternatives.number) {
1569
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1570
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(numberAlternatives.numbers[i])) ||
1571
- ((_b = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _b === void 0 ? void 0 : _b.includes(numberAlternatives.numbers[i]))) {
1572
- return info;
1770
+ await _screenshot(state, this);
1771
+ const dateAlternatives = findDateAlternatives(text);
1772
+ const numberAlternatives = findNumberAlternatives(text);
1773
+ if (dateAlternatives.date) {
1774
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1775
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1776
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1777
+ return state.info;
1778
+ }
1779
+ }
1780
+ }
1781
+ else if (numberAlternatives.number) {
1782
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1783
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1784
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1785
+ return state.info;
1786
+ }
1787
+ }
1788
+ }
1789
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1790
+ return state.info;
1573
1791
  }
1574
1792
  }
1575
- throw new Error("element doesn't contain text " + text);
1576
- }
1577
- 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))) {
1578
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1579
- info.value = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value;
1580
- throw new Error("element doesn't contain text " + text);
1793
+ catch (e) {
1794
+ // Log error but continue retrying until timeout is reached
1795
+ this.logger.warn("Retrying containsText due to: " + e.message);
1796
+ }
1797
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1581
1798
  }
1582
- return info;
1799
+ state.info.foundText = foundObj?.text;
1800
+ state.info.value = foundObj?.value;
1801
+ throw new Error("element doesn't contain text " + text);
1583
1802
  }
1584
1803
  catch (e) {
1585
- //await this.closeUnexpectedPopups();
1586
- this.logger.error("verify element contains text failed " + info.log);
1587
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1588
- info.screenshotPath = screenshotPath;
1589
- Object.assign(e, { info: info });
1590
- error = e;
1804
+ await _commandError(state, e, this);
1591
1805
  throw e;
1592
1806
  }
1593
1807
  finally {
1594
- const endTime = Date.now();
1595
- this._reportToWorld(world, {
1596
- element_name: selectors.element_name,
1597
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1598
- text: `Verify element contains text: ${text}`,
1599
- value: text,
1600
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1601
- result: error
1602
- ? {
1603
- status: "FAILED",
1604
- startTime,
1605
- endTime,
1606
- message: error === null || error === void 0 ? void 0 : error.message,
1607
- }
1608
- : {
1609
- status: "PASSED",
1610
- startTime,
1611
- endTime,
1612
- },
1613
- info: info,
1614
- });
1808
+ await _commandFinally(state, this);
1615
1809
  }
1616
1810
  }
1617
- _getDataFile(world = null) {
1618
- let dataFile = null;
1619
- if (world && world.reportFolder) {
1620
- dataFile = path.join(world.reportFolder, "data.json");
1811
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1812
+ const timeout = this._getFindElementTimeout(options);
1813
+ const startTime = Date.now();
1814
+ const state = {
1815
+ _params,
1816
+ value: referanceSnapshot,
1817
+ options,
1818
+ world,
1819
+ locate: false,
1820
+ scroll: false,
1821
+ screenshot: true,
1822
+ highlight: false,
1823
+ type: Types.SNAPSHOT_VALIDATION,
1824
+ text: `verify snapshot: ${referanceSnapshot}`,
1825
+ operation: "snapshotValidation",
1826
+ log: "***** verify snapshot *****\n",
1827
+ };
1828
+ if (!referanceSnapshot) {
1829
+ throw new Error("referanceSnapshot is null");
1830
+ }
1831
+ let text = null;
1832
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1833
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1621
1834
  }
1622
- else if (this.reportFolder) {
1623
- dataFile = path.join(this.reportFolder, "data.json");
1835
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1836
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1624
1837
  }
1625
- else if (this.context && this.context.reportFolder) {
1626
- dataFile = path.join(this.context.reportFolder, "data.json");
1838
+ else if (referanceSnapshot.startsWith("yaml:")) {
1839
+ text = referanceSnapshot.substring(5);
1627
1840
  }
1628
1841
  else {
1629
- dataFile = "data.json";
1842
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1843
+ }
1844
+ state.text = text;
1845
+ const newValue = await this._replaceWithLocalData(text, world);
1846
+ await _preCommand(state, this);
1847
+ let foundObj = null;
1848
+ try {
1849
+ let matchResult = null;
1850
+ while (Date.now() - startTime < timeout) {
1851
+ try {
1852
+ let scope = null;
1853
+ if (!frameSelectors) {
1854
+ scope = this.page;
1855
+ }
1856
+ else {
1857
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1858
+ }
1859
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1860
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1861
+ if (matchResult.errorLine !== -1) {
1862
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1863
+ }
1864
+ // highlight and screenshot
1865
+ try {
1866
+ await await highlightSnapshot(newValue, scope);
1867
+ await _screenshot(state, this);
1868
+ }
1869
+ catch (e) { }
1870
+ return state.info;
1871
+ }
1872
+ catch (e) {
1873
+ // Log error but continue retrying until timeout is reached
1874
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1875
+ }
1876
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1877
+ }
1878
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1879
+ }
1880
+ catch (e) {
1881
+ await _commandError(state, e, this);
1882
+ throw e;
1883
+ }
1884
+ finally {
1885
+ await _commandFinally(state, this);
1630
1886
  }
1631
- return dataFile;
1632
1887
  }
1633
1888
  async waitForUserInput(message, world = null) {
1634
1889
  if (!message) {
@@ -1658,13 +1913,22 @@ class StableBrowser {
1658
1913
  return;
1659
1914
  }
1660
1915
  // if data file exists, load it
1661
- const dataFile = this._getDataFile(world);
1916
+ const dataFile = _getDataFile(world, this.context, this);
1662
1917
  let data = this.getTestData(world);
1663
1918
  // merge the testData with the existing data
1664
1919
  Object.assign(data, testData);
1665
1920
  // save the data to the file
1666
1921
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1667
1922
  }
1923
+ overwriteTestData(testData, world = null) {
1924
+ if (!testData) {
1925
+ return;
1926
+ }
1927
+ // if data file exists, load it
1928
+ const dataFile = _getDataFile(world, this.context, this);
1929
+ // save the data to the file
1930
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1931
+ }
1668
1932
  _getDataFilePath(fileName) {
1669
1933
  let dataFile = path.join(this.project_path, "data", fileName);
1670
1934
  if (fs.existsSync(dataFile)) {
@@ -1761,12 +2025,7 @@ class StableBrowser {
1761
2025
  }
1762
2026
  }
1763
2027
  getTestData(world = null) {
1764
- const dataFile = this._getDataFile(world);
1765
- let data = {};
1766
- if (fs.existsSync(dataFile)) {
1767
- data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1768
- }
1769
- return data;
2028
+ return _getTestData(world, this.context, this);
1770
2029
  }
1771
2030
  async _screenShot(options = {}, world = null, info = null) {
1772
2031
  // collect url/path/title
@@ -1793,11 +2052,9 @@ class StableBrowser {
1793
2052
  if (!fs.existsSync(world.screenshotPath)) {
1794
2053
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1795
2054
  }
1796
- let nextIndex = 1;
1797
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1798
- nextIndex++;
1799
- }
1800
- const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
2055
+ // to make sure the path doesn't start with -
2056
+ const uuidStr = "id_" + randomUUID();
2057
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1801
2058
  try {
1802
2059
  await this.takeScreenshot(screenshotPath);
1803
2060
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1807,15 +2064,15 @@ class StableBrowser {
1807
2064
  // this.logger.info("unable to save screenshot " + screenshotPath);
1808
2065
  // }
1809
2066
  // });
2067
+ result.screenshotId = uuidStr;
2068
+ result.screenshotPath = screenshotPath;
2069
+ if (info && info.box) {
2070
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
2071
+ }
1810
2072
  }
1811
2073
  catch (e) {
1812
2074
  this.logger.info("unable to take screenshot, ignored");
1813
2075
  }
1814
- result.screenshotId = nextIndex;
1815
- result.screenshotPath = screenshotPath;
1816
- if (info && info.box) {
1817
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1818
- }
1819
2076
  }
1820
2077
  else if (options && options.screenshot) {
1821
2078
  result.screenshotPath = options.screenshotPath;
@@ -1840,7 +2097,6 @@ class StableBrowser {
1840
2097
  }
1841
2098
  async takeScreenshot(screenshotPath) {
1842
2099
  const playContext = this.context.playContext;
1843
- const client = await playContext.newCDPSession(this.page);
1844
2100
  // Using CDP to capture the screenshot
1845
2101
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
1846
2102
  document.body.scrollWidth,
@@ -1850,165 +2106,560 @@ class StableBrowser {
1850
2106
  document.body.clientWidth,
1851
2107
  document.documentElement.clientWidth,
1852
2108
  ])));
1853
- const viewportHeight = Math.max(...(await this.page.evaluate(() => [
1854
- document.body.scrollHeight,
1855
- document.documentElement.scrollHeight,
1856
- document.body.offsetHeight,
1857
- document.documentElement.offsetHeight,
1858
- document.body.clientHeight,
1859
- document.documentElement.clientHeight,
1860
- ])));
1861
- const { data } = await client.send("Page.captureScreenshot", {
1862
- format: "png",
1863
- // clip: {
1864
- // x: 0,
1865
- // y: 0,
1866
- // width: viewportWidth,
1867
- // height: viewportHeight,
1868
- // scale: 1,
1869
- // },
1870
- });
1871
- if (!screenshotPath) {
1872
- return data;
1873
- }
1874
- let screenshotBuffer = Buffer.from(data, "base64");
1875
- const sharpBuffer = sharp(screenshotBuffer);
1876
- const metadata = await sharpBuffer.metadata();
1877
- //check if you are on retina display and reduce the quality of the image
1878
- if (metadata.width > viewportWidth || metadata.height > viewportHeight) {
1879
- screenshotBuffer = await sharpBuffer
1880
- .resize(viewportWidth, viewportHeight, {
1881
- fit: sharp.fit.inside,
1882
- withoutEnlargement: true,
1883
- })
1884
- .toBuffer();
1885
- }
1886
- fs.writeFileSync(screenshotPath, screenshotBuffer);
1887
- await client.detach();
2109
+ let screenshotBuffer = null;
2110
+ // if (focusedElement) {
2111
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2112
+ // await this._unhighlightElements(focusedElement);
2113
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2114
+ // console.log(`Unhighlighted previous element`);
2115
+ // }
2116
+ // if (focusedElement) {
2117
+ // await this._highlightElements(focusedElement);
2118
+ // }
2119
+ if (this.context.browserName === "chromium") {
2120
+ const client = await playContext.newCDPSession(this.page);
2121
+ const { data } = await client.send("Page.captureScreenshot", {
2122
+ format: "png",
2123
+ // clip: {
2124
+ // x: 0,
2125
+ // y: 0,
2126
+ // width: viewportWidth,
2127
+ // height: viewportHeight,
2128
+ // scale: 1,
2129
+ // },
2130
+ });
2131
+ await client.detach();
2132
+ if (!screenshotPath) {
2133
+ return data;
2134
+ }
2135
+ screenshotBuffer = Buffer.from(data, "base64");
2136
+ }
2137
+ else {
2138
+ screenshotBuffer = await this.page.screenshot();
2139
+ }
2140
+ // if (focusedElement) {
2141
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
2142
+ // await this._unhighlightElements(focusedElement);
2143
+ // }
2144
+ let image = await Jimp.read(screenshotBuffer);
2145
+ // Get the image dimensions
2146
+ const { width, height } = image.bitmap;
2147
+ const resizeRatio = viewportWidth / width;
2148
+ // Resize the image to fit within the viewport dimensions without enlarging
2149
+ if (width > viewportWidth) {
2150
+ image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
2151
+ await image.write(screenshotPath);
2152
+ }
2153
+ else {
2154
+ fs.writeFileSync(screenshotPath, screenshotBuffer);
2155
+ }
2156
+ return screenshotBuffer;
1888
2157
  }
1889
2158
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1890
- this._validateSelectors(selectors);
1891
- const startTime = Date.now();
1892
- let error = null;
1893
- let screenshotId = null;
1894
- let screenshotPath = null;
2159
+ const state = {
2160
+ selectors,
2161
+ _params,
2162
+ options,
2163
+ world,
2164
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2165
+ text: `Verify element exists in page`,
2166
+ operation: "verifyElementExistInPage",
2167
+ log: "***** verify element " + selectors.element_name + " exists in page *****\n",
2168
+ };
1895
2169
  await new Promise((resolve) => setTimeout(resolve, 2000));
1896
- const info = {};
1897
- info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1898
- info.operation = "verify";
1899
- info.selectors = selectors;
1900
2170
  try {
1901
- const element = await this._locate(selectors, info, _params);
1902
- if (element) {
1903
- await this.scrollIfNeeded(element, info);
1904
- }
1905
- await this._highlightElements(element);
1906
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1907
- await expect(element).toHaveCount(1, { timeout: 10000 });
1908
- return info;
2171
+ await _preCommand(state, this);
2172
+ await expect(state.element).toHaveCount(1, { timeout: 10000 });
2173
+ return state.info;
1909
2174
  }
1910
2175
  catch (e) {
1911
- //await this.closeUnexpectedPopups();
1912
- this.logger.error("verify failed " + info.log);
1913
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1914
- info.screenshotPath = screenshotPath;
1915
- Object.assign(e, { info: info });
1916
- error = e;
1917
- throw e;
2176
+ await _commandError(state, e, this);
1918
2177
  }
1919
2178
  finally {
1920
- const endTime = Date.now();
1921
- this._reportToWorld(world, {
1922
- element_name: selectors.element_name,
1923
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1924
- text: "Verify element exists in page",
1925
- screenshotId,
1926
- result: error
1927
- ? {
1928
- status: "FAILED",
1929
- startTime,
1930
- endTime,
1931
- message: error === null || error === void 0 ? void 0 : error.message,
1932
- }
1933
- : {
1934
- status: "PASSED",
1935
- startTime,
1936
- endTime,
1937
- },
1938
- info: info,
1939
- });
2179
+ await _commandFinally(state, this);
1940
2180
  }
1941
2181
  }
1942
2182
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1943
- this._validateSelectors(selectors);
1944
- const startTime = Date.now();
1945
- let error = null;
1946
- let screenshotId = null;
1947
- let screenshotPath = null;
2183
+ const state = {
2184
+ selectors,
2185
+ _params,
2186
+ attribute,
2187
+ variable,
2188
+ options,
2189
+ world,
2190
+ type: Types.EXTRACT,
2191
+ text: `Extract attribute from element`,
2192
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
2193
+ operation: "extractAttribute",
2194
+ log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
2195
+ allowDisabled: true,
2196
+ };
1948
2197
  await new Promise((resolve) => setTimeout(resolve, 2000));
1949
- const info = {};
1950
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1951
- info.operation = "extract";
1952
- info.selectors = selectors;
1953
2198
  try {
1954
- const element = await this._locate(selectors, info, _params);
1955
- await this._highlightElements(element);
1956
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2199
+ await _preCommand(state, this);
1957
2200
  switch (attribute) {
1958
2201
  case "inner_text":
1959
- info.value = await element.innerText();
2202
+ state.value = await state.element.innerText();
1960
2203
  break;
1961
2204
  case "href":
1962
- info.value = await element.getAttribute("href");
2205
+ state.value = await state.element.getAttribute("href");
1963
2206
  break;
1964
2207
  case "value":
1965
- info.value = await element.inputValue();
2208
+ state.value = await state.element.inputValue();
2209
+ break;
2210
+ case "text":
2211
+ state.value = await state.element.textContent();
1966
2212
  break;
1967
2213
  default:
1968
- info.value = await element.getAttribute(attribute);
2214
+ state.value = await state.element.getAttribute(attribute);
1969
2215
  break;
1970
2216
  }
1971
- this[variable] = info.value;
1972
- if (world) {
1973
- world[variable] = info.value;
2217
+ if (options !== null) {
2218
+ if (options.regex && options.regex !== "") {
2219
+ // Construct a regex pattern from the provided string
2220
+ const regex = options.regex.slice(1, -1);
2221
+ const regexPattern = new RegExp(regex, "g");
2222
+ const matches = state.value.match(regexPattern);
2223
+ if (matches) {
2224
+ let newValue = "";
2225
+ for (const match of matches) {
2226
+ newValue += match;
2227
+ }
2228
+ state.value = newValue;
2229
+ }
2230
+ }
2231
+ if (options.trimSpaces && options.trimSpaces === true) {
2232
+ state.value = state.value.trim();
2233
+ }
1974
2234
  }
1975
- this.setTestData({ [variable]: info.value }, world);
1976
- this.logger.info("set test data: " + variable + "=" + info.value);
1977
- return info;
2235
+ state.info.value = state.value;
2236
+ this.setTestData({ [variable]: state.value }, world);
2237
+ this.logger.info("set test data: " + variable + "=" + state.value);
2238
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2239
+ return state.info;
1978
2240
  }
1979
2241
  catch (e) {
1980
- //await this.closeUnexpectedPopups();
1981
- this.logger.error("extract failed " + info.log);
1982
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1983
- info.screenshotPath = screenshotPath;
1984
- Object.assign(e, { info: info });
1985
- error = e;
1986
- throw e;
2242
+ await _commandError(state, e, this);
1987
2243
  }
1988
2244
  finally {
1989
- const endTime = Date.now();
1990
- this._reportToWorld(world, {
1991
- element_name: selectors.element_name,
1992
- type: Types.EXTRACT_ATTRIBUTE,
1993
- variable: variable,
1994
- value: info.value,
1995
- text: "Extract attribute from element",
1996
- screenshotId,
1997
- result: error
1998
- ? {
1999
- status: "FAILED",
2000
- startTime,
2001
- endTime,
2002
- message: error === null || error === void 0 ? void 0 : error.message,
2245
+ await _commandFinally(state, this);
2246
+ }
2247
+ }
2248
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2249
+ const state = {
2250
+ selectors,
2251
+ _params,
2252
+ property,
2253
+ variable,
2254
+ options,
2255
+ world,
2256
+ type: Types.EXTRACT_PROPERTY,
2257
+ text: `Extract property from element`,
2258
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2259
+ operation: "extractProperty",
2260
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2261
+ allowDisabled: true,
2262
+ };
2263
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2264
+ try {
2265
+ await _preCommand(state, this);
2266
+ switch (property) {
2267
+ case "inner_text":
2268
+ state.value = await state.element.innerText();
2269
+ break;
2270
+ case "href":
2271
+ state.value = await state.element.getAttribute("href");
2272
+ break;
2273
+ case "value":
2274
+ state.value = await state.element.inputValue();
2275
+ break;
2276
+ case "text":
2277
+ state.value = await state.element.textContent();
2278
+ break;
2279
+ default:
2280
+ if (property.startsWith("dataset.")) {
2281
+ const dataAttribute = property.substring(8);
2282
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2003
2283
  }
2004
- : {
2005
- status: "PASSED",
2006
- startTime,
2007
- endTime,
2008
- },
2009
- info: info,
2010
- });
2284
+ else {
2285
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2286
+ }
2287
+ }
2288
+ if (options !== null) {
2289
+ if (options.regex && options.regex !== "") {
2290
+ // Construct a regex pattern from the provided string
2291
+ const regex = options.regex.slice(1, -1);
2292
+ const regexPattern = new RegExp(regex, "g");
2293
+ const matches = state.value.match(regexPattern);
2294
+ if (matches) {
2295
+ let newValue = "";
2296
+ for (const match of matches) {
2297
+ newValue += match;
2298
+ }
2299
+ state.value = newValue;
2300
+ }
2301
+ }
2302
+ if (options.trimSpaces && options.trimSpaces === true) {
2303
+ state.value = state.value.trim();
2304
+ }
2305
+ }
2306
+ state.info.value = state.value;
2307
+ this.setTestData({ [variable]: state.value }, world);
2308
+ this.logger.info("set test data: " + variable + "=" + state.value);
2309
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2310
+ return state.info;
2311
+ }
2312
+ catch (e) {
2313
+ await _commandError(state, e, this);
2314
+ }
2315
+ finally {
2316
+ await _commandFinally(state, this);
2317
+ }
2318
+ }
2319
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2320
+ const state = {
2321
+ selectors,
2322
+ _params,
2323
+ attribute,
2324
+ value,
2325
+ options,
2326
+ world,
2327
+ type: Types.VERIFY_ATTRIBUTE,
2328
+ highlight: true,
2329
+ screenshot: true,
2330
+ text: `Verify element attribute`,
2331
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2332
+ operation: "verifyAttribute",
2333
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2334
+ allowDisabled: true,
2335
+ };
2336
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2337
+ let val;
2338
+ let expectedValue;
2339
+ try {
2340
+ await _preCommand(state, this);
2341
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2342
+ state.info.expectedValue = expectedValue;
2343
+ switch (attribute) {
2344
+ case "innerText":
2345
+ val = String(await state.element.innerText());
2346
+ break;
2347
+ case "text":
2348
+ val = String(await state.element.textContent());
2349
+ break;
2350
+ case "value":
2351
+ val = String(await state.element.inputValue());
2352
+ break;
2353
+ case "checked":
2354
+ val = String(await state.element.isChecked());
2355
+ break;
2356
+ case "disabled":
2357
+ val = String(await state.element.isDisabled());
2358
+ break;
2359
+ case "readOnly":
2360
+ const isEditable = await state.element.isEditable();
2361
+ val = String(!isEditable);
2362
+ break;
2363
+ default:
2364
+ val = String(await state.element.getAttribute(attribute));
2365
+ break;
2366
+ }
2367
+ state.info.value = val;
2368
+ let regex;
2369
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2370
+ const patternBody = expectedValue.slice(1, -1);
2371
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2372
+ regex = new RegExp(processedPattern, "gs");
2373
+ state.info.regex = true;
2374
+ }
2375
+ else {
2376
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2377
+ regex = new RegExp(escapedPattern, "g");
2378
+ }
2379
+ if (attribute === "innerText") {
2380
+ if (state.info.regex) {
2381
+ if (!regex.test(val)) {
2382
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2383
+ state.info.failCause.assertionFailed = true;
2384
+ state.info.failCause.lastError = errorMessage;
2385
+ throw new Error(errorMessage);
2386
+ }
2387
+ }
2388
+ else {
2389
+ const valLines = val.split("\n");
2390
+ const expectedLines = expectedValue.split("\n");
2391
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2392
+ if (!isPart) {
2393
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2394
+ state.info.failCause.assertionFailed = true;
2395
+ state.info.failCause.lastError = errorMessage;
2396
+ throw new Error(errorMessage);
2397
+ }
2398
+ }
2399
+ }
2400
+ else {
2401
+ if (!val.match(regex)) {
2402
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2403
+ state.info.failCause.assertionFailed = true;
2404
+ state.info.failCause.lastError = errorMessage;
2405
+ throw new Error(errorMessage);
2406
+ }
2407
+ }
2408
+ return state.info;
2409
+ }
2410
+ catch (e) {
2411
+ await _commandError(state, e, this);
2412
+ }
2413
+ finally {
2414
+ await _commandFinally(state, this);
2415
+ }
2416
+ }
2417
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2418
+ const state = {
2419
+ selectors,
2420
+ _params,
2421
+ property,
2422
+ value,
2423
+ options,
2424
+ world,
2425
+ type: Types.VERIFY_PROPERTY,
2426
+ highlight: true,
2427
+ screenshot: true,
2428
+ text: `Verify element property`,
2429
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2430
+ operation: "verifyProperty",
2431
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2432
+ allowDisabled: true,
2433
+ };
2434
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2435
+ let val;
2436
+ let expectedValue;
2437
+ try {
2438
+ await _preCommand(state, this);
2439
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2440
+ state.info.expectedValue = expectedValue;
2441
+ switch (property) {
2442
+ case "innerText":
2443
+ val = String(await state.element.innerText());
2444
+ break;
2445
+ case "text":
2446
+ val = String(await state.element.textContent());
2447
+ break;
2448
+ case "value":
2449
+ val = String(await state.element.inputValue());
2450
+ break;
2451
+ case "checked":
2452
+ val = String(await state.element.isChecked());
2453
+ break;
2454
+ case "disabled":
2455
+ val = String(await state.element.isDisabled());
2456
+ break;
2457
+ case "readOnly":
2458
+ const isEditable = await state.element.isEditable();
2459
+ val = String(!isEditable);
2460
+ break;
2461
+ case "innerHTML":
2462
+ val = String(await state.element.innerHTML());
2463
+ break;
2464
+ case "outerHTML":
2465
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2466
+ break;
2467
+ default:
2468
+ if (property.startsWith("dataset.")) {
2469
+ const dataAttribute = property.substring(8);
2470
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2471
+ }
2472
+ else {
2473
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2474
+ }
2475
+ }
2476
+ // Helper function to remove all style="" attributes
2477
+ const removeStyleAttributes = (htmlString) => {
2478
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2479
+ };
2480
+ // Remove style attributes for innerHTML and outerHTML properties
2481
+ if (property === "innerHTML" || property === "outerHTML") {
2482
+ val = removeStyleAttributes(val);
2483
+ expectedValue = removeStyleAttributes(expectedValue);
2484
+ }
2485
+ state.info.value = val;
2486
+ let regex;
2487
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2488
+ const patternBody = expectedValue.slice(1, -1);
2489
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2490
+ regex = new RegExp(processedPattern, "gs");
2491
+ state.info.regex = true;
2492
+ }
2493
+ else {
2494
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2495
+ regex = new RegExp(escapedPattern, "g");
2496
+ }
2497
+ if (property === "innerText") {
2498
+ if (state.info.regex) {
2499
+ if (!regex.test(val)) {
2500
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2501
+ state.info.failCause.assertionFailed = true;
2502
+ state.info.failCause.lastError = errorMessage;
2503
+ throw new Error(errorMessage);
2504
+ }
2505
+ }
2506
+ else {
2507
+ // Fix: Replace escaped newlines with actual newlines before splitting
2508
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2509
+ const valLines = val.split("\n");
2510
+ const expectedLines = normalizedExpectedValue.split("\n");
2511
+ // Check if all expected lines are present in the actual lines
2512
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2513
+ if (!isPart) {
2514
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2515
+ state.info.failCause.assertionFailed = true;
2516
+ state.info.failCause.lastError = errorMessage;
2517
+ throw new Error(errorMessage);
2518
+ }
2519
+ }
2520
+ }
2521
+ else {
2522
+ if (!val.match(regex)) {
2523
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2524
+ state.info.failCause.assertionFailed = true;
2525
+ state.info.failCause.lastError = errorMessage;
2526
+ throw new Error(errorMessage);
2527
+ }
2528
+ }
2529
+ return state.info;
2530
+ }
2531
+ catch (e) {
2532
+ await _commandError(state, e, this);
2533
+ }
2534
+ finally {
2535
+ await _commandFinally(state, this);
2536
+ }
2537
+ }
2538
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2539
+ // Convert timeout from seconds to milliseconds
2540
+ const timeoutMs = timeout * 1000;
2541
+ const state = {
2542
+ selectors,
2543
+ _params,
2544
+ condition,
2545
+ timeout: timeoutMs, // Store as milliseconds for internal use
2546
+ options,
2547
+ world,
2548
+ type: Types.CONDITIONAL_WAIT,
2549
+ highlight: true,
2550
+ screenshot: true,
2551
+ text: `Conditional wait for element`,
2552
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2553
+ operation: "conditionalWait",
2554
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2555
+ allowDisabled: true,
2556
+ info: {},
2557
+ };
2558
+ // Initialize startTime outside try block to ensure it's always accessible
2559
+ const startTime = Date.now();
2560
+ let conditionMet = false;
2561
+ let currentValue = null;
2562
+ let lastError = null;
2563
+ // Main retry loop - continues until timeout or condition is met
2564
+ while (Date.now() - startTime < timeoutMs) {
2565
+ const elapsedTime = Date.now() - startTime;
2566
+ const remainingTime = timeoutMs - elapsedTime;
2567
+ try {
2568
+ // Try to execute _preCommand (element location)
2569
+ await _preCommand(state, this);
2570
+ // If _preCommand succeeds, start condition checking
2571
+ const checkCondition = async () => {
2572
+ try {
2573
+ switch (condition.toLowerCase()) {
2574
+ case "checked":
2575
+ currentValue = await state.element.isChecked();
2576
+ return currentValue === true;
2577
+ case "unchecked":
2578
+ currentValue = await state.element.isChecked();
2579
+ return currentValue === false;
2580
+ case "visible":
2581
+ currentValue = await state.element.isVisible();
2582
+ return currentValue === true;
2583
+ case "hidden":
2584
+ currentValue = await state.element.isVisible();
2585
+ return currentValue === false;
2586
+ case "enabled":
2587
+ currentValue = await state.element.isDisabled();
2588
+ return currentValue === false;
2589
+ case "disabled":
2590
+ currentValue = await state.element.isDisabled();
2591
+ return currentValue === true;
2592
+ case "editable":
2593
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2594
+ currentValue = await state.element.isContentEditable();
2595
+ return currentValue === true;
2596
+ default:
2597
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2598
+ state.info.success = false;
2599
+ return false;
2600
+ }
2601
+ }
2602
+ catch (error) {
2603
+ // Don't throw here, just return false to continue retrying
2604
+ return false;
2605
+ }
2606
+ };
2607
+ // Inner loop for condition checking (once element is located)
2608
+ while (Date.now() - startTime < timeoutMs) {
2609
+ const currentElapsedTime = Date.now() - startTime;
2610
+ conditionMet = await checkCondition();
2611
+ if (conditionMet) {
2612
+ break;
2613
+ }
2614
+ // Check if we still have time for another attempt
2615
+ if (Date.now() - startTime + 50 < timeoutMs) {
2616
+ await new Promise((res) => setTimeout(res, 50));
2617
+ }
2618
+ else {
2619
+ break;
2620
+ }
2621
+ }
2622
+ // If we got here and condition is met, break out of main loop
2623
+ if (conditionMet) {
2624
+ break;
2625
+ }
2626
+ // If condition not met but no exception, we've timed out
2627
+ break;
2628
+ }
2629
+ catch (e) {
2630
+ lastError = e;
2631
+ const currentElapsedTime = Date.now() - startTime;
2632
+ const timeLeft = timeoutMs - currentElapsedTime;
2633
+ // Check if we have enough time left to retry
2634
+ if (timeLeft > 100) {
2635
+ await new Promise((resolve) => setTimeout(resolve, 50));
2636
+ }
2637
+ else {
2638
+ break;
2639
+ }
2640
+ }
2641
+ }
2642
+ const actualWaitTime = Date.now() - startTime;
2643
+ state.info = {
2644
+ success: conditionMet,
2645
+ conditionMet,
2646
+ actualWaitTime,
2647
+ currentValue,
2648
+ lastError: lastError?.message || null,
2649
+ message: conditionMet
2650
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2651
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2652
+ };
2653
+ if (lastError) {
2654
+ state.log += `Last error: ${lastError.message}\n`;
2655
+ }
2656
+ try {
2657
+ await _commandFinally(state, this);
2011
2658
  }
2659
+ catch (finallyError) {
2660
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2661
+ }
2662
+ return state.info;
2012
2663
  }
2013
2664
  async extractEmailData(emailAddress, options, world) {
2014
2665
  if (!emailAddress) {
@@ -2028,7 +2679,7 @@ class StableBrowser {
2028
2679
  if (options && options.timeout) {
2029
2680
  timeout = options.timeout;
2030
2681
  }
2031
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2682
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2032
2683
  const request = {
2033
2684
  method: "POST",
2034
2685
  url: serviceUrl,
@@ -2084,7 +2735,8 @@ class StableBrowser {
2084
2735
  catch (e) {
2085
2736
  errorCount++;
2086
2737
  if (errorCount > 3) {
2087
- throw e;
2738
+ // throw e;
2739
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
2088
2740
  }
2089
2741
  // ignore
2090
2742
  }
@@ -2098,27 +2750,32 @@ class StableBrowser {
2098
2750
  async _highlightElements(scope, css) {
2099
2751
  try {
2100
2752
  if (!scope) {
2753
+ // console.log(`Scope is not defined`);
2101
2754
  return;
2102
2755
  }
2103
2756
  if (!css) {
2104
2757
  scope
2105
2758
  .evaluate((node) => {
2106
2759
  if (node && node.style) {
2107
- let originalBorder = node.style.border;
2108
- node.style.border = "2px solid red";
2760
+ let originalOutline = node.style.outline;
2761
+ // console.log(`Original outline was: ${originalOutline}`);
2762
+ // node.__previousOutline = originalOutline;
2763
+ node.style.outline = "2px solid red";
2764
+ // console.log(`New outline is: ${node.style.outline}`);
2109
2765
  if (window) {
2110
2766
  window.addEventListener("beforeunload", function (e) {
2111
- node.style.border = originalBorder;
2767
+ node.style.outline = originalOutline;
2112
2768
  });
2113
2769
  }
2114
2770
  setTimeout(function () {
2115
- node.style.border = originalBorder;
2771
+ node.style.outline = originalOutline;
2116
2772
  }, 2000);
2117
2773
  }
2118
2774
  })
2119
2775
  .then(() => { })
2120
2776
  .catch((e) => {
2121
2777
  // ignore
2778
+ // console.error(`Could not highlight node : ${e}`);
2122
2779
  });
2123
2780
  }
2124
2781
  else {
@@ -2134,17 +2791,18 @@ class StableBrowser {
2134
2791
  if (!element.style) {
2135
2792
  return;
2136
2793
  }
2137
- var originalBorder = element.style.border;
2794
+ let originalOutline = element.style.outline;
2795
+ element.__previousOutline = originalOutline;
2138
2796
  // Set the new border to be red and 2px solid
2139
- element.style.border = "2px solid red";
2797
+ element.style.outline = "2px solid red";
2140
2798
  if (window) {
2141
2799
  window.addEventListener("beforeunload", function (e) {
2142
- element.style.border = originalBorder;
2800
+ element.style.outline = originalOutline;
2143
2801
  });
2144
2802
  }
2145
2803
  // Set a timeout to revert to the original border after 2 seconds
2146
2804
  setTimeout(function () {
2147
- element.style.border = originalBorder;
2805
+ element.style.outline = originalOutline;
2148
2806
  }, 2000);
2149
2807
  }
2150
2808
  return;
@@ -2152,6 +2810,7 @@ class StableBrowser {
2152
2810
  .then(() => { })
2153
2811
  .catch((e) => {
2154
2812
  // ignore
2813
+ // console.error(`Could not highlight css: ${e}`);
2155
2814
  });
2156
2815
  }
2157
2816
  }
@@ -2159,8 +2818,49 @@ class StableBrowser {
2159
2818
  console.debug(error);
2160
2819
  }
2161
2820
  }
2821
+ _matcher(text) {
2822
+ if (!text) {
2823
+ return { matcher: "contains", queryText: "" };
2824
+ }
2825
+ if (text.length < 2) {
2826
+ return { matcher: "contains", queryText: text };
2827
+ }
2828
+ const split = text.split(":");
2829
+ const matcher = split[0].toLowerCase();
2830
+ const queryText = split.slice(1).join(":").trim();
2831
+ return { matcher, queryText };
2832
+ }
2833
+ _getDomain(url) {
2834
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2835
+ return "";
2836
+ }
2837
+ let hostnameFragments = url.split("/")[2].split(".");
2838
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2839
+ return hostnameFragments.join("-").split(":").join("-");
2840
+ }
2841
+ let n = hostnameFragments.length;
2842
+ let fragments = [...hostnameFragments];
2843
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2844
+ hostnameFragments.pop();
2845
+ n = hostnameFragments.length;
2846
+ }
2847
+ if (n == 0) {
2848
+ if (fragments[0] === "www")
2849
+ fragments = fragments.slice(1);
2850
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2851
+ }
2852
+ if (hostnameFragments[0] === "www")
2853
+ hostnameFragments = hostnameFragments.slice(1);
2854
+ return hostnameFragments.join(".");
2855
+ }
2856
+ /**
2857
+ * Verify the page path matches the given path.
2858
+ * @param {string} pathPart - The path to verify.
2859
+ * @param {object} options - Options for verification.
2860
+ * @param {object} world - The world context.
2861
+ * @returns {Promise<object>} - The state info after verification.
2862
+ */
2162
2863
  async verifyPagePath(pathPart, options = {}, world = null) {
2163
- const startTime = Date.now();
2164
2864
  let error = null;
2165
2865
  let screenshotId = null;
2166
2866
  let screenshotPath = null;
@@ -2174,159 +2874,520 @@ class StableBrowser {
2174
2874
  pathPart = newValue;
2175
2875
  }
2176
2876
  info.pathPart = pathPart;
2877
+ const { matcher, queryText } = this._matcher(pathPart);
2878
+ const state = {
2879
+ text_search: queryText,
2880
+ options,
2881
+ world,
2882
+ locate: false,
2883
+ scroll: false,
2884
+ highlight: false,
2885
+ type: Types.VERIFY_PAGE_PATH,
2886
+ text: `Verify the page url is ${queryText}`,
2887
+ _text: `Verify the page url is ${queryText}`,
2888
+ operation: "verifyPagePath",
2889
+ log: "***** verify page url is " + queryText + " *****\n",
2890
+ };
2177
2891
  try {
2892
+ await _preCommand(state, this);
2893
+ state.info.text = queryText;
2178
2894
  for (let i = 0; i < 30; i++) {
2179
2895
  const url = await this.page.url();
2180
- if (!url.includes(pathPart)) {
2181
- if (i === 29) {
2182
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2896
+ switch (matcher) {
2897
+ case "exact":
2898
+ if (url !== queryText) {
2899
+ if (i === 29) {
2900
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2901
+ }
2902
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2903
+ continue;
2904
+ }
2905
+ break;
2906
+ case "contains":
2907
+ if (!url.includes(queryText)) {
2908
+ if (i === 29) {
2909
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2910
+ }
2911
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2912
+ continue;
2913
+ }
2914
+ break;
2915
+ case "starts-with":
2916
+ {
2917
+ const domain = this._getDomain(url);
2918
+ if (domain.length > 0 && domain !== queryText) {
2919
+ if (i === 29) {
2920
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2921
+ }
2922
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2923
+ continue;
2924
+ }
2925
+ }
2926
+ break;
2927
+ case "ends-with":
2928
+ {
2929
+ const urlObj = new URL(url);
2930
+ let route = "/";
2931
+ if (urlObj.pathname !== "/") {
2932
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2933
+ }
2934
+ else {
2935
+ route = "/";
2936
+ }
2937
+ if (route !== queryText) {
2938
+ if (i === 29) {
2939
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2940
+ }
2941
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2942
+ continue;
2943
+ }
2944
+ }
2945
+ break;
2946
+ case "regex":
2947
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2948
+ if (!regex.test(url)) {
2949
+ if (i === 29) {
2950
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2951
+ }
2952
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2953
+ continue;
2954
+ }
2955
+ break;
2956
+ default:
2957
+ console.log("Unknown matching type, defaulting to contains matching");
2958
+ if (!url.includes(pathPart)) {
2959
+ if (i === 29) {
2960
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2961
+ }
2962
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2963
+ continue;
2964
+ }
2965
+ }
2966
+ await _screenshot(state, this);
2967
+ return state.info;
2968
+ }
2969
+ }
2970
+ catch (e) {
2971
+ state.info.failCause.lastError = e.message;
2972
+ state.info.failCause.assertionFailed = true;
2973
+ await _commandError(state, e, this);
2974
+ }
2975
+ finally {
2976
+ await _commandFinally(state, this);
2977
+ }
2978
+ }
2979
+ /**
2980
+ * Verify the page title matches the given title.
2981
+ * @param {string} title - The title to verify.
2982
+ * @param {object} options - Options for verification.
2983
+ * @param {object} world - The world context.
2984
+ * @returns {Promise<object>} - The state info after verification.
2985
+ */
2986
+ async verifyPageTitle(title, options = {}, world = null) {
2987
+ let error = null;
2988
+ let screenshotId = null;
2989
+ let screenshotPath = null;
2990
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2991
+ const newValue = await this._replaceWithLocalData(title, world);
2992
+ if (newValue !== title) {
2993
+ this.logger.info(title + "=" + newValue);
2994
+ title = newValue;
2995
+ }
2996
+ const { matcher, queryText } = this._matcher(title);
2997
+ const state = {
2998
+ text_search: queryText,
2999
+ options,
3000
+ world,
3001
+ locate: false,
3002
+ scroll: false,
3003
+ highlight: false,
3004
+ type: Types.VERIFY_PAGE_TITLE,
3005
+ text: `Verify the page title is ${queryText}`,
3006
+ _text: `Verify the page title is ${queryText}`,
3007
+ operation: "verifyPageTitle",
3008
+ log: "***** verify page title is " + queryText + " *****\n",
3009
+ };
3010
+ try {
3011
+ await _preCommand(state, this);
3012
+ state.info.text = queryText;
3013
+ for (let i = 0; i < 30; i++) {
3014
+ const foundTitle = await this.page.title();
3015
+ switch (matcher) {
3016
+ case "exact":
3017
+ if (foundTitle !== queryText) {
3018
+ if (i === 29) {
3019
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
3020
+ }
3021
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3022
+ continue;
3023
+ }
3024
+ break;
3025
+ case "contains":
3026
+ if (!foundTitle.includes(queryText)) {
3027
+ if (i === 29) {
3028
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
3029
+ }
3030
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3031
+ continue;
3032
+ }
3033
+ break;
3034
+ case "starts-with":
3035
+ if (!foundTitle.startsWith(queryText)) {
3036
+ if (i === 29) {
3037
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
3038
+ }
3039
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3040
+ continue;
3041
+ }
3042
+ break;
3043
+ case "ends-with":
3044
+ if (!foundTitle.endsWith(queryText)) {
3045
+ if (i === 29) {
3046
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
3047
+ }
3048
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3049
+ continue;
3050
+ }
3051
+ break;
3052
+ case "regex":
3053
+ const regex = new RegExp(queryText.slice(1, -1), "g");
3054
+ if (!regex.test(foundTitle)) {
3055
+ if (i === 29) {
3056
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
3057
+ }
3058
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3059
+ continue;
3060
+ }
3061
+ break;
3062
+ default:
3063
+ console.log("Unknown matching type, defaulting to contains matching");
3064
+ if (!foundTitle.includes(title)) {
3065
+ if (i === 29) {
3066
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
3067
+ }
3068
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3069
+ continue;
3070
+ }
3071
+ }
3072
+ await _screenshot(state, this);
3073
+ return state.info;
3074
+ }
3075
+ }
3076
+ catch (e) {
3077
+ state.info.failCause.lastError = e.message;
3078
+ state.info.failCause.assertionFailed = true;
3079
+ await _commandError(state, e, this);
3080
+ }
3081
+ finally {
3082
+ await _commandFinally(state, this);
3083
+ }
3084
+ }
3085
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
3086
+ const frames = this.page.frames();
3087
+ let results = [];
3088
+ // let ignoreCase = false;
3089
+ for (let i = 0; i < frames.length; i++) {
3090
+ if (dateAlternatives.date) {
3091
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
3092
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3093
+ result.frame = frames[i];
3094
+ results.push(result);
3095
+ }
3096
+ }
3097
+ else if (numberAlternatives.number) {
3098
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
3099
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
3100
+ result.frame = frames[i];
3101
+ results.push(result);
3102
+ }
3103
+ }
3104
+ else {
3105
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
3106
+ result.frame = frames[i];
3107
+ results.push(result);
3108
+ }
3109
+ }
3110
+ state.info.results = results;
3111
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3112
+ return resultWithElementsFound;
3113
+ }
3114
+ async verifyTextExistInPage(text, options = {}, world = null) {
3115
+ text = unEscapeString(text);
3116
+ const state = {
3117
+ text_search: text,
3118
+ options,
3119
+ world,
3120
+ locate: false,
3121
+ scroll: false,
3122
+ highlight: false,
3123
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
3124
+ text: `Verify the text '${maskValue(text)}' exists in page`,
3125
+ _text: `Verify the text '${text}' exists in page`,
3126
+ operation: "verifyTextExistInPage",
3127
+ log: "***** verify text " + text + " exists in page *****\n",
3128
+ };
3129
+ if (testForRegex(text)) {
3130
+ text = text.replace(/\\"/g, '"');
3131
+ }
3132
+ const timeout = this._getFindElementTimeout(options);
3133
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3134
+ const newValue = await this._replaceWithLocalData(text, world);
3135
+ if (newValue !== text) {
3136
+ this.logger.info(text + "=" + newValue);
3137
+ text = newValue;
3138
+ }
3139
+ let dateAlternatives = findDateAlternatives(text);
3140
+ let numberAlternatives = findNumberAlternatives(text);
3141
+ try {
3142
+ await _preCommand(state, this);
3143
+ state.info.text = text;
3144
+ while (true) {
3145
+ let resultWithElementsFound = {
3146
+ length: 0,
3147
+ };
3148
+ try {
3149
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3150
+ }
3151
+ catch (error) {
3152
+ // ignore
3153
+ }
3154
+ if (resultWithElementsFound.length === 0) {
3155
+ if (Date.now() - state.startTime > timeout) {
3156
+ throw new Error(`Text ${text} not found in page`);
2183
3157
  }
2184
3158
  await new Promise((resolve) => setTimeout(resolve, 1000));
2185
3159
  continue;
2186
3160
  }
2187
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2188
- return info;
3161
+ try {
3162
+ if (resultWithElementsFound[0].randomToken) {
3163
+ const frame = resultWithElementsFound[0].frame;
3164
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
3165
+ await this._highlightElements(frame, dataAttribute);
3166
+ const element = await frame.locator(dataAttribute).first();
3167
+ if (element) {
3168
+ await this.scrollIfNeeded(element, state.info);
3169
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3170
+ }
3171
+ }
3172
+ await _screenshot(state, this);
3173
+ return state.info;
3174
+ }
3175
+ catch (error) {
3176
+ console.error(error);
3177
+ }
2189
3178
  }
2190
3179
  }
2191
3180
  catch (e) {
2192
- //await this.closeUnexpectedPopups();
2193
- this.logger.error("verify page path failed " + info.log);
2194
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2195
- info.screenshotPath = screenshotPath;
2196
- Object.assign(e, { info: info });
2197
- error = e;
2198
- throw e;
3181
+ await _commandError(state, e, this);
2199
3182
  }
2200
3183
  finally {
2201
- const endTime = Date.now();
2202
- this._reportToWorld(world, {
2203
- type: Types.VERIFY_PAGE_PATH,
2204
- text: "Verify page path",
2205
- screenshotId,
2206
- result: error
2207
- ? {
2208
- status: "FAILED",
2209
- startTime,
2210
- endTime,
2211
- message: error === null || error === void 0 ? void 0 : error.message,
2212
- }
2213
- : {
2214
- status: "PASSED",
2215
- startTime,
2216
- endTime,
2217
- },
2218
- info: info,
2219
- });
3184
+ await _commandFinally(state, this);
2220
3185
  }
2221
3186
  }
2222
- async verifyTextExistInPage(text, options = {}, world = null) {
3187
+ async waitForTextToDisappear(text, options = {}, world = null) {
2223
3188
  text = unEscapeString(text);
2224
- const startTime = Date.now();
2225
- const timeout = this._getLoadTimeout(options);
2226
- let error = null;
2227
- let screenshotId = null;
2228
- let screenshotPath = null;
3189
+ const state = {
3190
+ text_search: text,
3191
+ options,
3192
+ world,
3193
+ locate: false,
3194
+ scroll: false,
3195
+ highlight: false,
3196
+ type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
3197
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
3198
+ _text: `Verify the text '${text}' does not exist in page`,
3199
+ operation: "verifyTextNotExistInPage",
3200
+ log: "***** verify text " + text + " does not exist in page *****\n",
3201
+ };
3202
+ if (testForRegex(text)) {
3203
+ text = text.replace(/\\"/g, '"');
3204
+ }
3205
+ const timeout = this._getFindElementTimeout(options);
2229
3206
  await new Promise((resolve) => setTimeout(resolve, 2000));
2230
- const info = {};
2231
- info.log = "***** verify text " + text + " exists in page *****\n";
2232
- info.operation = "verifyTextExistInPage";
2233
3207
  const newValue = await this._replaceWithLocalData(text, world);
2234
3208
  if (newValue !== text) {
2235
3209
  this.logger.info(text + "=" + newValue);
2236
3210
  text = newValue;
2237
3211
  }
2238
- info.text = text;
2239
3212
  let dateAlternatives = findDateAlternatives(text);
2240
3213
  let numberAlternatives = findNumberAlternatives(text);
2241
3214
  try {
3215
+ await _preCommand(state, this);
3216
+ state.info.text = text;
3217
+ let resultWithElementsFound = {
3218
+ length: null, // initial cannot be 0
3219
+ };
2242
3220
  while (true) {
2243
- const frames = this.page.frames();
2244
- let results = [];
2245
- for (let i = 0; i < frames.length; i++) {
2246
- if (dateAlternatives.date) {
2247
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2248
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
2249
- result.frame = frames[i];
2250
- results.push(result);
2251
- }
2252
- }
2253
- else if (numberAlternatives.number) {
2254
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2255
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
2256
- result.frame = frames[i];
2257
- results.push(result);
2258
- }
2259
- }
2260
- else {
2261
- const result = await this._locateElementByText(frames[i], text, "*", true, {});
2262
- result.frame = frames[i];
2263
- results.push(result);
2264
- }
3221
+ try {
3222
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3223
+ }
3224
+ catch (error) {
3225
+ // ignore
2265
3226
  }
2266
- info.results = results;
2267
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2268
3227
  if (resultWithElementsFound.length === 0) {
2269
- if (Date.now() - startTime > timeout) {
2270
- throw new Error(`Text ${text} not found in page`);
3228
+ await _screenshot(state, this);
3229
+ return state.info;
3230
+ }
3231
+ if (Date.now() - state.startTime > timeout) {
3232
+ throw new Error(`Text ${text} found in page`);
3233
+ }
3234
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3235
+ }
3236
+ }
3237
+ catch (e) {
3238
+ await _commandError(state, e, this);
3239
+ }
3240
+ finally {
3241
+ await _commandFinally(state, this);
3242
+ }
3243
+ }
3244
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
3245
+ textAnchor = unEscapeString(textAnchor);
3246
+ textToVerify = unEscapeString(textToVerify);
3247
+ const state = {
3248
+ text_search: textToVerify,
3249
+ options,
3250
+ world,
3251
+ locate: false,
3252
+ scroll: false,
3253
+ highlight: false,
3254
+ type: Types.VERIFY_TEXT_WITH_RELATION,
3255
+ text: `Verify text with relation to another text`,
3256
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
3257
+ operation: "verify_text_with_relation",
3258
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3259
+ };
3260
+ const cmdStartTime = Date.now();
3261
+ let cmdEndTime = null;
3262
+ const timeout = this._getFindElementTimeout(options);
3263
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3264
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
3265
+ if (newValue !== textAnchor) {
3266
+ this.logger.info(textAnchor + "=" + newValue);
3267
+ textAnchor = newValue;
3268
+ }
3269
+ newValue = await this._replaceWithLocalData(textToVerify, world);
3270
+ if (newValue !== textToVerify) {
3271
+ this.logger.info(textToVerify + "=" + newValue);
3272
+ textToVerify = newValue;
3273
+ }
3274
+ let dateAlternatives = findDateAlternatives(textToVerify);
3275
+ let numberAlternatives = findNumberAlternatives(textToVerify);
3276
+ let foundAncore = false;
3277
+ try {
3278
+ await _preCommand(state, this);
3279
+ state.info.text = textToVerify;
3280
+ let resultWithElementsFound = {
3281
+ length: 0,
3282
+ };
3283
+ while (true) {
3284
+ try {
3285
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
3286
+ }
3287
+ catch (error) {
3288
+ // ignore
3289
+ }
3290
+ if (resultWithElementsFound.length === 0) {
3291
+ if (Date.now() - state.startTime > timeout) {
3292
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2271
3293
  }
2272
3294
  await new Promise((resolve) => setTimeout(resolve, 1000));
2273
3295
  continue;
2274
3296
  }
2275
- if (resultWithElementsFound[0].randomToken) {
2276
- const frame = resultWithElementsFound[0].frame;
2277
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2278
- await this._highlightElements(frame, dataAttribute);
2279
- const element = await frame.$(dataAttribute);
2280
- if (element) {
2281
- await this.scrollIfNeeded(element, info);
2282
- await element.dispatchEvent("bvt_verify_page_contains_text");
3297
+ else {
3298
+ cmdEndTime = Date.now();
3299
+ if (cmdEndTime - cmdStartTime > 55000) {
3300
+ if (foundAncore) {
3301
+ throw new Error(`Text ${textToVerify} not found in page`);
3302
+ }
3303
+ else {
3304
+ throw new Error(`Text ${textAnchor} not found in page`);
3305
+ }
3306
+ }
3307
+ }
3308
+ try {
3309
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
3310
+ foundAncore = true;
3311
+ const result = resultWithElementsFound[i];
3312
+ const token = result.randomToken;
3313
+ const frame = result.frame;
3314
+ let css = `[data-blinq-id-${token}]`;
3315
+ const climbArray1 = [];
3316
+ for (let i = 0; i < climb; i++) {
3317
+ climbArray1.push("..");
3318
+ }
3319
+ let climbXpath = "xpath=" + climbArray1.join("/");
3320
+ css = css + " >> " + climbXpath;
3321
+ const count = await frame.locator(css).count();
3322
+ for (let j = 0; j < count; j++) {
3323
+ const continer = await frame.locator(css).nth(j);
3324
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
3325
+ if (result.elementCount > 0) {
3326
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
3327
+ await this._highlightElements(frame, dataAttribute);
3328
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
3329
+ // if (world && world.screenshot && !world.screenshotPath) {
3330
+ // console.log(`Highlighting for vtrt while running from recorder`);
3331
+ // this._highlightElements(frame, dataAttribute)
3332
+ // .then(async () => {
3333
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
3334
+ // this._unhighlightElements(frame, dataAttribute).then(
3335
+ // () => {}
3336
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
3337
+ // );
3338
+ // })
3339
+ // .catch(e);
3340
+ // }
3341
+ //await this._highlightElements(frame, cssAnchor);
3342
+ const element = await frame.locator(dataAttribute).first();
3343
+ // await new Promise((resolve) => setTimeout(resolve, 100));
3344
+ // await this._unhighlightElements(frame, dataAttribute);
3345
+ if (element) {
3346
+ await this.scrollIfNeeded(element, state.info);
3347
+ await element.dispatchEvent("bvt_verify_page_contains_text");
3348
+ }
3349
+ await _screenshot(state, this);
3350
+ return state.info;
3351
+ }
3352
+ }
2283
3353
  }
2284
3354
  }
2285
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2286
- return info;
3355
+ catch (error) {
3356
+ console.error(error);
3357
+ }
2287
3358
  }
2288
3359
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2289
3360
  }
2290
3361
  catch (e) {
2291
- //await this.closeUnexpectedPopups();
2292
- this.logger.error("verify text exist in page failed " + info.log);
2293
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2294
- info.screenshotPath = screenshotPath;
2295
- Object.assign(e, { info: info });
2296
- error = e;
2297
- throw e;
3362
+ await _commandError(state, e, this);
2298
3363
  }
2299
3364
  finally {
2300
- const endTime = Date.now();
2301
- this._reportToWorld(world, {
2302
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2303
- text: "Verify text exists in page",
2304
- screenshotId,
2305
- result: error
2306
- ? {
2307
- status: "FAILED",
2308
- startTime,
2309
- endTime,
2310
- message: error === null || error === void 0 ? void 0 : error.message,
2311
- }
2312
- : {
2313
- status: "PASSED",
2314
- startTime,
2315
- endTime,
2316
- },
2317
- info: info,
2318
- });
3365
+ await _commandFinally(state, this);
2319
3366
  }
2320
3367
  }
2321
- _getServerUrl() {
2322
- let serviceUrl = "https://api.blinq.io";
2323
- if (process.env.NODE_ENV_BLINQ === "dev") {
2324
- serviceUrl = "https://dev.api.blinq.io";
2325
- }
2326
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2327
- serviceUrl = "https://stage.api.blinq.io";
3368
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
3369
+ const frames = this.page.frames();
3370
+ let results = [];
3371
+ let ignoreCase = false;
3372
+ for (let i = 0; i < frames.length; i++) {
3373
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
3374
+ result.frame = frames[i];
3375
+ const climbArray = [];
3376
+ for (let i = 0; i < climb; i++) {
3377
+ climbArray.push("..");
3378
+ }
3379
+ let climbXpath = "xpath=" + climbArray.join("/");
3380
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
3381
+ const count = await frames[i].locator(newLocator).count();
3382
+ if (count > 0) {
3383
+ result.elementCount = count;
3384
+ result.locator = newLocator;
3385
+ results.push(result);
3386
+ }
2328
3387
  }
2329
- return serviceUrl;
3388
+ // state.info.results = results;
3389
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3390
+ return resultWithElementsFound;
2330
3391
  }
2331
3392
  async visualVerification(text, options = {}, world = null) {
2332
3393
  const startTime = Date.now();
@@ -2342,14 +3403,17 @@ class StableBrowser {
2342
3403
  throw new Error("TOKEN is not set");
2343
3404
  }
2344
3405
  try {
2345
- let serviceUrl = this._getServerUrl();
3406
+ let serviceUrl = _getServerUrl();
2346
3407
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2347
3408
  info.screenshotPath = screenshotPath;
2348
3409
  const screenshot = await this.takeScreenshot();
2349
- const request = {
2350
- method: "POST",
3410
+ let request = {
3411
+ method: "post",
3412
+ maxBodyLength: Infinity,
2351
3413
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2352
3414
  headers: {
3415
+ "x-bvt-project-id": path.basename(this.project_path),
3416
+ "x-source": "aaa",
2353
3417
  "Content-Type": "application/json",
2354
3418
  Authorization: `Bearer ${process.env.TOKEN}`,
2355
3419
  },
@@ -2358,7 +3422,7 @@ class StableBrowser {
2358
3422
  screenshot: screenshot,
2359
3423
  }),
2360
3424
  };
2361
- let result = await this.context.api.request(request);
3425
+ const result = await axios.request(request);
2362
3426
  if (result.data.status !== true) {
2363
3427
  throw new Error("Visual validation failed");
2364
3428
  }
@@ -2378,20 +3442,22 @@ class StableBrowser {
2378
3442
  info.screenshotPath = screenshotPath;
2379
3443
  Object.assign(e, { info: info });
2380
3444
  error = e;
2381
- throw e;
3445
+ // throw e;
3446
+ await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
2382
3447
  }
2383
3448
  finally {
2384
3449
  const endTime = Date.now();
2385
- this._reportToWorld(world, {
3450
+ _reportToWorld(world, {
2386
3451
  type: Types.VERIFY_VISUAL,
2387
3452
  text: "Visual verification",
3453
+ _text: "Visual verification of " + text,
2388
3454
  screenshotId,
2389
3455
  result: error
2390
3456
  ? {
2391
3457
  status: "FAILED",
2392
3458
  startTime,
2393
3459
  endTime,
2394
- message: error === null || error === void 0 ? void 0 : error.message,
3460
+ message: error?.message,
2395
3461
  }
2396
3462
  : {
2397
3463
  status: "PASSED",
@@ -2423,13 +3489,14 @@ class StableBrowser {
2423
3489
  this.logger.info("Table data verified");
2424
3490
  }
2425
3491
  async getTableData(selectors, _params = null, options = {}, world = null) {
2426
- this._validateSelectors(selectors);
3492
+ _validateSelectors(selectors);
2427
3493
  const startTime = Date.now();
2428
3494
  let error = null;
2429
3495
  let screenshotId = null;
2430
3496
  let screenshotPath = null;
2431
3497
  const info = {};
2432
3498
  info.log = "";
3499
+ info.locatorLog = new LocatorLog(selectors);
2433
3500
  info.operation = "getTableData";
2434
3501
  info.selectors = selectors;
2435
3502
  try {
@@ -2445,11 +3512,12 @@ class StableBrowser {
2445
3512
  info.screenshotPath = screenshotPath;
2446
3513
  Object.assign(e, { info: info });
2447
3514
  error = e;
2448
- throw e;
3515
+ // throw e;
3516
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2449
3517
  }
2450
3518
  finally {
2451
3519
  const endTime = Date.now();
2452
- this._reportToWorld(world, {
3520
+ _reportToWorld(world, {
2453
3521
  element_name: selectors.element_name,
2454
3522
  type: Types.GET_TABLE_DATA,
2455
3523
  text: "Get table data",
@@ -2459,7 +3527,7 @@ class StableBrowser {
2459
3527
  status: "FAILED",
2460
3528
  startTime,
2461
3529
  endTime,
2462
- message: error === null || error === void 0 ? void 0 : error.message,
3530
+ message: error?.message,
2463
3531
  }
2464
3532
  : {
2465
3533
  status: "PASSED",
@@ -2471,7 +3539,7 @@ class StableBrowser {
2471
3539
  }
2472
3540
  }
2473
3541
  async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
2474
- this._validateSelectors(selectors);
3542
+ _validateSelectors(selectors);
2475
3543
  if (!query) {
2476
3544
  throw new Error("query is null");
2477
3545
  }
@@ -2504,7 +3572,7 @@ class StableBrowser {
2504
3572
  info.operation = "analyzeTable";
2505
3573
  info.selectors = selectors;
2506
3574
  info.query = query;
2507
- query = this._fixUsingParams(query, _params);
3575
+ query = _fixUsingParams(query, _params);
2508
3576
  info.query_fixed = query;
2509
3577
  info.operator = operator;
2510
3578
  info.value = value;
@@ -2610,11 +3678,12 @@ class StableBrowser {
2610
3678
  info.screenshotPath = screenshotPath;
2611
3679
  Object.assign(e, { info: info });
2612
3680
  error = e;
2613
- throw e;
3681
+ // throw e;
3682
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2614
3683
  }
2615
3684
  finally {
2616
3685
  const endTime = Date.now();
2617
- this._reportToWorld(world, {
3686
+ _reportToWorld(world, {
2618
3687
  element_name: selectors.element_name,
2619
3688
  type: Types.ANALYZE_TABLE,
2620
3689
  text: "Analyze table",
@@ -2624,7 +3693,7 @@ class StableBrowser {
2624
3693
  status: "FAILED",
2625
3694
  startTime,
2626
3695
  endTime,
2627
- message: error === null || error === void 0 ? void 0 : error.message,
3696
+ message: error?.message,
2628
3697
  }
2629
3698
  : {
2630
3699
  status: "PASSED",
@@ -2635,28 +3704,51 @@ class StableBrowser {
2635
3704
  });
2636
3705
  }
2637
3706
  }
2638
- async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2639
- if (!value) {
2640
- return value;
2641
- }
2642
- // find all the accurance of {{(.*?)}} and replace with the value
2643
- let regex = /{{(.*?)}}/g;
2644
- let matches = value.match(regex);
2645
- if (matches) {
2646
- const testData = this.getTestData(world);
2647
- for (let i = 0; i < matches.length; i++) {
2648
- let match = matches[i];
2649
- let key = match.substring(2, match.length - 2);
2650
- let newValue = objectPath.get(testData, key, null);
2651
- if (newValue !== null) {
2652
- value = value.replace(match, newValue);
2653
- }
3707
+ /**
3708
+ * Explicit wait/sleep function that pauses execution for a specified duration
3709
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3710
+ * @param options - Optional configuration object
3711
+ * @param world - Optional world context
3712
+ * @returns Promise that resolves after the specified duration
3713
+ */
3714
+ async sleep(duration = 1000, options = {}, world = null) {
3715
+ const state = {
3716
+ duration,
3717
+ options,
3718
+ world,
3719
+ locate: false,
3720
+ scroll: false,
3721
+ screenshot: false,
3722
+ highlight: false,
3723
+ type: Types.SLEEP,
3724
+ text: `Sleep for ${duration} ms`,
3725
+ _text: `Sleep for ${duration} ms`,
3726
+ operation: "sleep",
3727
+ log: `***** Sleep for ${duration} ms *****\n`,
3728
+ };
3729
+ try {
3730
+ await _preCommand(state, this);
3731
+ if (duration < 0) {
3732
+ throw new Error("Sleep duration cannot be negative");
2654
3733
  }
3734
+ await new Promise((resolve) => setTimeout(resolve, duration));
3735
+ return state.info;
3736
+ }
3737
+ catch (e) {
3738
+ await _commandError(state, e, this);
2655
3739
  }
2656
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2657
- return await decrypt(value, null, totpWait);
3740
+ finally {
3741
+ await _commandFinally(state, this);
3742
+ }
3743
+ }
3744
+ async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
3745
+ try {
3746
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3747
+ }
3748
+ catch (error) {
3749
+ this.logger.debug(error);
3750
+ throw error;
2658
3751
  }
2659
- return value;
2660
3752
  }
2661
3753
  _getLoadTimeout(options) {
2662
3754
  let timeout = 15000;
@@ -2668,6 +3760,37 @@ class StableBrowser {
2668
3760
  }
2669
3761
  return timeout;
2670
3762
  }
3763
+ _getFindElementTimeout(options) {
3764
+ if (options && options.timeout) {
3765
+ return options.timeout;
3766
+ }
3767
+ if (this.configuration.find_element_timeout) {
3768
+ return this.configuration.find_element_timeout;
3769
+ }
3770
+ return 30000;
3771
+ }
3772
+ async saveStoreState(path = null, world = null) {
3773
+ const storageState = await this.page.context().storageState();
3774
+ path = await this._replaceWithLocalData(path, this.world);
3775
+ //const testDataFile = _getDataFile(world, this.context, this);
3776
+ if (path) {
3777
+ // save { storageState: storageState } into the path
3778
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3779
+ }
3780
+ else {
3781
+ await this.setTestData({ storageState: storageState }, world);
3782
+ }
3783
+ }
3784
+ async restoreSaveState(path = null, world = null) {
3785
+ path = await this._replaceWithLocalData(path, this.world);
3786
+ await refreshBrowser(this, path, world);
3787
+ this.registerEventListeners(this.context);
3788
+ registerNetworkEvents(this.world, this, this.context, this.page);
3789
+ registerDownloadEvent(this.page, this.world, this.context);
3790
+ if (this.onRestoreSaveState) {
3791
+ this.onRestoreSaveState(path);
3792
+ }
3793
+ }
2671
3794
  async waitForPageLoad(options = {}, world = null) {
2672
3795
  let timeout = this._getLoadTimeout(options);
2673
3796
  const promiseArray = [];
@@ -2701,13 +3824,12 @@ class StableBrowser {
2701
3824
  else if (e.label === "domcontentloaded") {
2702
3825
  console.log("waited for the domcontent loaded timeout");
2703
3826
  }
2704
- console.log(".");
2705
3827
  }
2706
3828
  finally {
2707
3829
  await new Promise((resolve) => setTimeout(resolve, 2000));
2708
3830
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2709
3831
  const endTime = Date.now();
2710
- this._reportToWorld(world, {
3832
+ _reportToWorld(world, {
2711
3833
  type: Types.GET_PAGE_STATUS,
2712
3834
  text: "Wait for page load",
2713
3835
  screenshotId,
@@ -2716,7 +3838,7 @@ class StableBrowser {
2716
3838
  status: "FAILED",
2717
3839
  startTime,
2718
3840
  endTime,
2719
- message: error === null || error === void 0 ? void 0 : error.message,
3841
+ message: error?.message,
2720
3842
  }
2721
3843
  : {
2722
3844
  status: "PASSED",
@@ -2727,41 +3849,122 @@ class StableBrowser {
2727
3849
  }
2728
3850
  }
2729
3851
  async closePage(options = {}, world = null) {
2730
- const startTime = Date.now();
2731
- let error = null;
2732
- let screenshotId = null;
2733
- let screenshotPath = null;
2734
- const info = {};
3852
+ const state = {
3853
+ options,
3854
+ world,
3855
+ locate: false,
3856
+ scroll: false,
3857
+ highlight: false,
3858
+ type: Types.CLOSE_PAGE,
3859
+ text: `Close page`,
3860
+ _text: `Close the page`,
3861
+ operation: "closePage",
3862
+ log: "***** close page *****\n",
3863
+ throwError: false,
3864
+ };
2735
3865
  try {
3866
+ await _preCommand(state, this);
2736
3867
  await this.page.close();
2737
3868
  }
2738
3869
  catch (e) {
2739
- console.log(".");
3870
+ await _commandError(state, e, this);
2740
3871
  }
2741
3872
  finally {
2742
- await new Promise((resolve) => setTimeout(resolve, 2000));
2743
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2744
- const endTime = Date.now();
2745
- this._reportToWorld(world, {
2746
- type: Types.CLOSE_PAGE,
2747
- text: "close page",
2748
- screenshotId,
2749
- result: error
2750
- ? {
2751
- status: "FAILED",
2752
- startTime,
2753
- endTime,
2754
- message: error === null || error === void 0 ? void 0 : error.message,
3873
+ await _commandFinally(state, this);
3874
+ }
3875
+ }
3876
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3877
+ let operation = null;
3878
+ if (!options || !options.operation) {
3879
+ throw new Error("operation is not defined");
3880
+ }
3881
+ operation = options.operation;
3882
+ // validate operation is one of the supported operations
3883
+ if (operation != "click" && operation != "hover+click") {
3884
+ throw new Error("operation is not supported");
3885
+ }
3886
+ const state = {
3887
+ options,
3888
+ world,
3889
+ locate: false,
3890
+ scroll: false,
3891
+ highlight: false,
3892
+ type: Types.TABLE_OPERATION,
3893
+ text: `Table operation`,
3894
+ _text: `Table ${operation} operation`,
3895
+ operation: operation,
3896
+ log: "***** Table operation *****\n",
3897
+ };
3898
+ const timeout = this._getFindElementTimeout(options);
3899
+ try {
3900
+ await _preCommand(state, this);
3901
+ const start = Date.now();
3902
+ let cellArea = null;
3903
+ while (true) {
3904
+ try {
3905
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3906
+ if (cellArea) {
3907
+ break;
2755
3908
  }
2756
- : {
2757
- status: "PASSED",
2758
- startTime,
2759
- endTime,
2760
- },
2761
- info: info,
2762
- });
3909
+ }
3910
+ catch (e) {
3911
+ // ignore
3912
+ }
3913
+ if (Date.now() - start > timeout) {
3914
+ throw new Error(`Cell not found in table`);
3915
+ }
3916
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3917
+ }
3918
+ switch (operation) {
3919
+ case "click":
3920
+ if (!options.css) {
3921
+ // will click in the center of the cell
3922
+ let xOffset = 0;
3923
+ let yOffset = 0;
3924
+ if (options.xOffset) {
3925
+ xOffset = options.xOffset;
3926
+ }
3927
+ if (options.yOffset) {
3928
+ yOffset = options.yOffset;
3929
+ }
3930
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3931
+ }
3932
+ else {
3933
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3934
+ if (results.length === 0) {
3935
+ throw new Error(`Element not found in cell area`);
3936
+ }
3937
+ state.element = results[0];
3938
+ await performAction("click", state.element, options, this, state, _params);
3939
+ }
3940
+ break;
3941
+ case "hover+click":
3942
+ if (!options.css) {
3943
+ throw new Error("css is not defined");
3944
+ }
3945
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3946
+ if (results.length === 0) {
3947
+ throw new Error(`Element not found in cell area`);
3948
+ }
3949
+ state.element = results[0];
3950
+ await performAction("hover+click", state.element, options, this, state, _params);
3951
+ break;
3952
+ default:
3953
+ throw new Error("operation is not supported");
3954
+ }
3955
+ }
3956
+ catch (e) {
3957
+ await _commandError(state, e, this);
3958
+ }
3959
+ finally {
3960
+ await _commandFinally(state, this);
2763
3961
  }
2764
3962
  }
3963
+ saveTestDataAsGlobal(options, world) {
3964
+ const dataFile = _getDataFile(world, this.context, this);
3965
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
3966
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
3967
+ }
2765
3968
  async setViewportSize(width, hight, options = {}, world = null) {
2766
3969
  const startTime = Date.now();
2767
3970
  let error = null;
@@ -2778,22 +3981,23 @@ class StableBrowser {
2778
3981
  await this.page.setViewportSize({ width: width, height: hight });
2779
3982
  }
2780
3983
  catch (e) {
2781
- console.log(".");
3984
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2782
3985
  }
2783
3986
  finally {
2784
3987
  await new Promise((resolve) => setTimeout(resolve, 2000));
2785
3988
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2786
3989
  const endTime = Date.now();
2787
- this._reportToWorld(world, {
3990
+ _reportToWorld(world, {
2788
3991
  type: Types.SET_VIEWPORT,
2789
3992
  text: "set viewport size to " + width + "x" + hight,
3993
+ _text: "Set the viewport size to " + width + "x" + hight,
2790
3994
  screenshotId,
2791
3995
  result: error
2792
3996
  ? {
2793
3997
  status: "FAILED",
2794
3998
  startTime,
2795
3999
  endTime,
2796
- message: error === null || error === void 0 ? void 0 : error.message,
4000
+ message: error?.message,
2797
4001
  }
2798
4002
  : {
2799
4003
  status: "PASSED",
@@ -2814,13 +4018,13 @@ class StableBrowser {
2814
4018
  await this.page.reload();
2815
4019
  }
2816
4020
  catch (e) {
2817
- console.log(".");
4021
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2818
4022
  }
2819
4023
  finally {
2820
4024
  await new Promise((resolve) => setTimeout(resolve, 2000));
2821
4025
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2822
4026
  const endTime = Date.now();
2823
- this._reportToWorld(world, {
4027
+ _reportToWorld(world, {
2824
4028
  type: Types.GET_PAGE_STATUS,
2825
4029
  text: "page relaod",
2826
4030
  screenshotId,
@@ -2829,7 +4033,7 @@ class StableBrowser {
2829
4033
  status: "FAILED",
2830
4034
  startTime,
2831
4035
  endTime,
2832
- message: error === null || error === void 0 ? void 0 : error.message,
4036
+ message: error?.message,
2833
4037
  }
2834
4038
  : {
2835
4039
  status: "PASSED",
@@ -2856,11 +4060,216 @@ class StableBrowser {
2856
4060
  console.log("#-#");
2857
4061
  }
2858
4062
  }
2859
- _reportToWorld(world, properties) {
2860
- if (!world || !world.attach) {
2861
- return;
4063
+ async beforeScenario(world, scenario) {
4064
+ if (world && world.attach) {
4065
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4066
+ }
4067
+ this.context.loadedRoutes = null;
4068
+ this.beforeScenarioCalled = true;
4069
+ if (scenario && scenario.pickle && scenario.pickle.name) {
4070
+ this.scenarioName = scenario.pickle.name;
4071
+ }
4072
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
4073
+ this.featureName = scenario.gherkinDocument.feature.name;
4074
+ }
4075
+ if (this.context) {
4076
+ this.context.examplesRow = extractStepExampleParameters(scenario);
4077
+ }
4078
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
4079
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
4080
+ // check if @global_test_data tag is present
4081
+ if (this.tags.includes("@global_test_data")) {
4082
+ this.saveTestDataAsGlobal({}, world);
4083
+ }
4084
+ }
4085
+ // update test data based on feature/scenario
4086
+ let envName = null;
4087
+ if (this.context && this.context.environment) {
4088
+ envName = this.context.environment.name;
4089
+ }
4090
+ if (!process.env.TEMP_RUN) {
4091
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
4092
+ }
4093
+ await loadBrunoParams(this.context, this.context.environment.name);
4094
+ }
4095
+ async afterScenario(world, scenario) { }
4096
+ async beforeStep(world, step) {
4097
+ if (!this.beforeScenarioCalled) {
4098
+ this.beforeScenario(world, step);
4099
+ this.context.loadedRoutes = null;
4100
+ }
4101
+ if (this.stepIndex === undefined) {
4102
+ this.stepIndex = 0;
4103
+ }
4104
+ else {
4105
+ this.stepIndex++;
4106
+ }
4107
+ if (step && step.pickleStep && step.pickleStep.text) {
4108
+ this.stepName = step.pickleStep.text;
4109
+ this.logger.info("step: " + this.stepName);
4110
+ }
4111
+ else if (step && step.text) {
4112
+ this.stepName = step.text;
4113
+ }
4114
+ else {
4115
+ this.stepName = "step " + this.stepIndex;
4116
+ }
4117
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4118
+ if (this.context.browserObject.context) {
4119
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
4120
+ }
4121
+ }
4122
+ if (this.initSnapshotTaken === false) {
4123
+ this.initSnapshotTaken = true;
4124
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4125
+ const snapshot = await this.getAriaSnapshot();
4126
+ if (snapshot) {
4127
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
4128
+ }
4129
+ }
4130
+ }
4131
+ this.context.routeResults = null;
4132
+ await registerBeforeStepRoutes(this.context, this.stepName);
4133
+ networkBeforeStep(this.stepName);
4134
+ }
4135
+ async getAriaSnapshot() {
4136
+ try {
4137
+ // find the page url
4138
+ const url = await this.page.url();
4139
+ // extract the path from the url
4140
+ const path = new URL(url).pathname;
4141
+ // get the page title
4142
+ const title = await this.page.title();
4143
+ // go over other frams
4144
+ const frames = this.page.frames();
4145
+ const snapshots = [];
4146
+ const content = [`- path: ${path}`, `- title: ${title}`];
4147
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
4148
+ for (let i = 0; i < frames.length; i++) {
4149
+ const frame = frames[i];
4150
+ try {
4151
+ // Ensure frame is attached and has body
4152
+ const body = frame.locator("body");
4153
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4154
+ const snapshot = await body.ariaSnapshot({ timeout });
4155
+ if (!snapshot) {
4156
+ continue;
4157
+ }
4158
+ content.push(`- frame: ${i}`);
4159
+ content.push(snapshot);
4160
+ }
4161
+ catch (innerErr) {
4162
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4163
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4164
+ }
4165
+ }
4166
+ return content.join("\n");
4167
+ }
4168
+ catch (e) {
4169
+ console.log("Error in getAriaSnapshot");
4170
+ //console.debug(e);
4171
+ }
4172
+ return null;
4173
+ }
4174
+ /**
4175
+ * Sends command with custom payload to report.
4176
+ * @param commandText - Title of the command to be shown in the report.
4177
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
4178
+ * @param content - Content of the command to be shown in the report.
4179
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
4180
+ * @param world - Optional world context.
4181
+ * @public
4182
+ */
4183
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
4184
+ const state = {
4185
+ options,
4186
+ world,
4187
+ locate: false,
4188
+ scroll: false,
4189
+ screenshot: options.screenshot ?? false,
4190
+ highlight: options.highlight ?? false,
4191
+ type: Types.REPORT_COMMAND,
4192
+ text: commandText,
4193
+ _text: commandText,
4194
+ operation: "report_command",
4195
+ log: "***** " + commandText + " *****\n",
4196
+ };
4197
+ try {
4198
+ await _preCommand(state, this);
4199
+ const payload = {
4200
+ type: options.type ?? "text",
4201
+ content: content,
4202
+ screenshotId: null,
4203
+ };
4204
+ state.payload = payload;
4205
+ if (commandStatus === "FAILED") {
4206
+ state.throwError = true;
4207
+ throw new Error("Command failed");
4208
+ }
4209
+ }
4210
+ catch (e) {
4211
+ await _commandError(state, e, this);
4212
+ }
4213
+ finally {
4214
+ await _commandFinally(state, this);
4215
+ }
4216
+ }
4217
+ async afterStep(world, step) {
4218
+ this.stepName = null;
4219
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4220
+ if (this.context.browserObject.context) {
4221
+ await this.context.browserObject.context.tracing.stopChunk({
4222
+ path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
4223
+ });
4224
+ if (world && world.attach) {
4225
+ await world.attach(JSON.stringify({
4226
+ type: "trace",
4227
+ traceFilePath: `trace-${this.stepIndex}.zip`,
4228
+ }), "application/json+trace");
4229
+ }
4230
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
4231
+ }
4232
+ }
4233
+ if (this.context) {
4234
+ this.context.examplesRow = null;
4235
+ }
4236
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4237
+ const snapshot = await this.getAriaSnapshot();
4238
+ if (snapshot) {
4239
+ const obj = {};
4240
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
4241
+ }
4242
+ }
4243
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4244
+ if (this.context.routeResults) {
4245
+ if (world && world.attach) {
4246
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4247
+ }
2862
4248
  }
2863
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
4249
+ if (!process.env.TEMP_RUN) {
4250
+ const state = {
4251
+ world,
4252
+ locate: false,
4253
+ scroll: false,
4254
+ screenshot: true,
4255
+ highlight: true,
4256
+ type: Types.STEP_COMPLETE,
4257
+ text: "end of scenario",
4258
+ _text: "end of scenario",
4259
+ operation: "step_complete",
4260
+ log: "***** " + "end of scenario" + " *****\n",
4261
+ };
4262
+ try {
4263
+ await _preCommand(state, this);
4264
+ }
4265
+ catch (e) {
4266
+ await _commandError(state, e, this);
4267
+ }
4268
+ finally {
4269
+ await _commandFinally(state, this);
4270
+ }
4271
+ }
4272
+ networkAfterStep(this.stepName);
2864
4273
  }
2865
4274
  }
2866
4275
  function createTimedPromise(promise, label) {
@@ -2868,156 +4277,5 @@ function createTimedPromise(promise, label) {
2868
4277
  .then((result) => ({ status: "fulfilled", label, result }))
2869
4278
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2870
4279
  }
2871
- const KEYBOARD_EVENTS = [
2872
- "ALT",
2873
- "AltGraph",
2874
- "CapsLock",
2875
- "Control",
2876
- "Fn",
2877
- "FnLock",
2878
- "Hyper",
2879
- "Meta",
2880
- "NumLock",
2881
- "ScrollLock",
2882
- "Shift",
2883
- "Super",
2884
- "Symbol",
2885
- "SymbolLock",
2886
- "Enter",
2887
- "Tab",
2888
- "ArrowDown",
2889
- "ArrowLeft",
2890
- "ArrowRight",
2891
- "ArrowUp",
2892
- "End",
2893
- "Home",
2894
- "PageDown",
2895
- "PageUp",
2896
- "Backspace",
2897
- "Clear",
2898
- "Copy",
2899
- "CrSel",
2900
- "Cut",
2901
- "Delete",
2902
- "EraseEof",
2903
- "ExSel",
2904
- "Insert",
2905
- "Paste",
2906
- "Redo",
2907
- "Undo",
2908
- "Accept",
2909
- "Again",
2910
- "Attn",
2911
- "Cancel",
2912
- "ContextMenu",
2913
- "Escape",
2914
- "Execute",
2915
- "Find",
2916
- "Finish",
2917
- "Help",
2918
- "Pause",
2919
- "Play",
2920
- "Props",
2921
- "Select",
2922
- "ZoomIn",
2923
- "ZoomOut",
2924
- "BrightnessDown",
2925
- "BrightnessUp",
2926
- "Eject",
2927
- "LogOff",
2928
- "Power",
2929
- "PowerOff",
2930
- "PrintScreen",
2931
- "Hibernate",
2932
- "Standby",
2933
- "WakeUp",
2934
- "AllCandidates",
2935
- "Alphanumeric",
2936
- "CodeInput",
2937
- "Compose",
2938
- "Convert",
2939
- "Dead",
2940
- "FinalMode",
2941
- "GroupFirst",
2942
- "GroupLast",
2943
- "GroupNext",
2944
- "GroupPrevious",
2945
- "ModeChange",
2946
- "NextCandidate",
2947
- "NonConvert",
2948
- "PreviousCandidate",
2949
- "Process",
2950
- "SingleCandidate",
2951
- "HangulMode",
2952
- "HanjaMode",
2953
- "JunjaMode",
2954
- "Eisu",
2955
- "Hankaku",
2956
- "Hiragana",
2957
- "HiraganaKatakana",
2958
- "KanaMode",
2959
- "KanjiMode",
2960
- "Katakana",
2961
- "Romaji",
2962
- "Zenkaku",
2963
- "ZenkakuHanaku",
2964
- "F1",
2965
- "F2",
2966
- "F3",
2967
- "F4",
2968
- "F5",
2969
- "F6",
2970
- "F7",
2971
- "F8",
2972
- "F9",
2973
- "F10",
2974
- "F11",
2975
- "F12",
2976
- "Soft1",
2977
- "Soft2",
2978
- "Soft3",
2979
- "Soft4",
2980
- "ChannelDown",
2981
- "ChannelUp",
2982
- "Close",
2983
- "MailForward",
2984
- "MailReply",
2985
- "MailSend",
2986
- "MediaFastForward",
2987
- "MediaPause",
2988
- "MediaPlay",
2989
- "MediaPlayPause",
2990
- "MediaRecord",
2991
- "MediaRewind",
2992
- "MediaStop",
2993
- "MediaTrackNext",
2994
- "MediaTrackPrevious",
2995
- "AudioBalanceLeft",
2996
- "AudioBalanceRight",
2997
- "AudioBassBoostDown",
2998
- "AudioBassBoostToggle",
2999
- "AudioBassBoostUp",
3000
- "AudioFaderFront",
3001
- "AudioFaderRear",
3002
- "AudioSurroundModeNext",
3003
- "AudioTrebleDown",
3004
- "AudioTrebleUp",
3005
- "AudioVolumeDown",
3006
- "AudioVolumeMute",
3007
- "AudioVolumeUp",
3008
- "MicrophoneToggle",
3009
- "MicrophoneVolumeDown",
3010
- "MicrophoneVolumeMute",
3011
- "MicrophoneVolumeUp",
3012
- "TV",
3013
- "TV3DMode",
3014
- "TVAntennaCable",
3015
- "TVAudioDescription",
3016
- ];
3017
- function unEscapeString(str) {
3018
- const placeholder = "__NEWLINE__";
3019
- str = str.replace(new RegExp(placeholder, "g"), "\n");
3020
- return str;
3021
- }
3022
4280
  export { StableBrowser };
3023
4281
  //# sourceMappingURL=stable_browser.js.map