automation_model 1.0.455-dev → 1.0.455

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 -26
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.d.ts +6 -3
  10. package/lib/browser_manager.js +175 -50
  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 +96 -14
  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 +411 -0
  50. package/lib/network.js.map +1 -0
  51. package/lib/route.d.ts +21 -0
  52. package/lib/route.js +392 -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 +2544 -1291
  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 +6 -0
  69. package/lib/test_context.js +15 -11
  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 +19 -10
@@ -10,26 +10,42 @@ 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,19 +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);
92
148
  if (!context.pageLoading) {
93
149
  context.pageLoading = { status: false };
94
150
  }
151
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
152
+ this.page.on("dialog", (dialog) => dialog.accept());
153
+ }
95
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
+ }
96
160
  context.pageLoading.status = true;
97
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
+ }
98
170
  context.page = page;
99
171
  context.pages.push(page);
172
+ registerNetworkEvents(this.world, this, context, this.page);
173
+ registerDownloadEvent(this.page, this.world, context);
100
174
  page.on("close", async () => {
101
175
  if (this.context && this.context.pages && this.context.pages.length > 1) {
102
176
  this.context.pages.pop();
@@ -128,7 +202,7 @@ class StableBrowser {
128
202
  }
129
203
  let newContextCreated = false;
130
204
  if (!apps[appName]) {
131
- let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, 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);
132
206
  newContextCreated = true;
133
207
  apps[appName] = {
134
208
  context: newContext,
@@ -137,38 +211,47 @@ class StableBrowser {
137
211
  };
138
212
  }
139
213
  const tempContext = {};
140
- this._copyContext(this, tempContext);
141
- this._copyContext(apps[appName], this);
214
+ _copyContext(this, tempContext);
215
+ _copyContext(apps[appName], this);
142
216
  apps[this.appName] = tempContext;
143
217
  this.appName = appName;
144
218
  if (newContextCreated) {
145
219
  this.registerEventListeners(this.context);
146
220
  await this.goto(this.context.environment.baseUrl);
147
- await this.waitForPageLoad();
221
+ if (!this.fastMode) {
222
+ await this.waitForPageLoad();
223
+ }
148
224
  }
149
225
  }
150
- _copyContext(from, to) {
151
- to.browser = from.browser;
152
- to.page = from.page;
153
- to.context = from.context;
154
- }
155
- getWebLogFile(logFolder) {
156
- if (!fs.existsSync(logFolder)) {
157
- 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
+ }
158
236
  }
159
- let nextIndex = 1;
160
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
161
- 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
+ }
162
247
  }
163
- const fileName = nextIndex + ".json";
164
- return path.join(logFolder, fileName);
248
+ throw new Error("Tab not found: " + tabTitleOrIndex);
165
249
  }
166
250
  registerConsoleLogListener(page, context) {
167
251
  if (!this.context.webLogger) {
168
252
  this.context.webLogger = [];
169
253
  }
170
254
  page.on("console", async (msg) => {
171
- var _a;
172
255
  const obj = {
173
256
  type: msg.type(),
174
257
  text: msg.text(),
@@ -176,7 +259,9 @@ class StableBrowser {
176
259
  time: new Date().toISOString(),
177
260
  };
178
261
  this.context.webLogger.push(obj);
179
- (_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
+ }
180
265
  });
181
266
  }
182
267
  registerRequestListener(page, context, logFile) {
@@ -184,7 +269,6 @@ class StableBrowser {
184
269
  this.context.networkLogger = [];
185
270
  }
186
271
  page.on("request", async (data) => {
187
- var _a;
188
272
  const startTime = new Date().getTime();
189
273
  try {
190
274
  const pageUrl = new URL(page.url());
@@ -209,10 +293,10 @@ class StableBrowser {
209
293
  startTime,
210
294
  };
211
295
  context.networkLogger.push(obj);
212
- (_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" });
213
297
  }
214
298
  catch (error) {
215
- console.error("Error in request listener", error);
299
+ // console.error("Error in request listener", error);
216
300
  context.networkLogger.push({
217
301
  error: "not able to listen",
218
302
  message: error.message,
@@ -226,69 +310,110 @@ class StableBrowser {
226
310
  // async closeUnexpectedPopups() {
227
311
  // await closeUnexpectedPopups(this.page);
228
312
  // }
229
- 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);
230
318
  if (!url.startsWith("http")) {
231
319
  url = "https://" + url;
232
320
  }
233
- await this.page.goto(url, {
234
- timeout: 60000,
235
- });
236
- }
237
- _validateSelectors(selectors) {
238
- if (!selectors) {
239
- throw new Error("selectors is null");
240
- }
241
- if (!selectors.locators) {
242
- 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);
243
340
  }
244
- if (!Array.isArray(selectors.locators)) {
245
- throw new Error("selectors.locators expected to be array");
341
+ catch (error) {
342
+ console.error("Error on goto", error);
343
+ _commandError(state, error, this);
246
344
  }
247
- if (selectors.locators.length === 0) {
248
- throw new Error("selectors.locators expected to be non empty array");
345
+ finally {
346
+ await _commandFinally(state, this);
249
347
  }
250
348
  }
251
- _fixUsingParams(text, _params) {
252
- if (!_params || typeof text !== "string") {
253
- 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);
254
369
  }
255
- for (let key in _params) {
256
- let regValue = key;
257
- if (key.startsWith("_")) {
258
- // remove the _ prefix
259
- regValue = key.substring(1);
260
- }
261
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
370
+ catch (error) {
371
+ console.error("Error on goBack", error);
372
+ _commandError(state, error, this);
262
373
  }
263
- return text;
264
- }
265
- _fixLocatorUsingParams(locator, _params) {
266
- // check if not null
267
- if (!locator) {
268
- return locator;
374
+ finally {
375
+ await _commandFinally(state, this);
269
376
  }
270
- // clone the locator
271
- locator = JSON.parse(JSON.stringify(locator));
272
- this.scanAndManipulate(locator, _params);
273
- return locator;
274
377
  }
275
- _isObject(value) {
276
- 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
+ }
277
406
  }
278
- scanAndManipulate(currentObj, _params) {
279
- for (const key in currentObj) {
280
- if (typeof currentObj[key] === "string") {
281
- // Perform string manipulation
282
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
283
- }
284
- else if (this._isObject(currentObj[key])) {
285
- // Recursively scan nested objects
286
- 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);
287
415
  }
288
416
  }
289
- }
290
- _getLocator(locator, scope, _params) {
291
- locator = this._fixLocatorUsingParams(locator, _params);
292
417
  let locatorReturn;
293
418
  if (locator.role) {
294
419
  if (locator.role[1].nameReg) {
@@ -296,7 +421,7 @@ class StableBrowser {
296
421
  delete locator.role[1].nameReg;
297
422
  }
298
423
  // if (locator.role[1].name) {
299
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
424
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
300
425
  // }
301
426
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
302
427
  }
@@ -315,7 +440,7 @@ class StableBrowser {
315
440
  locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
316
441
  }
317
442
  }
318
- if (locator === null || locator === void 0 ? void 0 : locator.engine) {
443
+ if (locator?.engine) {
319
444
  if (locator.engine === "css") {
320
445
  locatorReturn = scope.locator(locator.selector);
321
446
  }
@@ -336,192 +461,180 @@ class StableBrowser {
336
461
  return locatorReturn;
337
462
  }
338
463
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
339
- 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);
340
468
  if (result.elementCount === 0) {
341
469
  return;
342
470
  }
343
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
471
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
344
472
  // css climb to parent element
345
473
  const climbArray = [];
346
474
  for (let i = 0; i < climb; i++) {
347
475
  climbArray.push("..");
348
476
  }
349
477
  let climbXpath = "xpath=" + climbArray.join("/");
350
- return textElementCss + " >> " + climbXpath + " >> " + css;
351
- }
352
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
353
- //const stringifyText = JSON.stringify(text);
354
- return await scope.evaluate(([text, tag, regex, partial]) => {
355
- function isParent(parent, child) {
356
- let currentNode = child.parentNode;
357
- while (currentNode !== null) {
358
- if (currentNode === parent) {
359
- return true;
360
- }
361
- currentNode = currentNode.parentNode;
362
- }
363
- return false;
364
- }
365
- document.isParent = isParent;
366
- function collectAllShadowDomElements(element, result = []) {
367
- // Check and add the element if it has a shadow root
368
- if (element.shadowRoot) {
369
- result.push(element);
370
- // Also search within the shadow root
371
- document.collectAllShadowDomElements(element.shadowRoot, result);
372
- }
373
- // Iterate over child nodes
374
- element.childNodes.forEach((child) => {
375
- // Recursively call the function for each child node
376
- document.collectAllShadowDomElements(child, result);
377
- });
378
- return result;
379
- }
380
- document.collectAllShadowDomElements = collectAllShadowDomElements;
381
- if (!tag) {
382
- tag = "*";
383
- }
384
- let elements = Array.from(document.querySelectorAll(tag));
385
- let shadowHosts = [];
386
- document.collectAllShadowDomElements(document, shadowHosts);
387
- for (let i = 0; i < shadowHosts.length; i++) {
388
- let shadowElement = shadowHosts[i].shadowRoot;
389
- if (!shadowElement) {
390
- console.log("shadowElement is null, for host " + shadowHosts[i]);
391
- continue;
392
- }
393
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
394
- elements = elements.concat(shadowElements);
395
- }
396
- let randomToken = null;
397
- const foundElements = [];
398
- if (regex) {
399
- let regexpSearch = new RegExp(text, "im");
400
- for (let i = 0; i < elements.length; i++) {
401
- const element = elements[i];
402
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
403
- (element.value && regexpSearch.test(element.value))) {
404
- foundElements.push(element);
405
- }
406
- }
407
- }
408
- else {
409
- text = text.trim();
410
- for (let i = 0; i < elements.length; i++) {
411
- const element = elements[i];
412
- if (partial) {
413
- if ((element.innerText && element.innerText.trim().includes(text)) ||
414
- (element.value && element.value.includes(text))) {
415
- foundElements.push(element);
416
- }
417
- }
418
- else {
419
- if ((element.innerText && element.innerText.trim() === text) ||
420
- (element.value && element.value === text)) {
421
- foundElements.push(element);
422
- }
423
- }
424
- }
425
- }
426
- let noChildElements = [];
427
- for (let i = 0; i < foundElements.length; i++) {
428
- let element = foundElements[i];
429
- let hasChild = false;
430
- for (let j = 0; j < foundElements.length; j++) {
431
- if (i === j) {
432
- continue;
433
- }
434
- if (isParent(element, foundElements[j])) {
435
- hasChild = true;
436
- 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;
437
500
  }
438
501
  }
439
- if (!hasChild) {
440
- noChildElements.push(element);
441
- }
442
- }
443
- let elementCount = 0;
444
- if (noChildElements.length > 0) {
445
- for (let i = 0; i < noChildElements.length; i++) {
446
- if (randomToken === null) {
447
- randomToken = Math.random().toString(36).substring(7);
448
- }
449
- let element = noChildElements[i];
450
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
451
- elementCount++;
502
+ if (!el.setAttribute) {
503
+ el = el.parentElement;
452
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;
453
515
  }
454
- return { elementCount: elementCount, randomToken: randomToken };
455
- }, [text1, tag1, regex1, partial1]);
516
+ tagCount++;
517
+ }
518
+ return { elementCount: tagCount, randomToken };
456
519
  }
457
- 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
+ }
458
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);
459
539
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
460
540
  let locator = null;
461
541
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
462
- 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);
463
544
  if (!locatorString) {
545
+ info.failCause.textNotFound = true;
546
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
464
547
  return;
465
548
  }
466
- locator = this._getLocator({ css: locatorString }, scope, _params);
549
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
467
550
  }
468
551
  else if (locatorSearch.text) {
469
- 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);
470
554
  if (result.elementCount === 0) {
555
+ info.failCause.textNotFound = true;
556
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
471
557
  return;
472
558
  }
473
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
559
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
474
560
  if (locatorSearch.childCss) {
475
561
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
476
562
  }
477
- locator = this._getLocator(locatorSearch, scope, _params);
563
+ locator = await this._getLocator(locatorSearch, scope, _params);
478
564
  }
479
565
  else {
480
- locator = this._getLocator(locatorSearch, scope, _params);
566
+ locator = await this._getLocator(locatorSearch, scope, _params);
481
567
  }
482
568
  // let cssHref = false;
483
569
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
484
570
  // cssHref = true;
485
571
  // }
486
572
  let count = await locator.count();
573
+ if (count > 0 && !info.failCause.count) {
574
+ info.failCause.count = count;
575
+ }
487
576
  //info.log += "total elements found " + count + "\n";
488
577
  //let visibleCount = 0;
489
578
  let visibleLocator = null;
490
- if (locatorSearch.index && locatorSearch.index < count) {
579
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
491
580
  foundLocators.push(locator.nth(locatorSearch.index));
581
+ if (info.locatorLog) {
582
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
583
+ }
492
584
  return;
493
585
  }
586
+ if (info.locatorLog && count === 0 && logErrors) {
587
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
588
+ }
494
589
  for (let j = 0; j < count; j++) {
495
590
  let visible = await locator.nth(j).isVisible();
496
591
  const enabled = await locator.nth(j).isEnabled();
497
592
  if (!visibleOnly) {
498
593
  visible = true;
499
594
  }
500
- if (visible && enabled) {
595
+ if (visible && (allowDisabled || enabled)) {
501
596
  foundLocators.push(locator.nth(j));
597
+ if (info.locatorLog) {
598
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
599
+ }
502
600
  }
503
- else {
601
+ else if (logErrors) {
602
+ info.failCause.visible = visible;
603
+ info.failCause.enabled = enabled;
504
604
  if (!info.printMessages) {
505
605
  info.printMessages = {};
506
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
+ }
507
615
  if (!info.printMessages[j.toString()]) {
508
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
616
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
509
617
  info.printMessages[j.toString()] = true;
510
618
  }
511
619
  }
512
620
  }
513
621
  }
514
622
  async closeUnexpectedPopups(info, _params) {
623
+ if (!info) {
624
+ info = {};
625
+ info.failCause = {};
626
+ info.log = "";
627
+ }
515
628
  if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
516
629
  if (!info) {
517
630
  info = {};
518
631
  }
519
- info.log += "scan for popup handlers" + "\n";
632
+ //info.log += "scan for popup handlers" + "\n";
520
633
  const handlerGroup = [];
521
634
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
522
635
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
523
636
  }
524
- const scopes = [this.page, ...this.page.frames()];
637
+ const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
525
638
  let result = null;
526
639
  let scope = null;
527
640
  for (let i = 0; i < scopes.length; i++) {
@@ -543,58 +656,108 @@ class StableBrowser {
543
656
  }
544
657
  if (result.foundElements.length > 0) {
545
658
  let dialogCloseLocator = result.foundElements[0].locator;
546
- await dialogCloseLocator.click();
547
- // wait for the dialog to close
548
- 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
+ }
549
674
  return { rerun: true };
550
675
  }
551
676
  }
552
677
  }
553
678
  return { rerun: false };
554
679
  }
