automation_model 1.0.457-dev → 1.0.457

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