automation_model 1.0.449-dev → 1.0.449

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