555
- async _locate(selectors, info, _params, timeout = 30000) {
680
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
681
+ if (!timeout) {
682
+ timeout = 30000;
683
+ }
556
684
  for (let i = 0; i < 3; i++) {
557
685
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
558
686
  for (let j = 0; j < selectors.locators.length; j++) {
559
687
  let selector = selectors.locators[j];
560
688
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
561
689
  }
562
- let element = await this._locate_internal(selectors, info, _params, timeout);
690
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
563
691
  if (!element.rerun) {
564
- 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);
565
714
  }
566
715
  }
567
716
  throw new Error("unable to locate element " + JSON.stringify(selectors));
568
717
  }
569
- async _locate_internal(selectors, info, _params, timeout = 30000) {
570
- let highPriorityTimeout = 5000;
571
- let visibleOnlyTimeout = 6000;
572
- let startTime = performance.now();
573
- let locatorsCount = 0;
574
- //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();
575
725
  let scope = this.page;
726
+ if (selectors.frame) {
727
+ return selectors.frame;
728
+ }
576
729
  if (selectors.iframe_src || selectors.frameLocators) {
577
- const findFrame = (frame, framescope) => {
730
+ const findFrame = async (frame, framescope) => {
578
731
  for (let i = 0; i < frame.selectors.length; i++) {
579
732
  let frameLocator = frame.selectors[i];
580
733
  if (frameLocator.css) {
581
- framescope = framescope.frameLocator(frameLocator.css);
734
+ let testframescope = framescope.frameLocator(frameLocator.css);
582
735
  if (frameLocator.index) {
583
- framescope = framescope.nth(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);
584
747
  }
585
- break;
586
748
  }
587
749
  }
588
750
  if (frame.children) {
589
- return findFrame(frame.children, framescope);
751
+ return await findFrame(frame.children, framescope);
590
752
  }
591
753
  return framescope;
592
754
  };
593
- info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
755
+ let fLocator = null;
594
756
  while (true) {
595
757
  let frameFound = false;
596
758
  if (selectors.nestFrmLoc) {
597
- scope = findFrame(selectors.nestFrmLoc, scope);
759
+ fLocator = selectors.nestFrmLoc;
760
+ scope = await findFrame(selectors.nestFrmLoc, scope);
598
761
  frameFound = true;
599
762
  break;
600
763
  }
@@ -602,6 +765,7 @@ class StableBrowser {
602
765
  for (let i = 0; i < selectors.frameLocators.length; i++) {
603
766
  let frameLocator = selectors.frameLocators[i];
604
767
  if (frameLocator.css) {
768
+ fLocator = frameLocator.css;
605
769
  scope = scope.frameLocator(frameLocator.css);
606
770
  frameFound = true;
607
771
  break;
@@ -609,20 +773,54 @@ class StableBrowser {
609
773
  }
610
774
  }
611
775
  if (!frameFound && selectors.iframe_src) {
776
+ fLocator = selectors.iframe_src;
612
777
  scope = this.page.frame({ url: selectors.iframe_src });
613
778
  }
614
779
  if (!scope) {
615
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
616
- 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}"`;
617
787
  throw new Error("unable to locate iframe " + selectors.iframe_src);
618
788
  }
619
789
  await new Promise((resolve) => setTimeout(resolve, 1000));
620
790
  }
621
791
  else {
792
+ if (info && info.locatorLog) {
793
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
794
+ }
622
795
  break;
623
796
  }
624
797
  }
625
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);
626
824
  let selectorsLocators = null;
627
825
  selectorsLocators = selectors.locators;
628
826
  // group selectors by priority
@@ -650,6 +848,7 @@ class StableBrowser {
650
848
  let highPriorityOnly = true;
651
849
  let visibleOnly = true;
652
850
  while (true) {
851
+ let scope = await this._findFrameScope(selectors, timeout, info);
653
852
  locatorsCount = 0;
654
853
  let result = [];
655
854
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -658,18 +857,13 @@ class StableBrowser {
658
857
  }
659
858
  // info.log += "scanning locators in priority 1" + "\n";
660
859
  let onlyPriority3 = selectorsLocators[0].priority === 3;
661
- 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);
662
861
  if (result.foundElements.length === 0) {
663
862
  // info.log += "scanning locators in priority 2" + "\n";
664
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
665
- }
666
- if (result.foundElements.length === 0 && onlyPriority3) {
667
- 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);
668
864
  }
669
- else {
670
- if (result.foundElements.length === 0 && !highPriorityOnly) {
671
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
672
- }
865
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
866
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
673
867
  }
674
868
  let foundElements = result.foundElements;
675
869
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -709,24 +903,43 @@ class StableBrowser {
709
903
  return maxCountElement.locator;
710
904
  }
711
905
  }
712
- if (performance.now() - startTime > timeout) {
906
+ if (Date.now() - startTime > timeout) {
713
907
  break;
714
908
  }
715
- if (performance.now() - startTime > highPriorityTimeout) {
716
- 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";
717
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
+ }
718
916
  }
719
- if (performance.now() - startTime > visibleOnlyTimeout) {
720
- 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";
721
919
  visibleOnly = false;
722
920
  }
723
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
+ }
724
927
  }
725
928
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
726
- 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
+ }
727
940
  throw new Error("failed to locate first element no elements found, " + info.log);
728
941
  }
729
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
942
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
730
943
  let foundElements = [];
731
944
  const result = {
732
945
  foundElements: foundElements,
@@ -734,17 +947,20 @@ class StableBrowser {
734
947
  for (let i = 0; i < locatorsGroup.length; i++) {
735
948
  let foundLocators = [];
736
949
  try {
737
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
950
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
738
951
  }
739
952
  catch (e) {
740
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
741
- 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);
742
956
  foundLocators = [];
743
957
  try {
744
- 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);
745
959
  }
746
960
  catch (e) {
747
- 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
+ }
748
964
  }
749
965
  }
750
966
  if (foundLocators.length === 1) {
@@ -755,270 +971,350 @@ class StableBrowser {
755
971
  });
756
972
  result.locatorIndex = i;
757
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
+ }
758
1011
  }
759
1012
  return result;
760
1013
  }
761
- async click(selectors, _params, options = {}, world = null) {
762
- 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);
763
1028
  const startTime = Date.now();
764
- if (options && options.context) {
765
- selectors.locators[0].text = options.context;
1029
+ let timeout = 30000;
1030
+ if (options && options.timeout) {
1031
+ timeout = options.timeout;
766
1032
  }
767
- const info = {};
768
- info.log = "***** click on " + selectors.element_name + " *****\n";
769
- info.operation = "click";
770
- info.selectors = selectors;
771
- let error = null;
772
- let screenshotId = null;
773
- let screenshotPath = null;
774
- try {
775
- let element = await this._locate(selectors, info, _params);
776
- await this.scrollIfNeeded(element, info);
777
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1033
+ while (true) {
778
1034
  try {
779
- await this._highlightElements(element);
780
- await element.click();
781
- 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
+ }
782
1048
  }
783
1049
  catch (e) {
784
- // await this.closeUnexpectedPopups();
785
- info.log += "click failed, will try again" + "\n";
786
- element = await this._locate(selectors, info, _params);
787
- await element.dispatchEvent("click");
788
- 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
+ }
789
1059
  }
790
- await this.waitForPageLoad();
791
- 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;
792
1131
  }
