automation_model 1.0.448-dev → 1.0.448

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