793
1132
  catch (e) {
794
- this.logger.error("click failed " + info.log);
795
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
796
- info.screenshotPath = screenshotPath;
797
- Object.assign(e, { info: info });
798
- error = e;
799
- throw e;
1133
+ await _commandError(state, e, this);
800
1134
  }
801
1135
  finally {
802
- const endTime = Date.now();
803
- this._reportToWorld(world, {
804
- element_name: selectors.element_name,
805
- type: Types.CLICK,
806
- text: `Click element`,
807
- screenshotId,
808
- result: error
809
- ? {
810
- status: "FAILED",
811
- startTime,
812
- endTime,
813
- message: error === null || error === void 0 ? void 0 : error.message,
814
- }
815
- : {
816
- status: "PASSED",
817
- startTime,
818
- endTime,
819
- },
820
- info: info,
821
- });
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);
822
1168
  }
1169
+ return found;
823
1170
  }
824
1171
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
825
- this._validateSelectors(selectors);
826
- const startTime = Date.now();
827
- const info = {};
828
- info.log = "";
829
- info.operation = "setCheck";
830
- info.checked = checked;
831
- info.selectors = selectors;
832
- let error = null;
833
- let screenshotId = null;
834
- 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
+ };
835
1183
  try {
836
- let element = await this._locate(selectors, info, _params);
837
- ({ 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));
838
1188
  try {
839
- await this._highlightElements(element);
840
- 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 });
841
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);
842
1198
  }
843
1199
  catch (e) {
844
1200
  if (e.message && e.message.includes("did not change its state")) {
845
1201
  this.logger.info("element did not change its state, ignoring...");
846
1202
  }
847
1203
  else {
848
- //await this.closeUnexpectedPopups();
849
- info.log += "setCheck failed, will try again" + "\n";
850
- element = await this._locate(selectors, info, _params);
851
- await element.setChecked(checked, { timeout: 5000, force: true });
852
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
+ }
853
1226
  }
854
1227
  }
855
1228
  await this.waitForPageLoad();
856
- return info;
1229
+ return state.info;
857
1230
  }
858
1231
  catch (e) {
859
- this.logger.error("setCheck failed " + info.log);
860
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
861
- info.screenshotPath = screenshotPath;
862
- Object.assign(e, { info: info });
863
- error = e;
864
- throw e;
1232
+ await _commandError(state, e, this);
865
1233
  }
866
1234
  finally {
867
- const endTime = Date.now();
868
- this._reportToWorld(world, {
869
- element_name: selectors.element_name,
870
- type: checked ? Types.CHECK : Types.UNCHECK,
871
- text: checked ? `Check element` : `Uncheck element`,
872
- screenshotId,
873
- result: error
874
- ? {
875
- status: "FAILED",
876
- startTime,
877
- endTime,
878
- message: error === null || error === void 0 ? void 0 : error.message,
879
- }
880
- : {
881
- status: "PASSED",
882
- startTime,
883
- endTime,
884
- },
885
- info: info,
886
- });
1235
+ await _commandFinally(state, this);
887
1236
  }
888
1237
  }
889
1238
  async hover(selectors, _params, options = {}, world = null) {
890
- this._validateSelectors(selectors);
891
- const startTime = Date.now();
892
- const info = {};
893
- info.log = "";
894
- info.operation = "hover";
895
- info.selectors = selectors;
896
- let error = null;
897
- let screenshotId = null;
898
- 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
+ };
899
1250
  try {
900
- let element = await this._locate(selectors, info, _params);
901
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
902
- try {
903
- await this._highlightElements(element);
904
- await element.hover();
905
- await new Promise((resolve) => setTimeout(resolve, 1000));
906
- }
907
- catch (e) {
908
- //await this.closeUnexpectedPopups();
909
- info.log += "hover failed, will try again" + "\n";
910
- element = await this._locate(selectors, info, _params);
911
- await element.hover({ timeout: 10000 });
912
- await new Promise((resolve) => setTimeout(resolve, 1000));
913
- }
1251
+ await _preCommand(state, this);
1252
+ await performAction("hover", state.element, options, this, state, _params);
1253
+ await _screenshot(state, this);
914
1254
  await this.waitForPageLoad();
915
- return info;
1255
+ return state.info;
916
1256
  }
917
1257
  catch (e) {
918
- this.logger.error("hover failed " + info.log);
919
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
920
- info.screenshotPath = screenshotPath;
921
- Object.assign(e, { info: info });
922
- error = e;
923
- throw e;
1258
+ await _commandError(state, e, this);
924
1259
  }
925
1260
  finally {
926
- const endTime = Date.now();
927
- this._reportToWorld(world, {
928
- element_name: selectors.element_name,
929
- type: Types.HOVER,
930
- text: `Hover element`,
931
- screenshotId,
932
- result: error
933
- ? {
934
- status: "FAILED",
935
- startTime,
936
- endTime,
937
- message: error === null || error === void 0 ? void 0 : error.message,
938
- }
939
- : {
940
- status: "PASSED",
941
- startTime,
942
- endTime,
943
- },
944
- info: info,
945
- });
1261
+ await _commandFinally(state, this);
946
1262
  }
947
1263
  }
948
1264
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
949
- this._validateSelectors(selectors);
950
1265
  if (!values) {
951
1266
  throw new Error("values is null");
952
1267
  }
953
- const startTime = Date.now();
954
- let error = null;
955
- let screenshotId = null;
956
- let screenshotPath = null;
957
- const info = {};
958
- info.log = "";
959
- info.operation = "selectOptions";
960
- 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
+ };
961
1280
  try {
962
- let element = await this._locate(selectors, info, _params);
963
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1281
+ await _preCommand(state, this);
964
1282
  try {
965
- await this._highlightElements(element);
966
- await element.selectOption(values);
1283
+ await state.element.selectOption(values);
967
1284
  }
968
1285
  catch (e) {
969
1286
  //await this.closeUnexpectedPopups();
970
- info.log += "selectOption failed, will try force" + "\n";
971
- 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 });
972
1289
  }
973
1290
  await this.waitForPageLoad();
974
- return info;
1291
+ return state.info;
975
1292
  }
976
1293
  catch (e) {
977
- this.logger.error("selectOption failed " + info.log);
978
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
979
- info.screenshotPath = screenshotPath;
980
- Object.assign(e, { info: info });
981
- this.logger.info("click failed, will try next selector");
982
- error = e;
983
- throw e;
1294
+ await _commandError(state, e, this);
984
1295
  }
985
1296
  finally {
986
- const endTime = Date.now();
987
- this._reportToWorld(world, {
988
- element_name: selectors.element_name,
989
- type: Types.SELECT,
990
- text: `Select option: ${values}`,
991
- value: values.toString(),
992
- screenshotId,
993
- result: error
994
- ? {
995
- status: "FAILED",
996
- startTime,
997
- endTime,
998
- message: error === null || error === void 0 ? void 0 : error.message,
999
- }
1000
- : {
1001
- status: "PASSED",
1002
- startTime,
1003
- endTime,
1004
- },
1005
- info: info,
1006
- });
1297
+ await _commandFinally(state, this);
1007
1298
  }
1008
1299
  }
1009
1300
  async type(_value, _params = null, options = {}, world = null) {
1010
- const startTime = Date.now();
1011
- let error = null;
1012
- let screenshotId = null;
1013
- let screenshotPath = null;
1014
- const info = {};
1015
- info.log = "";
1016
- info.operation = "type";
1017
- _value = this._fixUsingParams(_value, _params);
1018
- 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
+ };
1019
1315
  try {
1020
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1021
- const valueSegment = _value.split("&&");
1316
+ await _preCommand(state, this);
1317
+ const valueSegment = state.value.split("&&");
1022
1318
  for (let i = 0; i < valueSegment.length; i++) {
1023
1319
  if (i > 0) {
1024
1320
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1038,134 +1334,77 @@ class StableBrowser {
1038
1334
  await this.page.keyboard.type(value);
1039
1335
  }
1040
1336
  }
1041
- return info;
1337
+ return state.info;
1042
1338
  }
1043
1339
  catch (e) {
1044
- //await this.closeUnexpectedPopups();
1045
- this.logger.error("type failed " + info.log);
1046
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1047
- info.screenshotPath = screenshotPath;
1048
- Object.assign(e, { info: info });
1049
- error = e;
1050
- throw e;
1340
+ await _commandError(state, e, this);
1051
1341
  }
1052
1342
  finally {
1053
- const endTime = Date.now();
1054
- this._reportToWorld(world, {
1055
- type: Types.TYPE_PRESS,
1056
- screenshotId,
1057
- value: _value,
1058
- text: `type value: ${_value}`,
1059
- result: error
1060
- ? {
1061
- status: "FAILED",
1062
- startTime,
1063
- endTime,
1064
- message: error === null || error === void 0 ? void 0 : error.message,
1065
- }
1066
- : {
1067
- status: "PASSED",
1068
- startTime,
1069
- endTime,
1070
- },
1071
- info: info,
1072
- });
1343
+ await _commandFinally(state, this);
1073
1344
  }
1074
1345
  }
1075
1346
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
1076
- // set input value for non fillable inputs like date, time, range, color, etc.
1077
- this._validateSelectors(selectors);
1078
- const startTime = Date.now();
1079
- const info = {};
1080
- info.log = "***** set input value " + selectors.element_name + " *****\n";
1081
- info.operation = "setInputValue";
1082
- info.selectors = selectors;
1083
- value = this._fixUsingParams(value, _params);
1084
- info.value = value;
1085
- let error = null;
1086
- let screenshotId = null;
1087
- 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
+ };
1088
1358
  try {
1089
- value = await this._replaceWithLocalData(value, this);
1090
- let element = await this._locate(selectors, info, _params);
1091
- await this.scrollIfNeeded(element, info);
1092
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1093
- await this._highlightElements(element);
1359
+ await _preCommand(state, this);
1360
+ let value = await this._replaceWithLocalData(state.value, this);
1094
1361
  try {
1095
- await element.evaluateHandle((el, value) => {
1362
+ await state.element.evaluateHandle((el, value) => {
1096
1363
  el.value = value;
1097
1364
  }, value);
1098
1365
  }
1099
1366
  catch (error) {
1100
1367
  this.logger.error("setInputValue failed, will try again");
1101
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1102
- info.screenshotPath = screenshotPath;
1103
- Object.assign(error, { info: info });
1104
- await element.evaluateHandle((el, value) => {
1368
+ await _screenshot(state, this);
1369
+ Object.assign(error, { info: state.info });
1370
+ await state.element.evaluateHandle((el, value) => {
1105
1371
  el.value = value;
1106
1372
  });
1107
1373
  }
1108
1374
  }
1109
1375
  catch (e) {
1110
- this.logger.error("setInputValue failed " + info.log);
1111
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1112
- info.screenshotPath = screenshotPath;
1113
- Object.assign(e, { info: info });
1114
- error = e;
1115
- throw e;
1376
+ await _commandError(state, e, this);
1116
1377
  }
1117
1378
  finally {
1118
- const endTime = Date.now();
1119
- this._reportToWorld(world, {
1120
- element_name: selectors.element_name,
1121
- type: Types.SET_INPUT,
1122
- text: `Set input value`,
1123
- value: value,
1124
- screenshotId,
1125
- result: error
1126
- ? {
1127
- status: "FAILED",
1128
- startTime,
1129
- endTime,
1130
- message: error === null || error === void 0 ? void 0 : error.message,
1131
- }
1132
- : {
1133
- status: "PASSED",
1134
- startTime,
1135
- endTime,
1136
- },
1137
- info: info,
1138
- });
1379
+ await _commandFinally(state, this);
1139
1380
  }
1140
1381
  }
1141
1382
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1142
- this._validateSelectors(selectors);
1143
- const startTime = Date.now();
1144
- let error = null;
1145
- let screenshotId = null;
1146
- let screenshotPath = null;
1147
- const info = {};
1148
- info.log = "";
1149
- info.operation = Types.SET_DATE_TIME;
1150
- info.selectors = selectors;
1151
- 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
+ };
1152
1396
  try {
1153
- value = await this._replaceWithLocalData(value, this);
1154
- let element = await this._locate(selectors, info, _params);
1155
- //insert red border around the element
1156
- await this.scrollIfNeeded(element, info);
1157
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1158
- await this._highlightElements(element);
1397
+ await _preCommand(state, this);
1159
1398
  try {
1160
- await element.click();
1399
+ await performAction("click", state.element, options, this, state, _params);
1161
1400
  await new Promise((resolve) => setTimeout(resolve, 500));
1162
1401
  if (format) {
1163
- value = dayjs(value).format(format);
1164
- await element.fill(value);
1402
+ state.value = dayjs(state.value).format(format);
1403
+ await state.element.fill(state.value);
1165
1404
  }
1166
1405
  else {
1167
- const dateTimeValue = await getDateTimeValue({ value, element });
1168
- await element.evaluateHandle((el, dateTimeValue) => {
1406
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1407
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1169
1408
  el.value = ""; // clear input
1170
1409
  el.value = dateTimeValue;
1171
1410
  }, dateTimeValue);
@@ -1178,20 +1417,19 @@ class StableBrowser {
1178
1417
  }
1179
1418
  catch (err) {
1180
1419
  //await this.closeUnexpectedPopups();
1181
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1420
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1182
1421
  this.logger.info("Trying again");
1183
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1184
- info.screenshotPath = screenshotPath;
1185
- Object.assign(err, { info: info });
1422
+ await _screenshot(state, this);
1423
+ Object.assign(err, { info: state.info });
1186
1424
  await element.click();
1187
1425
  await new Promise((resolve) => setTimeout(resolve, 500));
1188
1426
  if (format) {
1189
- value = dayjs(value).format(format);
1190
- await element.fill(value);
1427
+ state.value = dayjs(state.value).format(format);
1428
+ await state.element.fill(state.value);
1191
1429
  }
1192
1430
  else {
1193
- const dateTimeValue = await getDateTimeValue({ value, element });
1194
- await element.evaluateHandle((el, dateTimeValue) => {
1431
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1432
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1195
1433
  el.value = ""; // clear input
1196
1434
  el.value = dateTimeValue;
1197
1435
  }, dateTimeValue);
@@ -1204,85 +1442,63 @@ class StableBrowser {
1204
1442
  }
1205
1443
  }
1206
1444
  catch (e) {
1207
- error = e;
1208
- throw e;
1445
+ await _commandError(state, e, this);
1209
1446
  }
1210
1447
  finally {
1211
- const endTime = Date.now();
1212
- this._reportToWorld(world, {
1213
- element_name: selectors.element_name,
1214
- type: Types.SET_DATE_TIME,
1215
- screenshotId,
1216
- value: value,
1217
- text: `setDateTime input with value: ${value}`,
1218
- result: error
1219
- ? {
1220
- status: "FAILED",
1221
- startTime,
1222
- endTime,
1223
- message: error === null || error === void 0 ? void 0 : error.message,
1224
- }
1225
- : {
1226
- status: "PASSED",
1227
- startTime,
1228
- endTime,
1229
- },
1230
- info: info,
1231
- });
1448
+ await _commandFinally(state, this);
1232
1449
  }
1233
1450
  }
1234
1451
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
1235
1452
  _value = unEscapeString(_value);
1236
- this._validateSelectors(selectors);
1237
- const startTime = Date.now();
1238
- let error = null;
1239
- let screenshotId = null;
1240
- let screenshotPath = null;
1241
- const info = {};
1242
- info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1243
- info.operation = "clickType";
1244
- info.selectors = selectors;
1245
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
+ }
1246
1470
  if (newValue !== _value) {
1247
1471
  //this.logger.info(_value + "=" + newValue);
1248
1472
  _value = newValue;
1249
1473
  }
1250
- info.value = _value;
1251
1474
  try {
1252
- let element = await this._locate(selectors, info, _params);
1253
- //insert red border around the element
1254
- await this.scrollIfNeeded(element, info);
1255
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1256
- await this._highlightElements(element);
1257
- if (options === null || options === undefined || !options.press) {
1475
+ await _preCommand(state, this);
1476
+ state.info.value = _value;
1477
+ if (!options.press) {
1258
1478
  try {
1259
- let currentValue = await element.inputValue();
1479
+ let currentValue = await state.element.inputValue();
1260
1480
  if (currentValue) {
1261
- await element.fill("");
1481
+ await state.element.fill("");
1262
1482
  }
1263
1483
  }
1264
1484
  catch (e) {
1265
1485
  this.logger.info("unable to clear input value");
1266
1486
  }
1267
1487
  }
1268
- if (options === null || options === undefined || options.press) {
1269
- try {
1270
- await element.click({ timeout: 5000 });
1271
- }
1272
- catch (e) {
1273
- await element.dispatchEvent("click");
1274
- }
1488
+ if (options.press) {
1489
+ options.timeout = 5000;
1490
+ await performAction("click", state.element, options, this, state, _params);
1275
1491
  }
1276
1492
  else {
1277
1493
  try {
1278
- await element.focus();
1494
+ await state.element.focus();
1279
1495
  }
1280
1496
  catch (e) {
1281
- await element.dispatchEvent("focus");
1497
+ await state.element.dispatchEvent("focus");
1282
1498
  }
1283
1499
  }
1284
1500
  await new Promise((resolve) => setTimeout(resolve, 500));
1285
- const valueSegment = _value.split("&&");
1501
+ const valueSegment = state.value.split("&&");
1286
1502
  for (let i = 0; i < valueSegment.length; i++) {
1287
1503
  if (i > 0) {
1288
1504
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1302,13 +1518,21 @@ class StableBrowser {
1302
1518
  await new Promise((resolve) => setTimeout(resolve, 500));
1303
1519
  }
1304
1520
  }
1521
+ //if (!this.fastMode) {
1522
+ await _screenshot(state, this);
1523
+ //}
1305
1524
  if (enter === true) {
1306
1525
  await new Promise((resolve) => setTimeout(resolve, 2000));
1307
1526
  await this.page.keyboard.press("Enter");
1308
1527
  await this.waitForPageLoad();
1309
1528
  }
1310
1529
  else if (enter === false) {
1311
- await element.dispatchEvent("change");
1530
+ try {
1531
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1532
+ }
1533
+ catch (e) {
1534
+ // ignore
1535
+ }
1312
1536
  //await this.page.keyboard.press("Tab");
1313
1537
  }
1314
1538
  else {
@@ -1317,112 +1541,95 @@ class StableBrowser {
1317
1541
  await this.waitForPageLoad();
1318
1542
  }
1319
1543
  }
1320
- return info;
1544
+ return state.info;
1321
1545
  }
1322
1546
  catch (e) {
1323
- //await this.closeUnexpectedPopups();
1324
- this.logger.error("fill failed " + JSON.stringify(info));
1325
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1326
- info.screenshotPath = screenshotPath;
1327
- Object.assign(e, { info: info });
1328
- error = e;
1329
- throw e;
1547
+ await _commandError(state, e, this);
1330
1548
  }
1331
1549
  finally {
1332
- const endTime = Date.now();
1333
- this._reportToWorld(world, {
1334
- element_name: selectors.element_name,
1335
- type: Types.FILL,
1336
- screenshotId,
1337
- value: _value,
1338
- text: `clickType input with value: ${_value}`,
1339
- result: error
1340
- ? {
1341
- status: "FAILED",
1342
- startTime,
1343
- endTime,
1344
- message: error === null || error === void 0 ? void 0 : error.message,
1345
- }
1346
- : {
1347
- status: "PASSED",
1348
- startTime,
1349
- endTime,
1350
- },
1351
- info: info,
1352
- });
1550
+ await _commandFinally(state, this);
1353
1551
  }
1354
1552
  }
1355
1553
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
1356
- this._validateSelectors(selectors);
1357
- value = unEscapeString(value);
1358
- const startTime = Date.now();
1359
- let error = null;
1360
- let screenshotId = null;
1361
- let screenshotPath = null;
1362
- const info = {};
1363
- info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1364
- info.operation = "fill";
1365
- info.selectors = selectors;
1366
- 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
+ };
1367
1565
  try {
1368
- let element = await this._locate(selectors, info, _params);
1369
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1370
- await this._highlightElements(element);
1371
- await element.fill(value);
1372
- await element.dispatchEvent("change");
1566
+ await _preCommand(state, this);
1567
+ await state.element.fill(value);
1568
+ await state.element.dispatchEvent("change");
1373
1569
  if (enter) {
1374
1570
  await new Promise((resolve) => setTimeout(resolve, 2000));
1375
1571
  await this.page.keyboard.press("Enter");
1376
1572
  }
1377
1573
  await this.waitForPageLoad();
1378
- return info;
1574
+ return state.info;
1379
1575
  }
1380
1576
  catch (e) {
1381
- //await this.closeUnexpectedPopups();
1382
- this.logger.error("fill failed " + info.log);
1383
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1384
- info.screenshotPath = screenshotPath;
1385
- Object.assign(e, { info: info });
1386
- error = e;
1387
- throw e;
1577
+ await _commandError(state, e, this);
1388
1578
  }
1389
1579
  finally {
1390
- const endTime = Date.now();
1391
- this._reportToWorld(world, {
1392
- element_name: selectors.element_name,
1393
- type: Types.FILL,
1394
- screenshotId,
1395
- value,
1396
- text: `Fill input with value: ${value}`,
1397
- result: error
1398
- ? {
1399
- status: "FAILED",
1400
- startTime,
1401
- endTime,
1402
- message: error === null || error === void 0 ? void 0 : error.message,
1403
- }
1404
- : {
1405
- status: "PASSED",
1406
- startTime,
1407
- endTime,
1408
- },
1409
- info: info,
1410
- });
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);
1411
1616
  }
1412
1617
  }
1413
1618
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1414
1619
  return await this._getText(selectors, 0, _params, options, info, world);
1415
1620
  }
1416
1621
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1417
- this._validateSelectors(selectors);
1622
+ const timeout = this._getFindElementTimeout(options);
1623
+ _validateSelectors(selectors);
1418
1624
  let screenshotId = null;
1419
1625
  let screenshotPath = null;
1420
1626
  if (!info.log) {
1421
1627
  info.log = "";
1628
+ info.locatorLog = new LocatorLog(selectors);
1422
1629
  }
1423
1630
  info.operation = "getText";
1424
1631
  info.selectors = selectors;
1425
- let element = await this._locate(selectors, info, _params);
1632
+ let element = await this._locate(selectors, info, _params, timeout);
1426
1633
  if (climb > 0) {
1427
1634
  const climbArray = [];
1428
1635
  for (let i = 0; i < climb; i++) {
@@ -1441,6 +1648,18 @@ class StableBrowser {
1441
1648
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1442
1649
  try {
1443
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
+ // }
1444
1663
  const elementText = await element.innerText();
1445
1664
  return {
1446
1665
  text: elementText,
@@ -1452,189 +1671,219 @@ class StableBrowser {
1452
1671
  }
1453
1672
  catch (e) {
1454
1673
  //await this.closeUnexpectedPopups();
1455
- this.logger.info("no innerText will use textContent");
1674
+ this.logger.info("no innerText, will use textContent");
1456
1675
  const elementText = await element.textContent();
1457
1676
  return { text: elementText, screenshotId, screenshotPath, value: value };
1458
1677
  }
1459
1678
  }
1460
1679
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1461
- var _a;
1462
- this._validateSelectors(selectors);
1463
1680
  if (!pattern) {
1464
1681
  throw new Error("pattern is null");
1465
1682
  }
1466
1683
  if (!text) {
1467
1684
  throw new Error("text is null");
1468
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
+ };
1469
1703
  const newValue = await this._replaceWithLocalData(text, world);
1470
1704
  if (newValue !== text) {
1471
1705
  this.logger.info(text + "=" + newValue);
1472
1706
  text = newValue;
1473
1707
  }
1474
- const startTime = Date.now();
1475
- let error = null;
1476
- let screenshotId = null;
1477
- let screenshotPath = null;
1478
- const info = {};
1479
- info.log =
1480
- "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1481
- info.operation = "containsPattern";
1482
- info.selectors = selectors;
1483
- info.value = text;
1484
- info.pattern = pattern;
1485
1708
  let foundObj = null;
1486
1709
  try {
1487
- 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);
1488
1713
  if (foundObj && foundObj.element) {
1489
- await this.scrollIfNeeded(foundObj.element, info);
1714
+ await this.scrollIfNeeded(foundObj.element, state.info);
1490
1715
  }
1491
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1716
+ await _screenshot(state, this);
1492
1717
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1493
1718
  pattern = pattern.replace("{text}", escapedText);
1494
1719
  let regex = new RegExp(pattern, "im");
1495
- 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))) {
1496
- 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;
1497
1722
  throw new Error("element doesn't contain text " + text);
1498
1723
  }
1499
- return info;
1724
+ return state.info;
1500
1725
  }
1501
1726
  catch (e) {
1502
- //await this.closeUnexpectedPopups();
1503
- this.logger.error("verify element contains text failed " + info.log);
1504
- this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
1505
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1506
- info.screenshotPath = screenshotPath;
1507
- Object.assign(e, { info: info });
1508
- error = e;
1509
- throw e;
1727
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1728
+ await _commandError(state, e, this);
1510
1729
  }
1511
1730
  finally {
1512
- const endTime = Date.now();
1513
- this._reportToWorld(world, {
1514
- element_name: selectors.element_name,
1515
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1516
- value: pattern,
1517
- text: `Verify element contains pattern: ${pattern}`,
1518
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1519
- result: error
1520
- ? {
1521
- status: "FAILED",
1522
- startTime,
1523
- endTime,
1524
- message: error === null || error === void 0 ? void 0 : error.message,
1525
- }
1526
- : {
1527
- status: "PASSED",
1528
- startTime,
1529
- endTime,
1530
- },
1531
- info: info,
1532
- });
1731
+ await _commandFinally(state, this);
1533
1732
  }
1534
1733
  }
1535
1734
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1536
- var _a, _b, _c;
1537
- this._validateSelectors(selectors);
1538
- 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
+ };
1539
1752
  if (!text) {
1540
1753
  throw new Error("text is null");
1541
1754
  }
1542
- const startTime = Date.now();
1543
- let error = null;
1544
- let screenshotId = null;
1545
- let screenshotPath = null;
1546
- const info = {};
1547
- info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1548
- info.operation = "containsText";
1549
- info.selectors = selectors;
1755
+ text = unEscapeString(text);
1550
1756
  const newValue = await this._replaceWithLocalData(text, world);
1551
1757
  if (newValue !== text) {
1552
1758
  this.logger.info(text + "=" + newValue);
1553
1759
  text = newValue;
1554
1760
  }
1555
- info.value = text;
1556
1761
  let foundObj = null;
1557
1762
  try {
1558
- foundObj = await this._getText(selectors, climb, _params, options, info, world);
1559
- if (foundObj && foundObj.element) {
1560
- await this.scrollIfNeeded(foundObj.element, info);
1561
- }
1562
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1563
- const dateAlternatives = findDateAlternatives(text);
1564
- const numberAlternatives = findNumberAlternatives(text);
1565
- if (dateAlternatives.date) {
1566
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1567
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
1568
- ((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
1569
- 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);
1570
1769
  }
1571
- }
1572
- throw new Error("element doesn't contain text " + text);
1573
- }
1574
- else if (numberAlternatives.number) {
1575
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1576
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(numberAlternatives.numbers[i])) ||
1577
- ((_b = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _b === void 0 ? void 0 : _b.includes(numberAlternatives.numbers[i]))) {
1578
- 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;
1579
1791
  }
1580
1792
  }
1581
- throw new Error("element doesn't contain text " + text);
1582
- }
1583
- 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))) {
1584
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1585
- info.value = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value;
1586
- 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
1587
1798
  }
1588
- return info;
1799
+ state.info.foundText = foundObj?.text;
1800
+ state.info.value = foundObj?.value;
1801
+ throw new Error("element doesn't contain text " + text);
1589
1802
  }
1590
1803
  catch (e) {
1591
- //await this.closeUnexpectedPopups();
1592
- this.logger.error("verify element contains text failed " + info.log);
1593
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1594
- info.screenshotPath = screenshotPath;
1595
- Object.assign(e, { info: info });
1596
- error = e;
1804
+ await _commandError(state, e, this);
1597
1805
  throw e;
1598
1806
  }
1599
1807
  finally {
1600
- const endTime = Date.now();
1601
- this._reportToWorld(world, {
1602
- element_name: selectors.element_name,
1603
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1604
- text: `Verify element contains text: ${text}`,
1605
- value: text,
1606
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1607
- result: error
1608
- ? {
1609
- status: "FAILED",
1610
- startTime,
1611
- endTime,
1612
- message: error === null || error === void 0 ? void 0 : error.message,
1613
- }
1614
- : {
1615
- status: "PASSED",
1616
- startTime,
1617
- endTime,
1618
- },
1619
- info: info,
1620
- });
1808
+ await _commandFinally(state, this);
1621
1809
  }
1622
1810
  }
1623
- _getDataFile(world = null) {
1624
- let dataFile = null;
1625
- if (world && world.reportFolder) {
1626
- 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");
1627
1830
  }
1628
- else if (this.reportFolder) {
1629
- dataFile = path.join(this.reportFolder, "data.json");
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");
1630
1834
  }
1631
- else if (this.context && this.context.reportFolder) {
1632
- dataFile = path.join(this.context.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");
1837
+ }
1838
+ else if (referanceSnapshot.startsWith("yaml:")) {
1839
+ text = referanceSnapshot.substring(5);
1633
1840
  }
1634
1841
  else {
1635
- 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);
1636
1886
  }
1637
- return dataFile;
1638
1887
  }
1639
1888
  async waitForUserInput(message, world = null) {
1640
1889
  if (!message) {
@@ -1664,13 +1913,22 @@ class StableBrowser {
1664
1913
  return;
1665
1914
  }
1666
1915
  // if data file exists, load it
1667
- const dataFile = this._getDataFile(world);
1916
+ const dataFile = _getDataFile(world, this.context, this);
1668
1917
  let data = this.getTestData(world);
1669
1918
  // merge the testData with the existing data
1670
1919
  Object.assign(data, testData);
1671
1920
  // save the data to the file
1672
1921
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1673
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
+ }
1674
1932
  _getDataFilePath(fileName) {
1675
1933
  let dataFile = path.join(this.project_path, "data", fileName);
1676
1934
  if (fs.existsSync(dataFile)) {
@@ -1767,12 +2025,7 @@ class StableBrowser {
1767
2025
  }
1768
2026
  }
1769
2027
  getTestData(world = null) {
1770
- const dataFile = this._getDataFile(world);
1771
- let data = {};
1772
- if (fs.existsSync(dataFile)) {
1773
- data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1774
- }
1775
- return data;
2028
+ return _getTestData(world, this.context, this);
1776
2029
  }
1777
2030
  async _screenShot(options = {}, world = null, info = null) {
1778
2031
  // collect url/path/title
@@ -1799,11 +2052,9 @@ class StableBrowser {
1799
2052
  if (!fs.existsSync(world.screenshotPath)) {
1800
2053
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1801
2054
  }
1802
- let nextIndex = 1;
1803
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1804
- nextIndex++;
1805
- }
1806
- 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");
1807
2058
  try {
1808
2059
  await this.takeScreenshot(screenshotPath);
1809
2060
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1813,15 +2064,15 @@ class StableBrowser {
1813
2064
  // this.logger.info("unable to save screenshot " + screenshotPath);
1814
2065
  // }
1815
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
+ }
1816
2072
  }
1817
2073
  catch (e) {
1818
2074
  this.logger.info("unable to take screenshot, ignored");
1819
2075
  }
1820
- result.screenshotId = nextIndex;
1821
- result.screenshotPath = screenshotPath;
1822
- if (info && info.box) {
1823
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1824
- }
1825
2076
  }
1826
2077
  else if (options && options.screenshot) {
1827
2078
  result.screenshotPath = options.screenshotPath;
@@ -1846,7 +2097,6 @@ class StableBrowser {
1846
2097
  }
1847
2098
  async takeScreenshot(screenshotPath) {
1848
2099
  const playContext = this.context.playContext;
1849
- const client = await playContext.newCDPSession(this.page);
1850
2100
  // Using CDP to capture the screenshot
1851
2101
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
1852
2102
  document.body.scrollWidth,
@@ -1856,165 +2106,561 @@ class StableBrowser {
1856
2106
  document.body.clientWidth,
1857
2107
  document.documentElement.clientWidth,
1858
2108
  ])));
1859
- const viewportHeight = Math.max(...(await this.page.evaluate(() => [
1860
- document.body.scrollHeight,
1861
- document.documentElement.scrollHeight,
1862
- document.body.offsetHeight,
1863
- document.documentElement.offsetHeight,
1864
- document.body.clientHeight,
1865
- document.documentElement.clientHeight,
1866
- ])));
1867
- const { data } = await client.send("Page.captureScreenshot", {
1868
- format: "png",
1869
- // clip: {
1870
- // x: 0,
1871
- // y: 0,
1872
- // width: viewportWidth,
1873
- // height: viewportHeight,
1874
- // scale: 1,
1875
- // },
1876
- });
1877
- if (!screenshotPath) {
1878
- return data;
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");
1879
2136
  }
1880
- let screenshotBuffer = Buffer.from(data, "base64");
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
+ // }
1881
2144
  let image = await Jimp.read(screenshotBuffer);
1882
2145
  // Get the image dimensions
1883
2146
  const { width, height } = image.bitmap;
2147
+ const resizeRatio = viewportWidth / width;
1884
2148
  // Resize the image to fit within the viewport dimensions without enlarging
1885
- if (width > viewportWidth || height > viewportHeight) {
1886
- image = image.resize({ w: viewportWidth, h: viewportHeight }); // Resize the image while maintaining aspect ratio
2149
+ if (width > viewportWidth) {
2150
+ image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
1887
2151
  await image.write(screenshotPath);
1888
2152
  }
1889
2153
  else {
1890
2154
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1891
2155
  }
1892
- await client.detach();
2156
+ return screenshotBuffer;
1893
2157
  }
1894
2158
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1895
- this._validateSelectors(selectors);
1896
- const startTime = Date.now();
1897
- let error = null;
1898
- let screenshotId = null;
1899
- 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
+ };
1900
2169
  await new Promise((resolve) => setTimeout(resolve, 2000));
1901
- const info = {};
1902
- info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1903
- info.operation = "verify";
1904
- info.selectors = selectors;
1905
2170
  try {
1906
- const element = await this._locate(selectors, info, _params);
1907
- if (element) {
1908
- await this.scrollIfNeeded(element, info);
1909
- }
1910
- await this._highlightElements(element);
1911
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1912
- await expect(element).toHaveCount(1, { timeout: 10000 });
1913
- return info;
2171
+ await _preCommand(state, this);
2172
+ await expect(state.element).toHaveCount(1, { timeout: 10000 });
2173
+ return state.info;
1914
2174
  }
1915
2175
  catch (e) {
1916
- //await this.closeUnexpectedPopups();
1917
- this.logger.error("verify failed " + info.log);
1918
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1919
- info.screenshotPath = screenshotPath;
1920
- Object.assign(e, { info: info });
1921
- error = e;
1922
- throw e;
2176
+ await _commandError(state, e, this);
1923
2177
  }
1924
2178
  finally {
1925
- const endTime = Date.now();
1926
- this._reportToWorld(world, {
1927
- element_name: selectors.element_name,
1928
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1929
- text: "Verify element exists in page",
1930
- screenshotId,
1931
- result: error
1932
- ? {
1933
- status: "FAILED",
1934
- startTime,
1935
- endTime,
1936
- message: error === null || error === void 0 ? void 0 : error.message,
1937
- }
1938
- : {
1939
- status: "PASSED",
1940
- startTime,
1941
- endTime,
1942
- },
1943
- info: info,
1944
- });
2179
+ await _commandFinally(state, this);
1945
2180
  }
1946
2181
  }
1947
2182
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1948
- this._validateSelectors(selectors);
1949
- const startTime = Date.now();
1950
- let error = null;
1951
- let screenshotId = null;
1952
- 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
+ };
1953
2197
  await new Promise((resolve) => setTimeout(resolve, 2000));
1954
- const info = {};
1955
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1956
- info.operation = "extract";
1957
- info.selectors = selectors;
1958
2198
  try {
1959
- const element = await this._locate(selectors, info, _params);
1960
- await this._highlightElements(element);
1961
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2199
+ await _preCommand(state, this);
1962
2200
  switch (attribute) {
1963
2201
  case "inner_text":
1964
- info.value = await element.innerText();
2202
+ state.value = await state.element.innerText();
1965
2203
  break;
1966
2204
  case "href":
1967
- info.value = await element.getAttribute("href");
2205
+ state.value = await state.element.getAttribute("href");
1968
2206
  break;
1969
2207
  case "value":
1970
- info.value = await element.inputValue();
2208
+ state.value = await state.element.inputValue();
2209
+ break;
2210
+ case "text":
2211
+ state.value = await state.element.textContent();
1971
2212
  break;
1972
2213
  default:
1973
- info.value = await element.getAttribute(attribute);
2214
+ state.value = await state.element.getAttribute(attribute);
1974
2215
  break;
1975
2216
  }
1976
- this[variable] = info.value;
1977
- if (world) {
1978
- 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
+ }
1979
2234
  }
1980
- this.setTestData({ [variable]: info.value }, world);
1981
- this.logger.info("set test data: " + variable + "=" + info.value);
1982
- 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;
1983
2240
  }
1984
2241
  catch (e) {
1985
- //await this.closeUnexpectedPopups();
1986
- this.logger.error("extract failed " + info.log);
1987
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1988
- info.screenshotPath = screenshotPath;
1989
- Object.assign(e, { info: info });
1990
- error = e;
1991
- throw e;
2242
+ await _commandError(state, e, this);
1992
2243
  }
1993
2244
  finally {
1994
- const endTime = Date.now();
1995
- this._reportToWorld(world, {
1996
- element_name: selectors.element_name,
1997
- type: Types.EXTRACT_ATTRIBUTE,
1998
- variable: variable,
1999
- value: info.value,
2000
- text: "Extract attribute from element",
2001
- screenshotId,
2002
- result: error
2003
- ? {
2004
- status: "FAILED",
2005
- startTime,
2006
- endTime,
2007
- 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}`)) || "";
2008
2283
  }
2009
- : {
2010
- status: "PASSED",
2011
- startTime,
2012
- endTime,
2013
- },
2014
- info: info,
2015
- });
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);
2016
2317
  }
2017
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);
2658
+ }
2659
+ catch (finallyError) {
2660
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2661
+ }
2662
+ return state.info;
2663
+ }
2018
2664
  async extractEmailData(emailAddress, options, world) {
2019
2665
  if (!emailAddress) {
2020
2666
  throw new Error("email address is null");
@@ -2033,7 +2679,7 @@ class StableBrowser {
2033
2679
  if (options && options.timeout) {
2034
2680
  timeout = options.timeout;
2035
2681
  }
2036
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2682
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2037
2683
  const request = {
2038
2684
  method: "POST",
2039
2685
  url: serviceUrl,
@@ -2089,7 +2735,8 @@ class StableBrowser {
2089
2735
  catch (e) {
2090
2736
  errorCount++;
2091
2737
  if (errorCount > 3) {
2092
- throw e;
2738
+ // throw e;
2739
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
2093
2740
  }
2094
2741
  // ignore
2095
2742
  }
@@ -2103,27 +2750,32 @@ class StableBrowser {
2103
2750
  async _highlightElements(scope, css) {
2104
2751
  try {
2105
2752
  if (!scope) {
2753
+ // console.log(`Scope is not defined`);
2106
2754
  return;
2107
2755
  }
2108
2756
  if (!css) {
2109
2757
  scope
2110
2758
  .evaluate((node) => {
2111
2759
  if (node && node.style) {
2112
- let originalBorder = node.style.border;
2113
- 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}`);
2114
2765
  if (window) {
2115
2766
  window.addEventListener("beforeunload", function (e) {
2116
- node.style.border = originalBorder;
2767
+ node.style.outline = originalOutline;
2117
2768
  });
2118
2769
  }
2119
2770
  setTimeout(function () {
2120
- node.style.border = originalBorder;
2771
+ node.style.outline = originalOutline;
2121
2772
  }, 2000);
2122
2773
  }
2123
2774
  })
2124
2775
  .then(() => { })
2125
2776
  .catch((e) => {
2126
2777
  // ignore
2778
+ // console.error(`Could not highlight node : ${e}`);
2127
2779
  });
2128
2780
  }
2129
2781
  else {
@@ -2139,17 +2791,18 @@ class StableBrowser {
2139
2791
  if (!element.style) {
2140
2792
  return;
2141
2793
  }
2142
- var originalBorder = element.style.border;
2794
+ let originalOutline = element.style.outline;
2795
+ element.__previousOutline = originalOutline;
2143
2796
  // Set the new border to be red and 2px solid
2144
- element.style.border = "2px solid red";
2797
+ element.style.outline = "2px solid red";
2145
2798
  if (window) {
2146
2799
  window.addEventListener("beforeunload", function (e) {
2147
- element.style.border = originalBorder;
2800
+ element.style.outline = originalOutline;
2148
2801
  });
2149
2802
  }
2150
2803
  // Set a timeout to revert to the original border after 2 seconds
2151
2804
  setTimeout(function () {
2152
- element.style.border = originalBorder;
2805
+ element.style.outline = originalOutline;
2153
2806
  }, 2000);
2154
2807
  }
2155
2808
  return;
@@ -2157,6 +2810,7 @@ class StableBrowser {
2157
2810
  .then(() => { })
2158
2811
  .catch((e) => {
2159
2812
  // ignore
2813
+ // console.error(`Could not highlight css: ${e}`);
2160
2814
  });
2161
2815
  }
2162
2816
  }
@@ -2164,8 +2818,49 @@ class StableBrowser {
2164
2818
  console.debug(error);
2165
2819
  }
2166
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
+ */
2167
2863
  async verifyPagePath(pathPart, options = {}, world = null) {
2168
- const startTime = Date.now();
2169
2864
  let error = null;
2170
2865
  let screenshotId = null;
2171
2866
  let screenshotPath = null;
@@ -2179,159 +2874,520 @@ class StableBrowser {
2179
2874
  pathPart = newValue;
2180
2875
  }
2181
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
+ };
2182
2891
  try {
2892
+ await _preCommand(state, this);
2893
+ state.info.text = queryText;
2183
2894
  for (let i = 0; i < 30; i++) {
2184
2895
  const url = await this.page.url();
2185
- if (!url.includes(pathPart)) {
2186
- if (i === 29) {
2187
- 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`);
2188
3157
  }
2189
3158
  await new Promise((resolve) => setTimeout(resolve, 1000));
2190
3159
  continue;
2191
3160
  }
2192
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2193
- 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
+ }
2194
3178
  }
2195
3179
  }
2196
3180
  catch (e) {
2197
- //await this.closeUnexpectedPopups();
2198
- this.logger.error("verify page path failed " + info.log);
2199
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2200
- info.screenshotPath = screenshotPath;
2201
- Object.assign(e, { info: info });
2202
- error = e;
2203
- throw e;
3181
+ await _commandError(state, e, this);
2204
3182
  }
2205
3183
  finally {
2206
- const endTime = Date.now();
2207
- this._reportToWorld(world, {
2208
- type: Types.VERIFY_PAGE_PATH,
2209
- text: "Verify page path",
2210
- screenshotId,
2211
- result: error
2212
- ? {
2213
- status: "FAILED",
2214
- startTime,
2215
- endTime,
2216
- message: error === null || error === void 0 ? void 0 : error.message,
2217
- }
2218
- : {
2219
- status: "PASSED",
2220
- startTime,
2221
- endTime,
2222
- },
2223
- info: info,
2224
- });
3184
+ await _commandFinally(state, this);
2225
3185
  }
2226
3186
  }
2227
- async verifyTextExistInPage(text, options = {}, world = null) {
3187
+ async waitForTextToDisappear(text, options = {}, world = null) {
2228
3188
  text = unEscapeString(text);
2229
- const startTime = Date.now();
2230
- const timeout = this._getLoadTimeout(options);
2231
- let error = null;
2232
- let screenshotId = null;
2233
- 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);
2234
3206
  await new Promise((resolve) => setTimeout(resolve, 2000));
2235
- const info = {};
2236
- info.log = "***** verify text " + text + " exists in page *****\n";
2237
- info.operation = "verifyTextExistInPage";
2238
3207
  const newValue = await this._replaceWithLocalData(text, world);
2239
3208
  if (newValue !== text) {
2240
3209
  this.logger.info(text + "=" + newValue);
2241
3210
  text = newValue;
2242
3211
  }
2243
- info.text = text;
2244
3212
  let dateAlternatives = findDateAlternatives(text);
2245
3213
  let numberAlternatives = findNumberAlternatives(text);
2246
3214
  try {
3215
+ await _preCommand(state, this);
3216
+ state.info.text = text;
3217
+ let resultWithElementsFound = {
3218
+ length: null, // initial cannot be 0
3219
+ };
2247
3220
  while (true) {
2248
- const frames = this.page.frames();
2249
- let results = [];
2250
- for (let i = 0; i < frames.length; i++) {
2251
- if (dateAlternatives.date) {
2252
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2253
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
2254
- result.frame = frames[i];
2255
- results.push(result);
2256
- }
2257
- }
2258
- else if (numberAlternatives.number) {
2259
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2260
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
2261
- result.frame = frames[i];
2262
- results.push(result);
2263
- }
2264
- }
2265
- else {
2266
- const result = await this._locateElementByText(frames[i], text, "*", true, {});
2267
- result.frame = frames[i];
2268
- results.push(result);
2269
- }
3221
+ try {
3222
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
3223
+ }
3224
+ catch (error) {
3225
+ // ignore
2270
3226
  }
2271
- info.results = results;
2272
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2273
3227
  if (resultWithElementsFound.length === 0) {
2274
- if (Date.now() - startTime > timeout) {
2275
- 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`);
2276
3293
  }
2277
3294
  await new Promise((resolve) => setTimeout(resolve, 1000));
2278
3295
  continue;
2279
3296
  }
2280
- if (resultWithElementsFound[0].randomToken) {
2281
- const frame = resultWithElementsFound[0].frame;
2282
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2283
- await this._highlightElements(frame, dataAttribute);
2284
- const element = await frame.$(dataAttribute);
2285
- if (element) {
2286
- await this.scrollIfNeeded(element, info);
2287
- 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
+ }
2288
3353
  }
2289
3354
  }
2290
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2291
- return info;
3355
+ catch (error) {
3356
+ console.error(error);
3357
+ }
2292
3358
  }
2293
3359
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2294
3360
  }
2295
3361
  catch (e) {
2296
- //await this.closeUnexpectedPopups();
2297
- this.logger.error("verify text exist in page failed " + info.log);
2298
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2299
- info.screenshotPath = screenshotPath;
2300
- Object.assign(e, { info: info });
2301
- error = e;
2302
- throw e;
3362
+ await _commandError(state, e, this);
2303
3363
  }
2304
3364
  finally {
2305
- const endTime = Date.now();
2306
- this._reportToWorld(world, {
2307
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2308
- text: "Verify text exists in page",
2309
- screenshotId,
2310
- result: error
2311
- ? {
2312
- status: "FAILED",
2313
- startTime,
2314
- endTime,
2315
- message: error === null || error === void 0 ? void 0 : error.message,
2316
- }
2317
- : {
2318
- status: "PASSED",
2319
- startTime,
2320
- endTime,
2321
- },
2322
- info: info,
2323
- });
3365
+ await _commandFinally(state, this);
2324
3366
  }
2325
3367
  }
2326
- _getServerUrl() {
2327
- let serviceUrl = "https://api.blinq.io";
2328
- if (process.env.NODE_ENV_BLINQ === "dev") {
2329
- serviceUrl = "https://dev.api.blinq.io";
2330
- }
2331
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2332
- 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
+ }
2333
3387
  }
2334
- return serviceUrl;
3388
+ // state.info.results = results;
3389
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
3390
+ return resultWithElementsFound;
2335
3391
  }
2336
3392
  async visualVerification(text, options = {}, world = null) {
2337
3393
  const startTime = Date.now();
@@ -2347,14 +3403,17 @@ class StableBrowser {
2347
3403
  throw new Error("TOKEN is not set");
2348
3404
  }
2349
3405
  try {
2350
- let serviceUrl = this._getServerUrl();
3406
+ let serviceUrl = _getServerUrl();
2351
3407
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2352
3408
  info.screenshotPath = screenshotPath;
2353
3409
  const screenshot = await this.takeScreenshot();
2354
- const request = {
2355
- method: "POST",
3410
+ let request = {
3411
+ method: "post",
3412
+ maxBodyLength: Infinity,
2356
3413
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2357
3414
  headers: {
3415
+ "x-bvt-project-id": path.basename(this.project_path),
3416
+ "x-source": "aaa",
2358
3417
  "Content-Type": "application/json",
2359
3418
  Authorization: `Bearer ${process.env.TOKEN}`,
2360
3419
  },
@@ -2363,7 +3422,7 @@ class StableBrowser {
2363
3422
  screenshot: screenshot,
2364
3423
  }),
2365
3424
  };
2366
- let result = await this.context.api.request(request);
3425
+ const result = await axios.request(request);
2367
3426
  if (result.data.status !== true) {
2368
3427
  throw new Error("Visual validation failed");
2369
3428
  }
@@ -2383,20 +3442,22 @@ class StableBrowser {
2383
3442
  info.screenshotPath = screenshotPath;
2384
3443
  Object.assign(e, { info: info });
2385
3444
  error = e;
2386
- throw e;
3445
+ // throw e;
3446
+ await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
2387
3447
  }
2388
3448
  finally {
2389
3449
  const endTime = Date.now();
2390
- this._reportToWorld(world, {
3450
+ _reportToWorld(world, {
2391
3451
  type: Types.VERIFY_VISUAL,
2392
3452
  text: "Visual verification",
3453
+ _text: "Visual verification of " + text,
2393
3454
  screenshotId,
2394
3455
  result: error
2395
3456
  ? {
2396
3457
  status: "FAILED",
2397
3458
  startTime,
2398
3459
  endTime,
2399
- message: error === null || error === void 0 ? void 0 : error.message,
3460
+ message: error?.message,
2400
3461
  }
2401
3462
  : {
2402
3463
  status: "PASSED",
@@ -2428,13 +3489,14 @@ class StableBrowser {
2428
3489
  this.logger.info("Table data verified");
2429
3490
  }
2430
3491
  async getTableData(selectors, _params = null, options = {}, world = null) {
2431
- this._validateSelectors(selectors);
3492
+ _validateSelectors(selectors);
2432
3493
  const startTime = Date.now();
2433
3494
  let error = null;
2434
3495
  let screenshotId = null;
2435
3496
  let screenshotPath = null;
2436
3497
  const info = {};
2437
3498
  info.log = "";
3499
+ info.locatorLog = new LocatorLog(selectors);
2438
3500
  info.operation = "getTableData";
2439
3501
  info.selectors = selectors;
2440
3502
  try {
@@ -2450,11 +3512,12 @@ class StableBrowser {
2450
3512
  info.screenshotPath = screenshotPath;
2451
3513
  Object.assign(e, { info: info });
2452
3514
  error = e;
2453
- throw e;
3515
+ // throw e;
3516
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2454
3517
  }
2455
3518
  finally {
2456
3519
  const endTime = Date.now();
2457
- this._reportToWorld(world, {
3520
+ _reportToWorld(world, {
2458
3521
  element_name: selectors.element_name,
2459
3522
  type: Types.GET_TABLE_DATA,
2460
3523
  text: "Get table data",
@@ -2464,7 +3527,7 @@ class StableBrowser {
2464
3527
  status: "FAILED",
2465
3528
  startTime,
2466
3529
  endTime,
2467
- message: error === null || error === void 0 ? void 0 : error.message,
3530
+ message: error?.message,
2468
3531
  }
2469
3532
  : {
2470
3533
  status: "PASSED",
@@ -2476,7 +3539,7 @@ class StableBrowser {
2476
3539
  }
2477
3540
  }
2478
3541
  async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
2479
- this._validateSelectors(selectors);
3542
+ _validateSelectors(selectors);
2480
3543
  if (!query) {
2481
3544
  throw new Error("query is null");
2482
3545
  }
@@ -2509,7 +3572,7 @@ class StableBrowser {
2509
3572
  info.operation = "analyzeTable";
2510
3573
  info.selectors = selectors;
2511
3574
  info.query = query;
2512
- query = this._fixUsingParams(query, _params);
3575
+ query = _fixUsingParams(query, _params);
2513
3576
  info.query_fixed = query;
2514
3577
  info.operator = operator;
2515
3578
  info.value = value;
@@ -2615,11 +3678,12 @@ class StableBrowser {
2615
3678
  info.screenshotPath = screenshotPath;
2616
3679
  Object.assign(e, { info: info });
2617
3680
  error = e;
2618
- throw e;
3681
+ // throw e;
3682
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2619
3683
  }
2620
3684
  finally {
2621
3685
  const endTime = Date.now();
2622
- this._reportToWorld(world, {
3686
+ _reportToWorld(world, {
2623
3687
  element_name: selectors.element_name,
2624
3688
  type: Types.ANALYZE_TABLE,
2625
3689
  text: "Analyze table",
@@ -2629,7 +3693,7 @@ class StableBrowser {
2629
3693
  status: "FAILED",
2630
3694
  startTime,
2631
3695
  endTime,
2632
- message: error === null || error === void 0 ? void 0 : error.message,
3696
+ message: error?.message,
2633
3697
  }
2634
3698
  : {
2635
3699
  status: "PASSED",
@@ -2640,28 +3704,51 @@ class StableBrowser {
2640
3704
  });
2641
3705
  }
2642
3706
  }
2643
- async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2644
- if (!value) {
2645
- return value;
2646
- }
2647
- // find all the accurance of {{(.*?)}} and replace with the value
2648
- let regex = /{{(.*?)}}/g;
2649
- let matches = value.match(regex);
2650
- if (matches) {
2651
- const testData = this.getTestData(world);
2652
- for (let i = 0; i < matches.length; i++) {
2653
- let match = matches[i];
2654
- let key = match.substring(2, match.length - 2);
2655
- let newValue = objectPath.get(testData, key, null);
2656
- if (newValue !== null) {
2657
- value = value.replace(match, newValue);
2658
- }
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");
2659
3733
  }
3734
+ await new Promise((resolve) => setTimeout(resolve, duration));
3735
+ return state.info;
3736
+ }
3737
+ catch (e) {
3738
+ await _commandError(state, e, this);
2660
3739
  }
2661
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2662
- 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;
2663
3751
  }
2664
- return value;
2665
3752
  }
2666
3753
  _getLoadTimeout(options) {
2667
3754
  let timeout = 15000;
@@ -2673,6 +3760,37 @@ class StableBrowser {
2673
3760
  }
2674
3761
  return timeout;
2675
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
+ }
2676
3794
  async waitForPageLoad(options = {}, world = null) {
2677
3795
  let timeout = this._getLoadTimeout(options);
2678
3796
  const promiseArray = [];
@@ -2706,13 +3824,12 @@ class StableBrowser {
2706
3824
  else if (e.label === "domcontentloaded") {
2707
3825
  console.log("waited for the domcontent loaded timeout");
2708
3826
  }
2709
- console.log(".");
2710
3827
  }
2711
3828
  finally {
2712
3829
  await new Promise((resolve) => setTimeout(resolve, 2000));
2713
3830
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2714
3831
  const endTime = Date.now();
2715
- this._reportToWorld(world, {
3832
+ _reportToWorld(world, {
2716
3833
  type: Types.GET_PAGE_STATUS,
2717
3834
  text: "Wait for page load",
2718
3835
  screenshotId,
@@ -2721,7 +3838,7 @@ class StableBrowser {
2721
3838
  status: "FAILED",
2722
3839
  startTime,
2723
3840
  endTime,
2724
- message: error === null || error === void 0 ? void 0 : error.message,
3841
+ message: error?.message,
2725
3842
  }
2726
3843
  : {
2727
3844
  status: "PASSED",
@@ -2732,41 +3849,122 @@ class StableBrowser {
2732
3849
  }
2733
3850
  }
2734
3851
  async closePage(options = {}, world = null) {
2735
- const startTime = Date.now();
2736
- let error = null;
2737
- let screenshotId = null;
2738
- let screenshotPath = null;
2739
- 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
+ };
2740
3865
  try {
3866
+ await _preCommand(state, this);
2741
3867
  await this.page.close();
2742
3868
  }
2743
3869
  catch (e) {
2744
- console.log(".");
3870
+ await _commandError(state, e, this);
2745
3871
  }
2746
3872
  finally {
2747
- await new Promise((resolve) => setTimeout(resolve, 2000));
2748
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2749
- const endTime = Date.now();
2750
- this._reportToWorld(world, {
2751
- type: Types.CLOSE_PAGE,
2752
- text: "close page",
2753
- screenshotId,
2754
- result: error
2755
- ? {
2756
- status: "FAILED",
2757
- startTime,
2758
- endTime,
2759
- 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;
2760
3908
  }
2761
- : {
2762
- status: "PASSED",
2763
- startTime,
2764
- endTime,
2765
- },
2766
- info: info,
2767
- });
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);
2768
3961
  }
2769
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
+ }
2770
3968
  async setViewportSize(width, hight, options = {}, world = null) {
2771
3969
  const startTime = Date.now();
2772
3970
  let error = null;
@@ -2783,22 +3981,23 @@ class StableBrowser {
2783
3981
  await this.page.setViewportSize({ width: width, height: hight });
2784
3982
  }
2785
3983
  catch (e) {
2786
- console.log(".");
3984
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2787
3985
  }
2788
3986
  finally {
2789
3987
  await new Promise((resolve) => setTimeout(resolve, 2000));
2790
3988
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2791
3989
  const endTime = Date.now();
2792
- this._reportToWorld(world, {
3990
+ _reportToWorld(world, {
2793
3991
  type: Types.SET_VIEWPORT,
2794
3992
  text: "set viewport size to " + width + "x" + hight,
3993
+ _text: "Set the viewport size to " + width + "x" + hight,
2795
3994
  screenshotId,
2796
3995
  result: error
2797
3996
  ? {
2798
3997
  status: "FAILED",
2799
3998
  startTime,
2800
3999
  endTime,
2801
- message: error === null || error === void 0 ? void 0 : error.message,
4000
+ message: error?.message,
2802
4001
  }
2803
4002
  : {
2804
4003
  status: "PASSED",
@@ -2819,13 +4018,13 @@ class StableBrowser {
2819
4018
  await this.page.reload();
2820
4019
  }
2821
4020
  catch (e) {
2822
- console.log(".");
4021
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2823
4022
  }
2824
4023
  finally {
2825
4024
  await new Promise((resolve) => setTimeout(resolve, 2000));
2826
4025
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2827
4026
  const endTime = Date.now();
2828
- this._reportToWorld(world, {
4027
+ _reportToWorld(world, {
2829
4028
  type: Types.GET_PAGE_STATUS,
2830
4029
  text: "page relaod",
2831
4030
  screenshotId,
@@ -2834,7 +4033,7 @@ class StableBrowser {
2834
4033
  status: "FAILED",
2835
4034
  startTime,
2836
4035
  endTime,
2837
- message: error === null || error === void 0 ? void 0 : error.message,
4036
+ message: error?.message,
2838
4037
  }
2839
4038
  : {
2840
4039
  status: "PASSED",
@@ -2861,11 +4060,216 @@ class StableBrowser {
2861
4060
  console.log("#-#");
2862
4061
  }
2863
4062
  }
2864
- _reportToWorld(world, properties) {
2865
- if (!world || !world.attach) {
2866
- 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
+ }
2867
4248
  }
2868
- 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);
2869
4273
  }
2870
4274
  }
2871
4275
  function createTimedPromise(promise, label) {
@@ -2873,156 +4277,5 @@ function createTimedPromise(promise, label) {
2873
4277
  .then((result) => ({ status: "fulfilled", label, result }))
2874
4278
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2875
4279
  }
2876
- const KEYBOARD_EVENTS = [
2877
- "ALT",
2878
- "AltGraph",
2879
- "CapsLock",
2880
- "Control",
2881
- "Fn",
2882
- "FnLock",
2883
- "Hyper",
2884
- "Meta",
2885
- "NumLock",
2886
- "ScrollLock",
2887
- "Shift",
2888
- "Super",
2889
- "Symbol",
2890
- "SymbolLock",
2891
- "Enter",
2892
- "Tab",
2893
- "ArrowDown",
2894
- "ArrowLeft",
2895
- "ArrowRight",
2896
- "ArrowUp",
2897
- "End",
2898
- "Home",
2899
- "PageDown",
2900
- "PageUp",
2901
- "Backspace",
2902
- "Clear",
2903
- "Copy",
2904
- "CrSel",
2905
- "Cut",
2906
- "Delete",
2907
- "EraseEof",
2908
- "ExSel",
2909
- "Insert",
2910
- "Paste",
2911
- "Redo",
2912
- "Undo",
2913
- "Accept",
2914
- "Again",
2915
- "Attn",
2916
- "Cancel",
2917
- "ContextMenu",
2918
- "Escape",
2919
- "Execute",
2920
- "Find",
2921
- "Finish",
2922
- "Help",
2923
- "Pause",
2924
- "Play",
2925
- "Props",
2926
- "Select",
2927
- "ZoomIn",
2928
- "ZoomOut",
2929
- "BrightnessDown",
2930
- "BrightnessUp",
2931
- "Eject",
2932
- "LogOff",
2933
- "Power",
2934
- "PowerOff",
2935
- "PrintScreen",
2936
- "Hibernate",
2937
- "Standby",
2938
- "WakeUp",
2939
- "AllCandidates",
2940
- "Alphanumeric",
2941
- "CodeInput",
2942
- "Compose",
2943
- "Convert",
2944
- "Dead",
2945
- "FinalMode",
2946
- "GroupFirst",
2947
- "GroupLast",
2948
- "GroupNext",
2949
- "GroupPrevious",
2950
- "ModeChange",
2951
- "NextCandidate",
2952
- "NonConvert",
2953
- "PreviousCandidate",
2954
- "Process",
2955
- "SingleCandidate",
2956
- "HangulMode",
2957
- "HanjaMode",
2958
- "JunjaMode",
2959
- "Eisu",
2960
- "Hankaku",
2961
- "Hiragana",
2962
- "HiraganaKatakana",
2963
- "KanaMode",
2964
- "KanjiMode",
2965
- "Katakana",
2966
- "Romaji",
2967
- "Zenkaku",
2968
- "ZenkakuHanaku",
2969
- "F1",
2970
- "F2",
2971
- "F3",
2972
- "F4",
2973
- "F5",
2974
- "F6",
2975
- "F7",
2976
- "F8",
2977
- "F9",
2978
- "F10",
2979
- "F11",
2980
- "F12",
2981
- "Soft1",
2982
- "Soft2",
2983
- "Soft3",
2984
- "Soft4",
2985
- "ChannelDown",
2986
- "ChannelUp",
2987
- "Close",
2988
- "MailForward",
2989
- "MailReply",
2990
- "MailSend",
2991
- "MediaFastForward",
2992
- "MediaPause",
2993
- "MediaPlay",
2994
- "MediaPlayPause",
2995
- "MediaRecord",
2996
- "MediaRewind",
2997
- "MediaStop",
2998
- "MediaTrackNext",
2999
- "MediaTrackPrevious",
3000
- "AudioBalanceLeft",
3001
- "AudioBalanceRight",
3002
- "AudioBassBoostDown",
3003
- "AudioBassBoostToggle",
3004
- "AudioBassBoostUp",
3005
- "AudioFaderFront",
3006
- "AudioFaderRear",
3007
- "AudioSurroundModeNext",
3008
- "AudioTrebleDown",
3009
- "AudioTrebleUp",
3010
- "AudioVolumeDown",
3011
- "AudioVolumeMute",
3012
- "AudioVolumeUp",
3013
- "MicrophoneToggle",
3014
- "MicrophoneVolumeDown",
3015
- "MicrophoneVolumeMute",
3016
- "MicrophoneVolumeUp",
3017
- "TV",
3018
- "TV3DMode",
3019
- "TVAntennaCable",
3020
- "TVAudioDescription",
3021
- ];
3022
- function unEscapeString(str) {
3023
- const placeholder = "__NEWLINE__";
3024
- str = str.replace(new RegExp(placeholder, "g"), "\n");
3025
- return str;
3026
- }
3027
4280
  export { StableBrowser };
3028
4281
  //# sourceMappingURL=stable_browser.js.map