automation_model 1.0.434-dev → 1.0.434

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 (60) hide show
  1. package/README.md +130 -0
  2. package/lib/api.d.ts +43 -1
  3. package/lib/api.js +241 -41
  4. package/lib/api.js.map +1 -1
  5. package/lib/auto_page.d.ts +5 -2
  6. package/lib/auto_page.js +177 -46
  7. package/lib/auto_page.js.map +1 -1
  8. package/lib/browser_manager.d.ts +7 -3
  9. package/lib/browser_manager.js +157 -49
  10. package/lib/browser_manager.js.map +1 -1
  11. package/lib/bruno.d.ts +2 -0
  12. package/lib/bruno.js +374 -0
  13. package/lib/bruno.js.map +1 -0
  14. package/lib/command_common.d.ts +6 -0
  15. package/lib/command_common.js +198 -0
  16. package/lib/command_common.js.map +1 -0
  17. package/lib/environment.d.ts +3 -0
  18. package/lib/environment.js +5 -2
  19. package/lib/environment.js.map +1 -1
  20. package/lib/error-messages.d.ts +6 -0
  21. package/lib/error-messages.js +206 -0
  22. package/lib/error-messages.js.map +1 -0
  23. package/lib/generation_scripts.d.ts +4 -0
  24. package/lib/generation_scripts.js +2 -0
  25. package/lib/generation_scripts.js.map +1 -0
  26. package/lib/index.d.ts +2 -0
  27. package/lib/index.js +2 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/init_browser.d.ts +5 -2
  30. package/lib/init_browser.js +124 -7
  31. package/lib/init_browser.js.map +1 -1
  32. package/lib/locate_element.d.ts +7 -0
  33. package/lib/locate_element.js +215 -0
  34. package/lib/locate_element.js.map +1 -0
  35. package/lib/locator.d.ts +36 -0
  36. package/lib/locator.js +165 -0
  37. package/lib/locator.js.map +1 -1
  38. package/lib/locator_log.d.ts +26 -0
  39. package/lib/locator_log.js +69 -0
  40. package/lib/locator_log.js.map +1 -0
  41. package/lib/network.d.ts +3 -0
  42. package/lib/network.js +183 -0
  43. package/lib/network.js.map +1 -0
  44. package/lib/scripts/axe.mini.js +12 -0
  45. package/lib/stable_browser.d.ts +103 -36
  46. package/lib/stable_browser.js +1766 -1246
  47. package/lib/stable_browser.js.map +1 -1
  48. package/lib/table.d.ts +13 -0
  49. package/lib/table.js +187 -0
  50. package/lib/table.js.map +1 -0
  51. package/lib/table_helper.d.ts +19 -0
  52. package/lib/table_helper.js +116 -0
  53. package/lib/table_helper.js.map +1 -0
  54. package/lib/test_context.d.ts +6 -0
  55. package/lib/test_context.js +14 -10
  56. package/lib/test_context.js.map +1 -1
  57. package/lib/utils.d.ts +22 -2
  58. package/lib/utils.js +669 -11
  59. package/lib/utils.js.map +1 -1
  60. package/package.json +14 -8
@@ -2,21 +2,31 @@
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, } from "./utils.js";
15
14
  import csv from "csv-parser";
16
15
  import { Readable } from "node:stream";
17
16
  import readline from "readline";
18
- 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 { loadBrunoParams } from "./bruno.js";
27
+ export const Types = {
19
28
  CLICK: "click_element",
29
+ WAIT_ELEMENT: "wait_element",
20
30
  NAVIGATE: "navigate",
21
31
  FILL: "fill_element",
22
32
  EXECUTE: "execute_page_method",
@@ -26,6 +36,8 @@ const Types = {
26
36
  GET_PAGE_STATUS: "get_page_status",
27
37
  CLICK_ROW_ACTION: "click_row_action",
28
38
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
39
+ VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
40
+ VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
29
41
  ANALYZE_TABLE: "analyze_table",
30
42
  SELECT: "select_combobox",
31
43
  VERIFY_PAGE_PATH: "verify_page_path",
@@ -36,21 +48,41 @@ const Types = {
36
48
  UNCHECK: "uncheck_element",
37
49
  EXTRACT: "extract_attribute",
38
50
  CLOSE_PAGE: "close_page",
51
+ TABLE_OPERATION: "table_operation",
39
52
  SET_DATE_TIME: "set_date_time",
40
53
  SET_VIEWPORT: "set_viewport",
41
54
  VERIFY_VISUAL: "verify_visual",
42
55
  LOAD_DATA: "load_data",
43
56
  SET_INPUT: "set_input",
57
+ WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
58
+ VERIFY_ATTRIBUTE: "verify_element_attribute",
59
+ VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
60
+ BRUNO: "bruno",
61
+ };
62
+ export const apps = {};
63
+ const formatElementName = (elementName) => {
64
+ return elementName ? JSON.stringify(elementName) : "element";
44
65
  };
45
66
  class StableBrowser {
46
- constructor(browser, page, logger = null, context = null) {
67
+ browser;
68
+ page;
69
+ logger;
70
+ context;
71
+ world;
72
+ project_path = null;
73
+ webLogFile = null;
74
+ networkLogger = null;
75
+ configuration = null;
76
+ appName = "main";
77
+ tags = null;
78
+ isRecording = false;
79
+ initSnapshotTaken = false;
80
+ constructor(browser, page, logger = null, context = null, world = null) {
47
81
  this.browser = browser;
48
82
  this.page = page;
49
83
  this.logger = logger;
50
84
  this.context = context;
51
- this.project_path = null;
52
- this.webLogFile = null;
53
- this.configuration = null;
85
+ this.world = world;
54
86
  if (!this.logger) {
55
87
  this.logger = console;
56
88
  }
@@ -75,17 +107,32 @@ class StableBrowser {
75
107
  catch (e) {
76
108
  this.logger.error("unable to read ai_config.json");
77
109
  }
78
- const logFolder = path.join(this.project_path, "logs", "web");
79
- this.webLogFile = this.getWebLogFile(logFolder);
80
- this.registerConsoleLogListener(page, context, this.webLogFile);
81
- this.registerRequestListener();
82
- context.pages = [this.page];
83
110
  context.pageLoading = { status: false };
111
+ context.pages = [this.page];
112
+ const logFolder = path.join(this.project_path, "logs", "web");
113
+ this.world = world;
114
+ this.registerEventListeners(this.context);
115
+ registerNetworkEvents(this.world, this, this.context, this.page);
116
+ registerDownloadEvent(this.page, this.world, this.context);
117
+ }
118
+ registerEventListeners(context) {
119
+ this.registerConsoleLogListener(this.page, context);
120
+ // this.registerRequestListener(this.page, context, this.webLogFile);
121
+ if (!context.pageLoading) {
122
+ context.pageLoading = { status: false };
123
+ }
84
124
  context.playContext.on("page", async function (page) {
125
+ if (this.configuration && this.configuration.closePopups === true) {
126
+ console.log("close unexpected popups");
127
+ await page.close();
128
+ return;
129
+ }
85
130
  context.pageLoading.status = true;
86
131
  this.page = page;
87
132
  context.page = page;
88
133
  context.pages.push(page);
134
+ registerNetworkEvents(this.world, this, context, this.page);
135
+ registerDownloadEvent(this.page, this.world, context);
89
136
  page.on("close", async () => {
90
137
  if (this.context && this.context.pages && this.context.pages.length > 1) {
91
138
  this.context.pages.pop();
@@ -110,117 +157,164 @@ class StableBrowser {
110
157
  context.pageLoading.status = false;
111
158
  }.bind(this));
112
159
  }
113
- getWebLogFile(logFolder) {
114
- if (!fs.existsSync(logFolder)) {
115
- fs.mkdirSync(logFolder, { recursive: true });
160
+ async switchApp(appName) {
161
+ // check if the current app (this.appName) is the same as the new app
162
+ if (this.appName === appName) {
163
+ return;
164
+ }
165
+ let newContextCreated = false;
166
+ if (!apps[appName]) {
167
+ let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
168
+ newContextCreated = true;
169
+ apps[appName] = {
170
+ context: newContext,
171
+ browser: newContext.browser,
172
+ page: newContext.page,
173
+ };
174
+ }
175
+ const tempContext = {};
176
+ _copyContext(this, tempContext);
177
+ _copyContext(apps[appName], this);
178
+ apps[this.appName] = tempContext;
179
+ this.appName = appName;
180
+ if (newContextCreated) {
181
+ this.registerEventListeners(this.context);
182
+ await this.goto(this.context.environment.baseUrl);
183
+ await this.waitForPageLoad();
184
+ }
185
+ }
186
+ async switchTab(tabTitleOrIndex) {
187
+ // first check if the tabNameOrIndex is a number
188
+ let index = parseInt(tabTitleOrIndex);
189
+ if (!isNaN(index)) {
190
+ if (index >= 0 && index < this.context.pages.length) {
191
+ this.page = this.context.pages[index];
192
+ this.context.page = this.page;
193
+ await this.page.bringToFront();
194
+ return;
195
+ }
116
196
  }
117
- let nextIndex = 1;
118
- while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
119
- nextIndex++;
197
+ // if the tabNameOrIndex is a string, find the tab by name
198
+ for (let i = 0; i < this.context.pages.length; i++) {
199
+ let page = this.context.pages[i];
200
+ let title = await page.title();
201
+ if (title.includes(tabTitleOrIndex)) {
202
+ this.page = page;
203
+ this.context.page = this.page;
204
+ await this.page.bringToFront();
205
+ return;
206
+ }
120
207
  }
121
- const fileName = nextIndex + ".json";
122
- return path.join(logFolder, fileName);
208
+ throw new Error("Tab not found: " + tabTitleOrIndex);
123
209
  }
124
- registerConsoleLogListener(page, context, logFile) {
210
+ registerConsoleLogListener(page, context) {
125
211
  if (!this.context.webLogger) {
126
212
  this.context.webLogger = [];
127
213
  }
128
214
  page.on("console", async (msg) => {
129
- this.context.webLogger.push({
215
+ const obj = {
130
216
  type: msg.type(),
131
217
  text: msg.text(),
132
218
  location: msg.location(),
133
219
  time: new Date().toISOString(),
134
- });
135
- await fs.promises.writeFile(logFile, JSON.stringify(this.context.webLogger, null, 2));
220
+ };
221
+ this.context.webLogger.push(obj);
222
+ if (msg.type() === "error") {
223
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
224
+ }
136
225
  });
137
226
  }
138
- registerRequestListener() {
139
- this.page.on("request", async (data) => {
227
+ registerRequestListener(page, context, logFile) {
228
+ if (!this.context.networkLogger) {
229
+ this.context.networkLogger = [];
230
+ }
231
+ page.on("request", async (data) => {
232
+ const startTime = new Date().getTime();
140
233
  try {
141
- const pageUrl = new URL(this.page.url());
234
+ const pageUrl = new URL(page.url());
142
235
  const requestUrl = new URL(data.url());
143
236
  if (pageUrl.hostname === requestUrl.hostname) {
144
237
  const method = data.method();
145
- if (method === "POST" || method === "GET" || method === "PUT" || method === "DELETE" || method === "PATCH") {
238
+ if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
146
239
  const token = await data.headerValue("Authorization");
147
240
  if (token) {
148
- this.context.authtoken = token;
241
+ context.authtoken = token;
149
242
  }
150
243
  }
151
244
  }
245
+ const response = await data.response();
246
+ const endTime = new Date().getTime();
247
+ const obj = {
248
+ url: data.url(),
249
+ method: data.method(),
250
+ postData: data.postData(),
251
+ error: data.failure() ? data.failure().errorText : null,
252
+ duration: endTime - startTime,
253
+ startTime,
254
+ };
255
+ context.networkLogger.push(obj);
256
+ this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
152
257
  }
153
258
  catch (error) {
154
- console.error("Error in request listener", error);
259
+ // console.error("Error in request listener", error);
260
+ context.networkLogger.push({
261
+ error: "not able to listen",
262
+ message: error.message,
263
+ stack: error.stack,
264
+ time: new Date().toISOString(),
265
+ });
266
+ // await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
155
267
  }
156
268
  });
157
269
  }
158
270
  // async closeUnexpectedPopups() {
159
271
  // await closeUnexpectedPopups(this.page);
160
272
  // }
161
- async goto(url) {
273
+ async goto(url, world = null) {
274
+ if (!url) {
275
+ throw new Error("url is null, verify that the environment file is correct");
276
+ }
162
277
  if (!url.startsWith("http")) {
163
278
  url = "https://" + url;
164
279
  }
165
- await this.page.goto(url, {
166
- timeout: 60000,
167
- });
168
- }
169
- _validateSelectors(selectors) {
170
- if (!selectors) {
171
- throw new Error("selectors is null");
172
- }
173
- if (!selectors.locators) {
174
- throw new Error("selectors.locators is null");
175
- }
176
- if (!Array.isArray(selectors.locators)) {
177
- throw new Error("selectors.locators expected to be array");
178
- }
179
- if (selectors.locators.length === 0) {
180
- throw new Error("selectors.locators expected to be non empty array");
181
- }
182
- }
183
- _fixUsingParams(text, _params) {
184
- if (!_params || typeof text !== "string") {
185
- return text;
280
+ const state = {
281
+ value: url,
282
+ world: world,
283
+ type: Types.NAVIGATE,
284
+ text: `Navigate Page to: ${url}`,
285
+ operation: "goto",
286
+ log: "***** navigate page to " + url + " *****\n",
287
+ info: {},
288
+ locate: false,
289
+ scroll: false,
290
+ screenshot: false,
291
+ highlight: false,
292
+ };
293
+ try {
294
+ await _preCommand(state, this);
295
+ await this.page.goto(url, {
296
+ timeout: 60000,
297
+ });
298
+ await _screenshot(state, this);
186
299
  }
187
- for (let key in _params) {
188
- let regValue = key;
189
- if (key.startsWith("_")) {
190
- // remove the _ prefix
191
- regValue = key.substring(1);
192
- }
193
- text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
300
+ catch (error) {
301
+ console.error("Error on goto", error);
302
+ _commandError(state, error, this);
194
303
  }
195
- return text;
196
- }
197
- _fixLocatorUsingParams(locator, _params) {
198
- // check if not null
199
- if (!locator) {
200
- return locator;
304
+ finally {
305
+ await _commandFinally(state, this);
201
306
  }
202
- // clone the locator
203
- locator = JSON.parse(JSON.stringify(locator));
204
- this.scanAndManipulate(locator, _params);
205
- return locator;
206
307
  }
207
- _isObject(value) {
208
- return value && typeof value === "object" && value.constructor === Object;
209
- }
210
- scanAndManipulate(currentObj, _params) {
211
- for (const key in currentObj) {
212
- if (typeof currentObj[key] === "string") {
213
- // Perform string manipulation
214
- currentObj[key] = this._fixUsingParams(currentObj[key], _params);
215
- }
216
- else if (this._isObject(currentObj[key])) {
217
- // Recursively scan nested objects
218
- this.scanAndManipulate(currentObj[key], _params);
308
+ async _getLocator(locator, scope, _params) {
309
+ locator = _fixLocatorUsingParams(locator, _params);
310
+ // locator = await this._replaceWithLocalData(locator);
311
+ for (let key in locator) {
312
+ if (typeof locator[key] !== "string")
313
+ continue;
314
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
315
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
219
316
  }
220
317
  }
221
- }
222
- _getLocator(locator, scope, _params) {
223
- locator = this._fixLocatorUsingParams(locator, _params);
224
318
  let locatorReturn;
225
319
  if (locator.role) {
226
320
  if (locator.role[1].nameReg) {
@@ -228,7 +322,7 @@ class StableBrowser {
228
322
  delete locator.role[1].nameReg;
229
323
  }
230
324
  // if (locator.role[1].name) {
231
- // locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
325
+ // locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
232
326
  // }
233
327
  locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
234
328
  }
@@ -247,7 +341,7 @@ class StableBrowser {
247
341
  locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
248
342
  }
249
343
  }
250
- if (locator === null || locator === void 0 ? void 0 : locator.engine) {
344
+ if (locator?.engine) {
251
345
  if (locator.engine === "css") {
252
346
  locatorReturn = scope.locator(locator.selector);
253
347
  }
@@ -268,192 +362,181 @@ class StableBrowser {
268
362
  return locatorReturn;
269
363
  }
270
364
  async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
271
- let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
365
+ if (css && css.locator) {
366
+ css = css.locator;
367
+ }
368
+ let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
272
369
  if (result.elementCount === 0) {
273
370
  return;
274
371
  }
275
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
372
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
276
373
  // css climb to parent element
277
374
  const climbArray = [];
278
375
  for (let i = 0; i < climb; i++) {
279
376
  climbArray.push("..");
280
377
  }
281
378
  let climbXpath = "xpath=" + climbArray.join("/");
282
- return textElementCss + " >> " + climbXpath + " >> " + css;
283
- }
284
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
285
- //const stringifyText = JSON.stringify(text);
286
- return await scope.evaluate(([text, tag, regex, partial]) => {
287
- function isParent(parent, child) {
288
- let currentNode = child.parentNode;
289
- while (currentNode !== null) {
290
- if (currentNode === parent) {
291
- return true;
292
- }
293
- currentNode = currentNode.parentNode;
294
- }
295
- return false;
296
- }
297
- document.isParent = isParent;
298
- function collectAllShadowDomElements(element, result = []) {
299
- // Check and add the element if it has a shadow root
300
- if (element.shadowRoot) {
301
- result.push(element);
302
- // Also search within the shadow root
303
- document.collectAllShadowDomElements(element.shadowRoot, result);
304
- }
305
- // Iterate over child nodes
306
- element.childNodes.forEach((child) => {
307
- // Recursively call the function for each child node
308
- document.collectAllShadowDomElements(child, result);
309
- });
310
- return result;
311
- }
312
- document.collectAllShadowDomElements = collectAllShadowDomElements;
313
- if (!tag) {
314
- tag = "*";
315
- }
316
- let elements = Array.from(document.querySelectorAll(tag));
317
- let shadowHosts = [];
318
- document.collectAllShadowDomElements(document, shadowHosts);
319
- for (let i = 0; i < shadowHosts.length; i++) {
320
- let shadowElement = shadowHosts[i].shadowRoot;
321
- if (!shadowElement) {
322
- console.log("shadowElement is null, for host " + shadowHosts[i]);
323
- continue;
324
- }
325
- let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
326
- elements = elements.concat(shadowElements);
327
- }
328
- let randomToken = null;
329
- const foundElements = [];
330
- if (regex) {
331
- let regexpSearch = new RegExp(text, "im");
332
- for (let i = 0; i < elements.length; i++) {
333
- const element = elements[i];
334
- if ((element.innerText && regexpSearch.test(element.innerText)) ||
335
- (element.value && regexpSearch.test(element.value))) {
336
- foundElements.push(element);
337
- }
338
- }
339
- }
340
- else {
341
- text = text.trim();
342
- for (let i = 0; i < elements.length; i++) {
343
- const element = elements[i];
344
- if (partial) {
345
- if ((element.innerText && element.innerText.trim().includes(text)) ||
346
- (element.value && element.value.includes(text))) {
347
- foundElements.push(element);
348
- }
349
- }
350
- else {
351
- if ((element.innerText && element.innerText.trim() === text) ||
352
- (element.value && element.value === text)) {
353
- foundElements.push(element);
354
- }
355
- }
356
- }
357
- }
358
- let noChildElements = [];
359
- for (let i = 0; i < foundElements.length; i++) {
360
- let element = foundElements[i];
361
- let hasChild = false;
362
- for (let j = 0; j < foundElements.length; j++) {
363
- if (i === j) {
364
- continue;
365
- }
366
- if (isParent(element, foundElements[j])) {
367
- hasChild = true;
368
- break;
369
- }
370
- }
371
- if (!hasChild) {
372
- noChildElements.push(element);
373
- }
374
- }
375
- let elementCount = 0;
376
- if (noChildElements.length > 0) {
377
- for (let i = 0; i < noChildElements.length; i++) {
378
- if (randomToken === null) {
379
- randomToken = Math.random().toString(36).substring(7);
380
- }
381
- let element = noChildElements[i];
382
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
383
- elementCount++;
384
- }
379
+ let resultCss = textElementCss + " >> " + climbXpath;
380
+ if (css) {
381
+ resultCss = resultCss + " >> " + css;
382
+ }
383
+ return resultCss;
384
+ }
385
+ async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
386
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
387
+ const locator = scope.locator(query);
388
+ const count = await locator.count();
389
+ if (!tag1) {
390
+ tag1 = "*";
391
+ }
392
+ const randomToken = Math.random().toString(36).substring(7);
393
+ let tagCount = 0;
394
+ for (let i = 0; i < count; i++) {
395
+ const element = locator.nth(i);
396
+ // check if the tag matches
397
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
398
+ if (!tag.startsWith("*")) {
399
+ if (el.tagName.toLowerCase() !== tag) {
400
+ return false;
401
+ }
402
+ }
403
+ if (!el.setAttribute) {
404
+ el = el.parentElement;
405
+ }
406
+ // remove any attributes start with data-blinq-id
407
+ // for (let i = 0; i < el.attributes.length; i++) {
408
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
409
+ // el.removeAttribute(el.attributes[i].name);
410
+ // }
411
+ // }
412
+ el.setAttribute("data-blinq-id-" + randomToken, "");
413
+ return true;
414
+ }, [tag1, randomToken]))) {
415
+ continue;
385
416
  }
386
- return { elementCount: elementCount, randomToken: randomToken };
387
- }, [text1, tag1, regex1, partial1]);
417
+ tagCount++;
418
+ }
419
+ return { elementCount: tagCount, randomToken };
388
420
  }
389
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
421
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
422
+ if (!info) {
423
+ info = {};
424
+ }
425
+ if (!info.failCause) {
426
+ info.failCause = {};
427
+ }
428
+ if (!info.log) {
429
+ info.log = "";
430
+ info.locatorLog = new LocatorLog(selectorHierarchy);
431
+ }
390
432
  let locatorSearch = selectorHierarchy[index];
433
+ let originalLocatorSearch = "";
434
+ try {
435
+ originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
436
+ locatorSearch = JSON.parse(originalLocatorSearch);
437
+ }
438
+ catch (e) {
439
+ console.error(e);
440
+ }
391
441
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
392
442
  let locator = null;
393
443
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
394
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
444
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
445
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
395
446
  if (!locatorString) {
447
+ info.failCause.textNotFound = true;
448
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
396
449
  return;
397
450
  }
398
- locator = this._getLocator({ css: locatorString }, scope, _params);
451
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
399
452
  }
400
453
  else if (locatorSearch.text) {
401
- let result = await this._locateElementByText(scope, this._fixUsingParams(locatorSearch.text, _params), locatorSearch.tag, false, locatorSearch.partial === true, _params);
454
+ let text = _fixUsingParams(locatorSearch.text, _params);
455
+ let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
402
456
  if (result.elementCount === 0) {
457
+ info.failCause.textNotFound = true;
458
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
403
459
  return;
404
460
  }
405
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
461
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
406
462
  if (locatorSearch.childCss) {
407
463
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
408
464
  }
409
- locator = this._getLocator(locatorSearch, scope, _params);
465
+ locator = await this._getLocator(locatorSearch, scope, _params);
410
466
  }
411
467
  else {
412
- locator = this._getLocator(locatorSearch, scope, _params);
468
+ locator = await this._getLocator(locatorSearch, scope, _params);
413
469
  }
414
470
  // let cssHref = false;
415
471
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
416
472
  // cssHref = true;
417
473
  // }
418
474
  let count = await locator.count();
475
+ if (count > 0 && !info.failCause.count) {
476
+ info.failCause.count = count;
477
+ }
419
478
  //info.log += "total elements found " + count + "\n";
420
479
  //let visibleCount = 0;
421
480
  let visibleLocator = null;
422
- if (locatorSearch.index && locatorSearch.index < count) {
481
+ if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
423
482
  foundLocators.push(locator.nth(locatorSearch.index));
483
+ if (info.locatorLog) {
484
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
485
+ }
424
486
  return;
425
487
  }
488
+ if (info.locatorLog && count === 0) {
489
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
490
+ }
426
491
  for (let j = 0; j < count; j++) {
427
492
  let visible = await locator.nth(j).isVisible();
428
493
  const enabled = await locator.nth(j).isEnabled();
429
494
  if (!visibleOnly) {
430
495
  visible = true;
431
496
  }
432
- if (visible && enabled) {
497
+ if (visible && (allowDisabled || enabled)) {
433
498
  foundLocators.push(locator.nth(j));
499
+ if (info.locatorLog) {
500
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
501
+ }
434
502
  }
435
503
  else {
504
+ info.failCause.visible = visible;
505
+ info.failCause.enabled = enabled;
436
506
  if (!info.printMessages) {
437
507
  info.printMessages = {};
438
508
  }
509
+ if (info.locatorLog && !visible) {
510
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
511
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
512
+ }
513
+ if (info.locatorLog && !enabled) {
514
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
515
+ info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
516
+ }
439
517
  if (!info.printMessages[j.toString()]) {
440
- info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
518
+ //info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
441
519
  info.printMessages[j.toString()] = true;
442
520
  }
443
521
  }
444
522
  }
445
523
  }
446
524
  async closeUnexpectedPopups(info, _params) {
525
+ if (!info) {
526
+ info = {};
527
+ info.failCause = {};
528
+ info.log = "";
529
+ }
447
530
  if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
448
531
  if (!info) {
449
532
  info = {};
450
533
  }
451
- info.log += "scan for popup handlers" + "\n";
534
+ //info.log += "scan for popup handlers" + "\n";
452
535
  const handlerGroup = [];
453
536
  for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
454
537
  handlerGroup.push(this.configuration.popupHandlers[i].locator);
455
538
  }
456
- const scopes = [this.page, ...this.page.frames()];
539
+ const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
457
540
  let result = null;
458
541
  let scope = null;
459
542
  for (let i = 0; i < scopes.length; i++) {
@@ -475,53 +558,108 @@ class StableBrowser {
475
558
  }
476
559
  if (result.foundElements.length > 0) {
477
560
  let dialogCloseLocator = result.foundElements[0].locator;
478
- await dialogCloseLocator.click();
561
+ try {
562
+ await scope?.evaluate(() => {
563
+ window.__isClosingPopups = true;
564
+ });
565
+ await dialogCloseLocator.click();
566
+ // wait for the dialog to close
567
+ await dialogCloseLocator.waitFor({ state: "hidden" });
568
+ }
569
+ catch (e) {
570
+ }
571
+ finally {
572
+ await scope?.evaluate(() => {
573
+ window.__isClosingPopups = false;
574
+ });
575
+ }
479
576
  return { rerun: true };
480
577
  }
481
578
  }
482
579
  }
483
580
  return { rerun: false };
484
581
  }
485
- async _locate(selectors, info, _params, timeout = 30000) {
582
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
583
+ if (!timeout) {
584
+ timeout = 30000;
585
+ }
486
586
  for (let i = 0; i < 3; i++) {
487
587
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
488
588
  for (let j = 0; j < selectors.locators.length; j++) {
489
589
  let selector = selectors.locators[j];
490
590
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
491
591
  }
492
- let element = await this._locate_internal(selectors, info, _params, timeout);
592
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
493
593
  if (!element.rerun) {
494
- return element;
594
+ const randomToken = Math.random().toString(36).substring(7);
595
+ element.evaluate((el, randomToken) => {
596
+ el.setAttribute("data-blinq-id-" + randomToken, "");
597
+ }, randomToken);
598
+ // if (element._frame) {
599
+ // return element;
600
+ // }
601
+ const scope = element._frame ?? element.page();
602
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
603
+ let prefixSelector = "";
604
+ const frameControlSelector = " >> internal:control=enter-frame";
605
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
606
+ if (frameSelectorIndex !== -1) {
607
+ // remove everything after the >> internal:control=enter-frame
608
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
609
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
610
+ }
611
+ // if (element?._frame?._selector) {
612
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
613
+ // }
614
+ const newSelector = prefixSelector + newElementSelector;
615
+ return scope.locator(newSelector);
495
616
  }
496
617
  }
497
618
  throw new Error("unable to locate element " + JSON.stringify(selectors));
498
619
  }
499
- async _locate_internal(selectors, info, _params, timeout = 30000) {
500
- let highPriorityTimeout = 5000;
501
- let visibleOnlyTimeout = 6000;
502
- let startTime = performance.now();
503
- let locatorsCount = 0;
504
- //let arrayMode = Array.isArray(selectors);
620
+ async _findFrameScope(selectors, timeout = 30000, info) {
621
+ if (!info) {
622
+ info = {};
623
+ info.failCause = {};
624
+ info.log = "";
625
+ }
626
+ let startTime = Date.now();
505
627
  let scope = this.page;
628
+ if (selectors.frame) {
629
+ return selectors.frame;
630
+ }
506
631
  if (selectors.iframe_src || selectors.frameLocators) {
507
- const findFrame = (frame, framescope) => {
632
+ const findFrame = async (frame, framescope) => {
508
633
  for (let i = 0; i < frame.selectors.length; i++) {
509
634
  let frameLocator = frame.selectors[i];
510
635
  if (frameLocator.css) {
511
- framescope = framescope.frameLocator(frameLocator.css);
512
- break;
636
+ let testframescope = framescope.frameLocator(frameLocator.css);
637
+ if (frameLocator.index) {
638
+ testframescope = framescope.nth(frameLocator.index);
639
+ }
640
+ try {
641
+ await testframescope.owner().evaluateHandle(() => true, null, {
642
+ timeout: 5000,
643
+ });
644
+ framescope = testframescope;
645
+ break;
646
+ }
647
+ catch (error) {
648
+ console.error("frame not found " + frameLocator.css);
649
+ }
513
650
  }
514
651
  }
515
652
  if (frame.children) {
516
- return findFrame(frame.children, framescope);
653
+ return await findFrame(frame.children, framescope);
517
654
  }
518
655
  return framescope;
519
656
  };
520
- info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
657
+ let fLocator = null;
521
658
  while (true) {
522
659
  let frameFound = false;
523
660
  if (selectors.nestFrmLoc) {
524
- scope = findFrame(selectors.nestFrmLoc, scope);
661
+ fLocator = selectors.nestFrmLoc;
662
+ scope = await findFrame(selectors.nestFrmLoc, scope);
525
663
  frameFound = true;
526
664
  break;
527
665
  }
@@ -529,6 +667,7 @@ class StableBrowser {
529
667
  for (let i = 0; i < selectors.frameLocators.length; i++) {
530
668
  let frameLocator = selectors.frameLocators[i];
531
669
  if (frameLocator.css) {
670
+ fLocator = frameLocator.css;
532
671
  scope = scope.frameLocator(frameLocator.css);
533
672
  frameFound = true;
534
673
  break;
@@ -536,20 +675,55 @@ class StableBrowser {
536
675
  }
537
676
  }
538
677
  if (!frameFound && selectors.iframe_src) {
678
+ fLocator = selectors.iframe_src;
539
679
  scope = this.page.frame({ url: selectors.iframe_src });
540
680
  }
541
681
  if (!scope) {
542
- info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
543
- if (performance.now() - startTime > timeout) {
682
+ if (info && info.locatorLog) {
683
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
684
+ }
685
+ //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
686
+ if (Date.now() - startTime > timeout) {
687
+ info.failCause.iframeNotFound = true;
688
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
544
689
  throw new Error("unable to locate iframe " + selectors.iframe_src);
545
690
  }
546
691
  await new Promise((resolve) => setTimeout(resolve, 1000));
547
692
  }
548
693
  else {
694
+ if (info && info.locatorLog) {
695
+ info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
696
+ }
549
697
  break;
550
698
  }
551
699
  }
552
700
  }
701
+ if (!scope) {
702
+ scope = this.page;
703
+ }
704
+ return scope;
705
+ }
706
+ async _getDocumentBody(selectors, timeout = 30000, info) {
707
+ let scope = await this._findFrameScope(selectors, timeout, info);
708
+ return scope.evaluate(() => {
709
+ var bodyContent = document.body.innerHTML;
710
+ return bodyContent;
711
+ });
712
+ }
713
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
714
+ if (!info) {
715
+ info = {};
716
+ info.failCause = {};
717
+ info.log = "";
718
+ info.locatorLog = new LocatorLog(selectors);
719
+ }
720
+ let highPriorityTimeout = 5000;
721
+ let visibleOnlyTimeout = 6000;
722
+ let startTime = Date.now();
723
+ let locatorsCount = 0;
724
+ let lazy_scroll = false;
725
+ //let arrayMode = Array.isArray(selectors);
726
+ let scope = await this._findFrameScope(selectors, timeout, info);
553
727
  let selectorsLocators = null;
554
728
  selectorsLocators = selectors.locators;
555
729
  // group selectors by priority
@@ -585,18 +759,13 @@ class StableBrowser {
585
759
  }
586
760
  // info.log += "scanning locators in priority 1" + "\n";
587
761
  let onlyPriority3 = selectorsLocators[0].priority === 3;
588
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
762
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
589
763
  if (result.foundElements.length === 0) {
590
764
  // info.log += "scanning locators in priority 2" + "\n";
591
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
592
- }
593
- if (result.foundElements.length === 0 && onlyPriority3) {
594
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
765
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
595
766
  }
596
- else {
597
- if (result.foundElements.length === 0 && !highPriorityOnly) {
598
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
599
- }
767
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
768
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
600
769
  }
601
770
  let foundElements = result.foundElements;
602
771
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -636,24 +805,43 @@ class StableBrowser {
636
805
  return maxCountElement.locator;
637
806
  }
638
807
  }
639
- if (performance.now() - startTime > timeout) {
808
+ if (Date.now() - startTime > timeout) {
640
809
  break;
641
810
  }
642
- if (performance.now() - startTime > highPriorityTimeout) {
643
- info.log += "high priority timeout, will try all elements" + "\n";
811
+ if (Date.now() - startTime > highPriorityTimeout) {
812
+ //info.log += "high priority timeout, will try all elements" + "\n";
644
813
  highPriorityOnly = false;
814
+ if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
815
+ lazy_scroll = true;
816
+ await scrollPageToLoadLazyElements(this.page);
817
+ }
645
818
  }
646
- if (performance.now() - startTime > visibleOnlyTimeout) {
647
- info.log += "visible only timeout, will try all elements" + "\n";
819
+ if (Date.now() - startTime > visibleOnlyTimeout) {
820
+ //info.log += "visible only timeout, will try all elements" + "\n";
648
821
  visibleOnly = false;
649
822
  }
650
823
  await new Promise((resolve) => setTimeout(resolve, 1000));
824
+ // sheck of more of half of the timeout has passed
825
+ if (Date.now() - startTime > timeout / 2) {
826
+ highPriorityOnly = false;
827
+ visibleOnly = false;
828
+ }
651
829
  }
652
830
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
653
- info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
831
+ // if (info.locatorLog) {
832
+ // const lines = info.locatorLog.toString().split("\n");
833
+ // for (let line of lines) {
834
+ // this.logger.debug(line);
835
+ // }
836
+ // }
837
+ //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
838
+ info.failCause.locatorNotFound = true;
839
+ if (!info?.failCause?.lastError) {
840
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
841
+ }
654
842
  throw new Error("failed to locate first element no elements found, " + info.log);
655
843
  }
656
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
844
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
657
845
  let foundElements = [];
658
846
  const result = {
659
847
  foundElements: foundElements,
@@ -661,14 +849,15 @@ class StableBrowser {
661
849
  for (let i = 0; i < locatorsGroup.length; i++) {
662
850
  let foundLocators = [];
663
851
  try {
664
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
852
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
665
853
  }
666
854
  catch (e) {
667
- this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
668
- this.logger.debug(e);
855
+ // this call can fail it the browser is navigating
856
+ // this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
857
+ // this.logger.debug(e);
669
858
  foundLocators = [];
670
859
  try {
671
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
860
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
672
861
  }
673
862
  catch (e) {
674
863
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -682,90 +871,228 @@ class StableBrowser {
682
871
  });
683
872
  result.locatorIndex = i;
684
873
  }
874
+ if (foundLocators.length > 1) {
875
+ // remove elements that consume the same space with 10 pixels tolerance
876
+ const boxes = [];
877
+ for (let j = 0; j < foundLocators.length; j++) {
878
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
879
+ }
880
+ for (let j = 0; j < boxes.length; j++) {
881
+ for (let k = 0; k < boxes.length; k++) {
882
+ if (j === k) {
883
+ continue;
884
+ }
885
+ // check if x, y, width, height are the same with 10 pixels tolerance
886
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
887
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
888
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
889
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
890
+ // as the element is not unique, will remove it
891
+ boxes.splice(k, 1);
892
+ k--;
893
+ }
894
+ }
895
+ }
896
+ if (boxes.length === 1) {
897
+ result.foundElements.push({
898
+ locator: boxes[0].locator.first(),
899
+ box: boxes[0].box,
900
+ unique: true,
901
+ });
902
+ result.locatorIndex = i;
903
+ }
904
+ else {
905
+ info.failCause.foundMultiple = true;
906
+ if (info.locatorLog) {
907
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
908
+ }
909
+ }
910
+ }
685
911
  }
686
912
  return result;
687
913
  }
688
- async click(selectors, _params, options = {}, world = null) {
689
- this._validateSelectors(selectors);
914
+ async simpleClick(elementDescription, _params, options = {}, world = null) {
915
+ const state = {
916
+ locate: false,
917
+ scroll: false,
918
+ highlight: false,
919
+ _params,
920
+ options,
921
+ world,
922
+ type: Types.CLICK,
923
+ text: "Click element",
924
+ operation: "simpleClick",
925
+ log: "***** click on " + elementDescription + " *****\n",
926
+ };
927
+ _preCommand(state, this);
690
928
  const startTime = Date.now();
691
- if (options && options.context) {
692
- selectors.locators[0].text = options.context;
929
+ let timeout = 30000;
930
+ if (options && options.timeout) {
931
+ timeout = options.timeout;
693
932
  }
694
- const info = {};
695
- info.log = "***** click on " + selectors.element_name + " *****\n";
696
- info.operation = "click";
697
- info.selectors = selectors;
698
- let error = null;
699
- let screenshotId = null;
700
- let screenshotPath = null;
701
- try {
702
- let element = await this._locate(selectors, info, _params);
703
- await this.scrollIfNeeded(element, info);
704
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
933
+ while (true) {
705
934
  try {
706
- await this._highlightElements(element);
707
- await element.click();
708
- await new Promise((resolve) => setTimeout(resolve, 1000));
935
+ const result = await locate_element(this.context, elementDescription, "click");
936
+ if (result?.elementNumber >= 0) {
937
+ const selectors = {
938
+ frame: result?.frame,
939
+ locators: [
940
+ {
941
+ css: result?.css,
942
+ },
943
+ ],
944
+ };
945
+ await this.click(selectors, _params, options, world);
946
+ return;
947
+ }
709
948
  }
710
949
  catch (e) {
711
- // await this.closeUnexpectedPopups();
712
- info.log += "click failed, will try again" + "\n";
713
- element = await this._locate(selectors, info, _params);
714
- await element.dispatchEvent("click");
715
- await new Promise((resolve) => setTimeout(resolve, 1000));
950
+ if (performance.now() - startTime > timeout) {
951
+ // throw e;
952
+ try {
953
+ await _commandError(state, "timeout looking for " + elementDescription, this);
954
+ }
955
+ finally {
956
+ await _commandFinally(state, this);
957
+ }
958
+ }
716
959
  }
960
+ await new Promise((resolve) => setTimeout(resolve, 3000));
961
+ }
962
+ }
963
+ async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
964
+ const state = {
965
+ locate: false,
966
+ scroll: false,
967
+ highlight: false,
968
+ _params,
969
+ options,
970
+ world,
971
+ type: Types.FILL,
972
+ text: "Fill element",
973
+ operation: "simpleClickType",
974
+ log: "***** click type on " + elementDescription + " *****\n",
975
+ };
976
+ _preCommand(state, this);
977
+ const startTime = Date.now();
978
+ let timeout = 30000;
979
+ if (options && options.timeout) {
980
+ timeout = options.timeout;
981
+ }
982
+ while (true) {
983
+ try {
984
+ const result = await locate_element(this.context, elementDescription, "fill", value);
985
+ if (result?.elementNumber >= 0) {
986
+ const selectors = {
987
+ frame: result?.frame,
988
+ locators: [
989
+ {
990
+ css: result?.css,
991
+ },
992
+ ],
993
+ };
994
+ await this.clickType(selectors, value, false, _params, options, world);
995
+ return;
996
+ }
997
+ }
998
+ catch (e) {
999
+ if (performance.now() - startTime > timeout) {
1000
+ // throw e;
1001
+ try {
1002
+ await _commandError(state, "timeout looking for " + elementDescription, this);
1003
+ }
1004
+ finally {
1005
+ await _commandFinally(state, this);
1006
+ }
1007
+ }
1008
+ }
1009
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1010
+ }
1011
+ }
1012
+ async click(selectors, _params, options = {}, world = null) {
1013
+ const state = {
1014
+ selectors,
1015
+ _params,
1016
+ options,
1017
+ world,
1018
+ text: "Click element",
1019
+ _text: "Click on " + selectors.element_name,
1020
+ type: Types.CLICK,
1021
+ operation: "click",
1022
+ log: "***** click on " + selectors.element_name + " *****\n",
1023
+ };
1024
+ try {
1025
+ await _preCommand(state, this);
1026
+ await performAction("click", state.element, options, this, state, _params);
717
1027
  await this.waitForPageLoad();
718
- return info;
1028
+ return state.info;
719
1029
  }
720
1030
  catch (e) {
721
- this.logger.error("click failed " + info.log);
722
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
723
- info.screenshotPath = screenshotPath;
724
- Object.assign(e, { info: info });
725
- error = e;
726
- throw e;
1031
+ await _commandError(state, e, this);
727
1032
  }
728
1033
  finally {
729
- const endTime = Date.now();
730
- this._reportToWorld(world, {
731
- element_name: selectors.element_name,
732
- type: Types.CLICK,
733
- text: `Click element`,
734
- screenshotId,
735
- result: error
736
- ? {
737
- status: "FAILED",
738
- startTime,
739
- endTime,
740
- message: error === null || error === void 0 ? void 0 : error.message,
741
- }
742
- : {
743
- status: "PASSED",
744
- startTime,
745
- endTime,
746
- },
747
- info: info,
748
- });
1034
+ await _commandFinally(state, this);
1035
+ }
1036
+ }
1037
+ async waitForElement(selectors, _params, options = {}, world = null) {
1038
+ const timeout = this._getFindElementTimeout(options);
1039
+ const state = {
1040
+ selectors,
1041
+ _params,
1042
+ options,
1043
+ world,
1044
+ text: "Wait for element",
1045
+ _text: "Wait for " + selectors.element_name,
1046
+ type: Types.WAIT_ELEMENT,
1047
+ operation: "waitForElement",
1048
+ log: "***** wait for " + selectors.element_name + " *****\n",
1049
+ };
1050
+ let found = false;
1051
+ try {
1052
+ await _preCommand(state, this);
1053
+ // if (state.options && state.options.context) {
1054
+ // state.selectors.locators[0].text = state.options.context;
1055
+ // }
1056
+ await state.element.waitFor({ timeout: timeout });
1057
+ found = true;
1058
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1059
+ }
1060
+ catch (e) {
1061
+ console.error("Error on waitForElement", e);
1062
+ // await _commandError(state, e, this);
749
1063
  }
1064
+ finally {
1065
+ await _commandFinally(state, this);
1066
+ }
1067
+ return found;
750
1068
  }
751
1069
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
752
- this._validateSelectors(selectors);
753
- const startTime = Date.now();
754
- const info = {};
755
- info.log = "";
756
- info.operation = "setCheck";
757
- info.checked = checked;
758
- info.selectors = selectors;
759
- let error = null;
760
- let screenshotId = null;
761
- let screenshotPath = null;
1070
+ const state = {
1071
+ selectors,
1072
+ _params,
1073
+ options,
1074
+ world,
1075
+ type: checked ? Types.CHECK : Types.UNCHECK,
1076
+ text: checked ? `Check element` : `Uncheck element`,
1077
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
1078
+ operation: "setCheck",
1079
+ log: "***** check " + selectors.element_name + " *****\n",
1080
+ };
762
1081
  try {
763
- let element = await this._locate(selectors, info, _params);
764
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1082
+ await _preCommand(state, this);
1083
+ state.info.checked = checked;
1084
+ // let element = await this._locate(selectors, info, _params);
1085
+ // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
765
1086
  try {
766
- await this._highlightElements(element);
767
- await element.setChecked(checked);
1087
+ // if (world && world.screenshot && !world.screenshotPath) {
1088
+ // console.log(`Highlighting while running from recorder`);
1089
+ await this._highlightElements(state.element);
1090
+ await state.element.setChecked(checked);
768
1091
  await new Promise((resolve) => setTimeout(resolve, 1000));
1092
+ // await this._unHighlightElements(element);
1093
+ // }
1094
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1095
+ // await this._unHighlightElements(element);
769
1096
  }
770
1097
  catch (e) {
771
1098
  if (e.message && e.message.includes("did not change its state")) {
@@ -773,179 +1100,102 @@ class StableBrowser {
773
1100
  }
774
1101
  else {
775
1102
  //await this.closeUnexpectedPopups();
776
- info.log += "setCheck failed, will try again" + "\n";
777
- element = await this._locate(selectors, info, _params);
778
- await element.setChecked(checked, { timeout: 5000, force: true });
1103
+ state.info.log += "setCheck failed, will try again" + "\n";
1104
+ state.element = await this._locate(selectors, state.info, _params);
1105
+ await state.element.setChecked(checked, { timeout: 5000, force: true });
779
1106
  await new Promise((resolve) => setTimeout(resolve, 1000));
780
1107
  }
781
1108
  }
782
1109
  await this.waitForPageLoad();
783
- return info;
1110
+ return state.info;
784
1111
  }
785
1112
  catch (e) {
786
- this.logger.error("setCheck failed " + info.log);
787
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
788
- info.screenshotPath = screenshotPath;
789
- Object.assign(e, { info: info });
790
- error = e;
791
- throw e;
1113
+ await _commandError(state, e, this);
792
1114
  }
793
1115
  finally {
794
- const endTime = Date.now();
795
- this._reportToWorld(world, {
796
- element_name: selectors.element_name,
797
- type: checked ? Types.CHECK : Types.UNCHECK,
798
- text: checked ? `Check element` : `Uncheck element`,
799
- screenshotId,
800
- result: error
801
- ? {
802
- status: "FAILED",
803
- startTime,
804
- endTime,
805
- message: error === null || error === void 0 ? void 0 : error.message,
806
- }
807
- : {
808
- status: "PASSED",
809
- startTime,
810
- endTime,
811
- },
812
- info: info,
813
- });
1116
+ await _commandFinally(state, this);
814
1117
  }
815
1118
  }
816
1119
  async hover(selectors, _params, options = {}, world = null) {
817
- this._validateSelectors(selectors);
818
- const startTime = Date.now();
819
- const info = {};
820
- info.log = "";
821
- info.operation = "hover";
822
- info.selectors = selectors;
823
- let error = null;
824
- let screenshotId = null;
825
- let screenshotPath = null;
1120
+ const state = {
1121
+ selectors,
1122
+ _params,
1123
+ options,
1124
+ world,
1125
+ type: Types.HOVER,
1126
+ text: `Hover element`,
1127
+ _text: `Hover on ${selectors.element_name}`,
1128
+ operation: "hover",
1129
+ log: "***** hover " + selectors.element_name + " *****\n",
1130
+ };
826
1131
  try {
827
- let element = await this._locate(selectors, info, _params);
828
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
829
- try {
830
- await this._highlightElements(element);
831
- await element.hover();
832
- await new Promise((resolve) => setTimeout(resolve, 1000));
833
- }
834
- catch (e) {
835
- //await this.closeUnexpectedPopups();
836
- info.log += "hover failed, will try again" + "\n";
837
- element = await this._locate(selectors, info, _params);
838
- await element.hover({ timeout: 10000 });
839
- await new Promise((resolve) => setTimeout(resolve, 1000));
840
- }
1132
+ await _preCommand(state, this);
1133
+ await performAction("hover", state.element, options, this, state, _params);
1134
+ await _screenshot(state, this);
841
1135
  await this.waitForPageLoad();
842
- return info;
1136
+ return state.info;
843
1137
  }
844
1138
  catch (e) {
845
- this.logger.error("hover failed " + info.log);
846
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
847
- info.screenshotPath = screenshotPath;
848
- Object.assign(e, { info: info });
849
- error = e;
850
- throw e;
1139
+ await _commandError(state, e, this);
851
1140
  }
852
1141
  finally {
853
- const endTime = Date.now();
854
- this._reportToWorld(world, {
855
- element_name: selectors.element_name,
856
- type: Types.HOVER,
857
- text: `Hover element`,
858
- screenshotId,
859
- result: error
860
- ? {
861
- status: "FAILED",
862
- startTime,
863
- endTime,
864
- message: error === null || error === void 0 ? void 0 : error.message,
865
- }
866
- : {
867
- status: "PASSED",
868
- startTime,
869
- endTime,
870
- },
871
- info: info,
872
- });
1142
+ await _commandFinally(state, this);
873
1143
  }
874
1144
  }
875
1145
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
876
- this._validateSelectors(selectors);
877
1146
  if (!values) {
878
1147
  throw new Error("values is null");
879
1148
  }
880
- const startTime = Date.now();
881
- let error = null;
882
- let screenshotId = null;
883
- let screenshotPath = null;
884
- const info = {};
885
- info.log = "";
886
- info.operation = "selectOptions";
887
- info.selectors = selectors;
1149
+ const state = {
1150
+ selectors,
1151
+ _params,
1152
+ options,
1153
+ world,
1154
+ value: values.toString(),
1155
+ type: Types.SELECT,
1156
+ text: `Select option: ${values}`,
1157
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1158
+ operation: "selectOption",
1159
+ log: "***** select option " + selectors.element_name + " *****\n",
1160
+ };
888
1161
  try {
889
- let element = await this._locate(selectors, info, _params);
890
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1162
+ await _preCommand(state, this);
891
1163
  try {
892
- await this._highlightElements(element);
893
- await element.selectOption(values);
1164
+ await state.element.selectOption(values);
894
1165
  }
895
1166
  catch (e) {
896
1167
  //await this.closeUnexpectedPopups();
897
- info.log += "selectOption failed, will try force" + "\n";
898
- await element.selectOption(values, { timeout: 10000, force: true });
1168
+ state.info.log += "selectOption failed, will try force" + "\n";
1169
+ await state.element.selectOption(values, { timeout: 10000, force: true });
899
1170
  }
900
1171
  await this.waitForPageLoad();
901
- return info;
1172
+ return state.info;
902
1173
  }
903
1174
  catch (e) {
904
- this.logger.error("selectOption failed " + info.log);
905
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
906
- info.screenshotPath = screenshotPath;
907
- Object.assign(e, { info: info });
908
- this.logger.info("click failed, will try next selector");
909
- error = e;
910
- throw e;
1175
+ await _commandError(state, e, this);
911
1176
  }
912
1177
  finally {
913
- const endTime = Date.now();
914
- this._reportToWorld(world, {
915
- element_name: selectors.element_name,
916
- type: Types.SELECT,
917
- text: `Select option: ${values}`,
918
- value: values.toString(),
919
- screenshotId,
920
- result: error
921
- ? {
922
- status: "FAILED",
923
- startTime,
924
- endTime,
925
- message: error === null || error === void 0 ? void 0 : error.message,
926
- }
927
- : {
928
- status: "PASSED",
929
- startTime,
930
- endTime,
931
- },
932
- info: info,
933
- });
1178
+ await _commandFinally(state, this);
934
1179
  }
935
1180
  }
936
1181
  async type(_value, _params = null, options = {}, world = null) {
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 = "type";
944
- _value = this._fixUsingParams(_value, _params);
945
- info.value = _value;
1182
+ const state = {
1183
+ value: _value,
1184
+ _params,
1185
+ options,
1186
+ world,
1187
+ locate: false,
1188
+ scroll: false,
1189
+ highlight: false,
1190
+ type: Types.TYPE_PRESS,
1191
+ text: `Type value: ${_value}`,
1192
+ _text: `Type value: ${_value}`,
1193
+ operation: "type",
1194
+ log: "",
1195
+ };
946
1196
  try {
947
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
948
- const valueSegment = _value.split("&&");
1197
+ await _preCommand(state, this);
1198
+ const valueSegment = state.value.split("&&");
949
1199
  for (let i = 0; i < valueSegment.length; i++) {
950
1200
  if (i > 0) {
951
1201
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -965,134 +1215,77 @@ class StableBrowser {
965
1215
  await this.page.keyboard.type(value);
966
1216
  }
967
1217
  }
968
- return info;
1218
+ return state.info;
969
1219
  }
970
1220
  catch (e) {
971
- //await this.closeUnexpectedPopups();
972
- this.logger.error("type failed " + info.log);
973
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
974
- info.screenshotPath = screenshotPath;
975
- Object.assign(e, { info: info });
976
- error = e;
977
- throw e;
1221
+ await _commandError(state, e, this);
978
1222
  }
979
1223
  finally {
980
- const endTime = Date.now();
981
- this._reportToWorld(world, {
982
- type: Types.TYPE_PRESS,
983
- screenshotId,
984
- value: _value,
985
- text: `type value: ${_value}`,
986
- result: error
987
- ? {
988
- status: "FAILED",
989
- startTime,
990
- endTime,
991
- message: error === null || error === void 0 ? void 0 : error.message,
992
- }
993
- : {
994
- status: "PASSED",
995
- startTime,
996
- endTime,
997
- },
998
- info: info,
999
- });
1224
+ await _commandFinally(state, this);
1000
1225
  }
1001
1226
  }
1002
1227
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
1003
- // set input value for non fillable inputs like date, time, range, color, etc.
1004
- this._validateSelectors(selectors);
1005
- const startTime = Date.now();
1006
- const info = {};
1007
- info.log = "***** set input value " + selectors.element_name + " *****\n";
1008
- info.operation = "setInputValue";
1009
- info.selectors = selectors;
1010
- value = this._fixUsingParams(value, _params);
1011
- info.value = value;
1012
- let error = null;
1013
- let screenshotId = null;
1014
- let screenshotPath = null;
1228
+ const state = {
1229
+ selectors,
1230
+ _params,
1231
+ value,
1232
+ options,
1233
+ world,
1234
+ type: Types.SET_INPUT,
1235
+ text: `Set input value`,
1236
+ operation: "setInputValue",
1237
+ log: "***** set input value " + selectors.element_name + " *****\n",
1238
+ };
1015
1239
  try {
1016
- value = await this._replaceWithLocalData(value, this);
1017
- let element = await this._locate(selectors, info, _params);
1018
- await this.scrollIfNeeded(element, info);
1019
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1020
- await this._highlightElements(element);
1240
+ await _preCommand(state, this);
1241
+ let value = await this._replaceWithLocalData(state.value, this);
1021
1242
  try {
1022
- await element.evaluateHandle((el, value) => {
1243
+ await state.element.evaluateHandle((el, value) => {
1023
1244
  el.value = value;
1024
1245
  }, value);
1025
1246
  }
1026
1247
  catch (error) {
1027
1248
  this.logger.error("setInputValue failed, will try again");
1028
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1029
- info.screenshotPath = screenshotPath;
1030
- Object.assign(error, { info: info });
1031
- await element.evaluateHandle((el, value) => {
1249
+ await _screenshot(state, this);
1250
+ Object.assign(error, { info: state.info });
1251
+ await state.element.evaluateHandle((el, value) => {
1032
1252
  el.value = value;
1033
1253
  });
1034
1254
  }
1035
1255
  }
1036
1256
  catch (e) {
1037
- this.logger.error("setInputValue failed " + info.log);
1038
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1039
- info.screenshotPath = screenshotPath;
1040
- Object.assign(e, { info: info });
1041
- error = e;
1042
- throw e;
1257
+ await _commandError(state, e, this);
1043
1258
  }
1044
1259
  finally {
1045
- const endTime = Date.now();
1046
- this._reportToWorld(world, {
1047
- element_name: selectors.element_name,
1048
- type: Types.SET_INPUT,
1049
- text: `Set input value`,
1050
- value: value,
1051
- screenshotId,
1052
- result: error
1053
- ? {
1054
- status: "FAILED",
1055
- startTime,
1056
- endTime,
1057
- message: error === null || error === void 0 ? void 0 : error.message,
1058
- }
1059
- : {
1060
- status: "PASSED",
1061
- startTime,
1062
- endTime,
1063
- },
1064
- info: info,
1065
- });
1260
+ await _commandFinally(state, this);
1066
1261
  }
1067
1262
  }
1068
1263
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
1069
- this._validateSelectors(selectors);
1070
- const startTime = Date.now();
1071
- let error = null;
1072
- let screenshotId = null;
1073
- let screenshotPath = null;
1074
- const info = {};
1075
- info.log = "";
1076
- info.operation = Types.SET_DATE_TIME;
1077
- info.selectors = selectors;
1078
- info.value = value;
1264
+ const state = {
1265
+ selectors,
1266
+ _params,
1267
+ value: await this._replaceWithLocalData(value, this),
1268
+ options,
1269
+ world,
1270
+ type: Types.SET_DATE_TIME,
1271
+ text: `Set date time value: ${value}`,
1272
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1273
+ operation: "setDateTime",
1274
+ log: "***** set date time value " + selectors.element_name + " *****\n",
1275
+ throwError: false,
1276
+ };
1079
1277
  try {
1080
- value = await this._replaceWithLocalData(value, this);
1081
- let element = await this._locate(selectors, info, _params);
1082
- //insert red border around the element
1083
- await this.scrollIfNeeded(element, info);
1084
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1085
- await this._highlightElements(element);
1278
+ await _preCommand(state, this);
1086
1279
  try {
1087
- await element.click();
1280
+ await performAction("click", state.element, options, this, state, _params);
1088
1281
  await new Promise((resolve) => setTimeout(resolve, 500));
1089
1282
  if (format) {
1090
- value = dayjs(value).format(format);
1091
- await element.fill(value);
1283
+ state.value = dayjs(state.value).format(format);
1284
+ await state.element.fill(state.value);
1092
1285
  }
1093
1286
  else {
1094
- const dateTimeValue = await getDateTimeValue({ value, element });
1095
- await element.evaluateHandle((el, dateTimeValue) => {
1287
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1288
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1096
1289
  el.value = ""; // clear input
1097
1290
  el.value = dateTimeValue;
1098
1291
  }, dateTimeValue);
@@ -1105,20 +1298,19 @@ class StableBrowser {
1105
1298
  }
1106
1299
  catch (err) {
1107
1300
  //await this.closeUnexpectedPopups();
1108
- this.logger.error("setting date time input failed " + JSON.stringify(info));
1301
+ this.logger.error("setting date time input failed " + JSON.stringify(state.info));
1109
1302
  this.logger.info("Trying again");
1110
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1111
- info.screenshotPath = screenshotPath;
1112
- Object.assign(err, { info: info });
1303
+ await _screenshot(state, this);
1304
+ Object.assign(err, { info: state.info });
1113
1305
  await element.click();
1114
1306
  await new Promise((resolve) => setTimeout(resolve, 500));
1115
1307
  if (format) {
1116
- value = dayjs(value).format(format);
1117
- await element.fill(value);
1308
+ state.value = dayjs(state.value).format(format);
1309
+ await state.element.fill(state.value);
1118
1310
  }
1119
1311
  else {
1120
- const dateTimeValue = await getDateTimeValue({ value, element });
1121
- await element.evaluateHandle((el, dateTimeValue) => {
1312
+ const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
1313
+ await state.element.evaluateHandle((el, dateTimeValue) => {
1122
1314
  el.value = ""; // clear input
1123
1315
  el.value = dateTimeValue;
1124
1316
  }, dateTimeValue);
@@ -1131,84 +1323,63 @@ class StableBrowser {
1131
1323
  }
1132
1324
  }
1133
1325
  catch (e) {
1134
- error = e;
1135
- throw e;
1326
+ await _commandError(state, e, this);
1136
1327
  }
1137
1328
  finally {
1138
- const endTime = Date.now();
1139
- this._reportToWorld(world, {
1140
- element_name: selectors.element_name,
1141
- type: Types.SET_DATE_TIME,
1142
- screenshotId,
1143
- value: value,
1144
- text: `setDateTime input with value: ${value}`,
1145
- result: error
1146
- ? {
1147
- status: "FAILED",
1148
- startTime,
1149
- endTime,
1150
- message: error === null || error === void 0 ? void 0 : error.message,
1151
- }
1152
- : {
1153
- status: "PASSED",
1154
- startTime,
1155
- endTime,
1156
- },
1157
- info: info,
1158
- });
1329
+ await _commandFinally(state, this);
1159
1330
  }
1160
1331
  }
1161
1332
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
1162
- this._validateSelectors(selectors);
1163
- const startTime = Date.now();
1164
- let error = null;
1165
- let screenshotId = null;
1166
- let screenshotPath = null;
1167
- const info = {};
1168
- info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
1169
- info.operation = "clickType";
1170
- info.selectors = selectors;
1333
+ _value = unEscapeString(_value);
1171
1334
  const newValue = await this._replaceWithLocalData(_value, world);
1335
+ const state = {
1336
+ selectors,
1337
+ _params,
1338
+ value: newValue,
1339
+ originalValue: _value,
1340
+ options,
1341
+ world,
1342
+ type: Types.FILL,
1343
+ text: `Click type input with value: ${_value}`,
1344
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1345
+ operation: "clickType",
1346
+ log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1347
+ };
1348
+ if (!options) {
1349
+ options = {};
1350
+ }
1172
1351
  if (newValue !== _value) {
1173
1352
  //this.logger.info(_value + "=" + newValue);
1174
1353
  _value = newValue;
1175
1354
  }
1176
- info.value = _value;
1177
1355
  try {
1178
- let element = await this._locate(selectors, info, _params);
1179
- //insert red border around the element
1180
- await this.scrollIfNeeded(element, info);
1181
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1182
- await this._highlightElements(element);
1183
- if (options === null || options === undefined || !options.press) {
1356
+ await _preCommand(state, this);
1357
+ state.info.value = _value;
1358
+ if (!options.press) {
1184
1359
  try {
1185
- let currentValue = await element.inputValue();
1360
+ let currentValue = await state.element.inputValue();
1186
1361
  if (currentValue) {
1187
- await element.fill("");
1362
+ await state.element.fill("");
1188
1363
  }
1189
1364
  }
1190
1365
  catch (e) {
1191
1366
  this.logger.info("unable to clear input value");
1192
1367
  }
1193
1368
  }
1194
- if (options === null || options === undefined || options.press) {
1195
- try {
1196
- await element.click({ timeout: 5000 });
1197
- }
1198
- catch (e) {
1199
- await element.dispatchEvent("click");
1200
- }
1369
+ if (options.press) {
1370
+ options.timeout = 5000;
1371
+ await performAction("click", state.element, options, this, state, _params);
1201
1372
  }
1202
1373
  else {
1203
1374
  try {
1204
- await element.focus();
1375
+ await state.element.focus();
1205
1376
  }
1206
1377
  catch (e) {
1207
- await element.dispatchEvent("focus");
1378
+ await state.element.dispatchEvent("focus");
1208
1379
  }
1209
1380
  }
1210
1381
  await new Promise((resolve) => setTimeout(resolve, 500));
1211
- const valueSegment = _value.split("&&");
1382
+ const valueSegment = state.value.split("&&");
1212
1383
  for (let i = 0; i < valueSegment.length; i++) {
1213
1384
  if (i > 0) {
1214
1385
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -1228,13 +1399,19 @@ class StableBrowser {
1228
1399
  await new Promise((resolve) => setTimeout(resolve, 500));
1229
1400
  }
1230
1401
  }
1402
+ await _screenshot(state, this);
1231
1403
  if (enter === true) {
1232
1404
  await new Promise((resolve) => setTimeout(resolve, 2000));
1233
1405
  await this.page.keyboard.press("Enter");
1234
1406
  await this.waitForPageLoad();
1235
1407
  }
1236
1408
  else if (enter === false) {
1237
- await element.dispatchEvent("change");
1409
+ try {
1410
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1411
+ }
1412
+ catch (e) {
1413
+ // ignore
1414
+ }
1238
1415
  //await this.page.keyboard.press("Tab");
1239
1416
  }
1240
1417
  else {
@@ -1243,111 +1420,60 @@ class StableBrowser {
1243
1420
  await this.waitForPageLoad();
1244
1421
  }
1245
1422
  }
1246
- return info;
1423
+ return state.info;
1247
1424
  }
1248
1425
  catch (e) {
1249
- //await this.closeUnexpectedPopups();
1250
- this.logger.error("fill failed " + JSON.stringify(info));
1251
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1252
- info.screenshotPath = screenshotPath;
1253
- Object.assign(e, { info: info });
1254
- error = e;
1255
- throw e;
1426
+ await _commandError(state, e, this);
1256
1427
  }
1257
1428
  finally {
1258
- const endTime = Date.now();
1259
- this._reportToWorld(world, {
1260
- element_name: selectors.element_name,
1261
- type: Types.FILL,
1262
- screenshotId,
1263
- value: _value,
1264
- text: `clickType input with value: ${_value}`,
1265
- result: error
1266
- ? {
1267
- status: "FAILED",
1268
- startTime,
1269
- endTime,
1270
- message: error === null || error === void 0 ? void 0 : error.message,
1271
- }
1272
- : {
1273
- status: "PASSED",
1274
- startTime,
1275
- endTime,
1276
- },
1277
- info: info,
1278
- });
1429
+ await _commandFinally(state, this);
1279
1430
  }
1280
1431
  }
1281
1432
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
1282
- this._validateSelectors(selectors);
1283
- const startTime = Date.now();
1284
- let error = null;
1285
- let screenshotId = null;
1286
- let screenshotPath = null;
1287
- const info = {};
1288
- info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
1289
- info.operation = "fill";
1290
- info.selectors = selectors;
1291
- info.value = value;
1433
+ const state = {
1434
+ selectors,
1435
+ _params,
1436
+ value: unEscapeString(value),
1437
+ options,
1438
+ world,
1439
+ type: Types.FILL,
1440
+ text: `Fill input with value: ${value}`,
1441
+ operation: "fill",
1442
+ log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
1443
+ };
1292
1444
  try {
1293
- let element = await this._locate(selectors, info, _params);
1294
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1295
- await this._highlightElements(element);
1296
- await element.fill(value);
1297
- await element.dispatchEvent("change");
1445
+ await _preCommand(state, this);
1446
+ await state.element.fill(value);
1447
+ await state.element.dispatchEvent("change");
1298
1448
  if (enter) {
1299
1449
  await new Promise((resolve) => setTimeout(resolve, 2000));
1300
1450
  await this.page.keyboard.press("Enter");
1301
1451
  }
1302
1452
  await this.waitForPageLoad();
1303
- return info;
1453
+ return state.info;
1304
1454
  }
1305
1455
  catch (e) {
1306
- //await this.closeUnexpectedPopups();
1307
- this.logger.error("fill failed " + info.log);
1308
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1309
- info.screenshotPath = screenshotPath;
1310
- Object.assign(e, { info: info });
1311
- error = e;
1312
- throw e;
1456
+ await _commandError(state, e, this);
1313
1457
  }
1314
1458
  finally {
1315
- const endTime = Date.now();
1316
- this._reportToWorld(world, {
1317
- element_name: selectors.element_name,
1318
- type: Types.FILL,
1319
- screenshotId,
1320
- value,
1321
- text: `Fill input with value: ${value}`,
1322
- result: error
1323
- ? {
1324
- status: "FAILED",
1325
- startTime,
1326
- endTime,
1327
- message: error === null || error === void 0 ? void 0 : error.message,
1328
- }
1329
- : {
1330
- status: "PASSED",
1331
- startTime,
1332
- endTime,
1333
- },
1334
- info: info,
1335
- });
1459
+ await _commandFinally(state, this);
1336
1460
  }
1337
1461
  }
1338
1462
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1339
1463
  return await this._getText(selectors, 0, _params, options, info, world);
1340
1464
  }
1341
1465
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1342
- this._validateSelectors(selectors);
1466
+ const timeout = this._getFindElementTimeout(options);
1467
+ _validateSelectors(selectors);
1343
1468
  let screenshotId = null;
1344
1469
  let screenshotPath = null;
1345
1470
  if (!info.log) {
1346
1471
  info.log = "";
1472
+ info.locatorLog = new LocatorLog(selectors);
1347
1473
  }
1348
1474
  info.operation = "getText";
1349
1475
  info.selectors = selectors;
1350
- let element = await this._locate(selectors, info, _params);
1476
+ let element = await this._locate(selectors, info, _params, timeout);
1351
1477
  if (climb > 0) {
1352
1478
  const climbArray = [];
1353
1479
  for (let i = 0; i < climb; i++) {
@@ -1366,6 +1492,18 @@ class StableBrowser {
1366
1492
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1367
1493
  try {
1368
1494
  await this._highlightElements(element);
1495
+ // if (world && world.screenshot && !world.screenshotPath) {
1496
+ // // console.log(`Highlighting for get text while running from recorder`);
1497
+ // this._highlightElements(element)
1498
+ // .then(async () => {
1499
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1500
+ // this._unhighlightElements(element).then(
1501
+ // () => {}
1502
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1503
+ // );
1504
+ // })
1505
+ // .catch(e);
1506
+ // }
1369
1507
  const elementText = await element.innerText();
1370
1508
  return {
1371
1509
  text: elementText,
@@ -1377,188 +1515,142 @@ class StableBrowser {
1377
1515
  }
1378
1516
  catch (e) {
1379
1517
  //await this.closeUnexpectedPopups();
1380
- this.logger.info("no innerText will use textContent");
1518
+ this.logger.info("no innerText, will use textContent");
1381
1519
  const elementText = await element.textContent();
1382
1520
  return { text: elementText, screenshotId, screenshotPath, value: value };
1383
1521
  }
1384
1522
  }
1385
1523
  async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
1386
- var _a;
1387
- this._validateSelectors(selectors);
1388
1524
  if (!pattern) {
1389
1525
  throw new Error("pattern is null");
1390
1526
  }
1391
1527
  if (!text) {
1392
1528
  throw new Error("text is null");
1393
1529
  }
1530
+ const state = {
1531
+ selectors,
1532
+ _params,
1533
+ pattern,
1534
+ value: pattern,
1535
+ options,
1536
+ world,
1537
+ locate: false,
1538
+ scroll: false,
1539
+ screenshot: false,
1540
+ highlight: false,
1541
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1542
+ text: `Verify element contains pattern: ${pattern}`,
1543
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1544
+ operation: "containsPattern",
1545
+ log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1546
+ };
1394
1547
  const newValue = await this._replaceWithLocalData(text, world);
1395
1548
  if (newValue !== text) {
1396
1549
  this.logger.info(text + "=" + newValue);
1397
1550
  text = newValue;
1398
1551
  }
1399
- const startTime = Date.now();
1400
- let error = null;
1401
- let screenshotId = null;
1402
- let screenshotPath = null;
1403
- const info = {};
1404
- info.log =
1405
- "***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
1406
- info.operation = "containsPattern";
1407
- info.selectors = selectors;
1408
- info.value = text;
1409
- info.pattern = pattern;
1410
1552
  let foundObj = null;
1411
1553
  try {
1412
- foundObj = await this._getText(selectors, 0, _params, options, info, world);
1554
+ await _preCommand(state, this);
1555
+ state.info.pattern = pattern;
1556
+ foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
1413
1557
  if (foundObj && foundObj.element) {
1414
- await this.scrollIfNeeded(foundObj.element, info);
1558
+ await this.scrollIfNeeded(foundObj.element, state.info);
1415
1559
  }
1416
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1560
+ await _screenshot(state, this);
1417
1561
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1418
1562
  pattern = pattern.replace("{text}", escapedText);
1419
1563
  let regex = new RegExp(pattern, "im");
1420
- 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))) {
1421
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1564
+ if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
1565
+ state.info.foundText = foundObj?.text;
1422
1566
  throw new Error("element doesn't contain text " + text);
1423
1567
  }
1424
- return info;
1568
+ return state.info;
1425
1569
  }
1426
1570
  catch (e) {
1427
- //await this.closeUnexpectedPopups();
1428
- this.logger.error("verify element contains text failed " + info.log);
1429
- this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
1430
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1431
- info.screenshotPath = screenshotPath;
1432
- Object.assign(e, { info: info });
1433
- error = e;
1434
- throw e;
1571
+ this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
1572
+ await _commandError(state, e, this);
1435
1573
  }
1436
1574
  finally {
1437
- const endTime = Date.now();
1438
- this._reportToWorld(world, {
1439
- element_name: selectors.element_name,
1440
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1441
- value: pattern,
1442
- text: `Verify element contains pattern: ${pattern}`,
1443
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1444
- result: error
1445
- ? {
1446
- status: "FAILED",
1447
- startTime,
1448
- endTime,
1449
- message: error === null || error === void 0 ? void 0 : error.message,
1450
- }
1451
- : {
1452
- status: "PASSED",
1453
- startTime,
1454
- endTime,
1455
- },
1456
- info: info,
1457
- });
1575
+ await _commandFinally(state, this);
1458
1576
  }
1459
1577
  }
1460
1578
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1461
- var _a, _b, _c;
1462
- this._validateSelectors(selectors);
1579
+ const timeout = this._getFindElementTimeout(options);
1580
+ const startTime = Date.now();
1581
+ const state = {
1582
+ selectors,
1583
+ _params,
1584
+ value: text,
1585
+ options,
1586
+ world,
1587
+ locate: false,
1588
+ scroll: false,
1589
+ screenshot: false,
1590
+ highlight: false,
1591
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1592
+ text: `Verify element contains text: ${text}`,
1593
+ operation: "containsText",
1594
+ log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
1595
+ };
1463
1596
  if (!text) {
1464
1597
  throw new Error("text is null");
1465
1598
  }
1466
- const startTime = Date.now();
1467
- let error = null;
1468
- let screenshotId = null;
1469
- let screenshotPath = null;
1470
- const info = {};
1471
- info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
1472
- info.operation = "containsText";
1473
- info.selectors = selectors;
1599
+ text = unEscapeString(text);
1474
1600
  const newValue = await this._replaceWithLocalData(text, world);
1475
1601
  if (newValue !== text) {
1476
1602
  this.logger.info(text + "=" + newValue);
1477
1603
  text = newValue;
1478
1604
  }
1479
- info.value = text;
1480
1605
  let foundObj = null;
1481
1606
  try {
1482
- foundObj = await this._getText(selectors, climb, _params, options, info, world);
1483
- if (foundObj && foundObj.element) {
1484
- await this.scrollIfNeeded(foundObj.element, info);
1485
- }
1486
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1487
- const dateAlternatives = findDateAlternatives(text);
1488
- const numberAlternatives = findNumberAlternatives(text);
1489
- if (dateAlternatives.date) {
1490
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1491
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(dateAlternatives.dates[i])) ||
1492
- ((_a = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _a === void 0 ? void 0 : _a.includes(dateAlternatives.dates[i]))) {
1493
- return info;
1607
+ while (Date.now() - startTime < timeout) {
1608
+ try {
1609
+ await _preCommand(state, this);
1610
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1611
+ if (foundObj && foundObj.element) {
1612
+ await this.scrollIfNeeded(foundObj.element, state.info);
1613
+ }
1614
+ await _screenshot(state, this);
1615
+ const dateAlternatives = findDateAlternatives(text);
1616
+ const numberAlternatives = findNumberAlternatives(text);
1617
+ if (dateAlternatives.date) {
1618
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1619
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1620
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1621
+ return state.info;
1622
+ }
1623
+ }
1494
1624
  }
1495
- }
1496
- throw new Error("element doesn't contain text " + text);
1497
- }
1498
- else if (numberAlternatives.number) {
1499
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1500
- if ((foundObj === null || foundObj === void 0 ? void 0 : foundObj.text.includes(numberAlternatives.numbers[i])) ||
1501
- ((_b = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value) === null || _b === void 0 ? void 0 : _b.includes(numberAlternatives.numbers[i]))) {
1502
- return info;
1625
+ else if (numberAlternatives.number) {
1626
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1627
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1628
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1629
+ return state.info;
1630
+ }
1631
+ }
1632
+ }
1633
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1634
+ return state.info;
1503
1635
  }
1504
1636
  }
1505
- throw new Error("element doesn't contain text " + text);
1506
- }
1507
- 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))) {
1508
- info.foundText = foundObj === null || foundObj === void 0 ? void 0 : foundObj.text;
1509
- info.value = foundObj === null || foundObj === void 0 ? void 0 : foundObj.value;
1510
- throw new Error("element doesn't contain text " + text);
1637
+ catch (e) {
1638
+ // Log error but continue retrying until timeout is reached
1639
+ this.logger.warn("Retrying containsText due to: " + e.message);
1640
+ }
1641
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1511
1642
  }
1512
- return info;
1643
+ state.info.foundText = foundObj?.text;
1644
+ state.info.value = foundObj?.value;
1645
+ throw new Error("element doesn't contain text " + text);
1513
1646
  }
1514
1647
  catch (e) {
1515
- //await this.closeUnexpectedPopups();
1516
- this.logger.error("verify element contains text failed " + info.log);
1517
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1518
- info.screenshotPath = screenshotPath;
1519
- Object.assign(e, { info: info });
1520
- error = e;
1648
+ await _commandError(state, e, this);
1521
1649
  throw e;
1522
1650
  }
1523
1651
  finally {
1524
- const endTime = Date.now();
1525
- this._reportToWorld(world, {
1526
- element_name: selectors.element_name,
1527
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1528
- text: `Verify element contains text: ${text}`,
1529
- value: text,
1530
- screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
1531
- result: error
1532
- ? {
1533
- status: "FAILED",
1534
- startTime,
1535
- endTime,
1536
- message: error === null || error === void 0 ? void 0 : error.message,
1537
- }
1538
- : {
1539
- status: "PASSED",
1540
- startTime,
1541
- endTime,
1542
- },
1543
- info: info,
1544
- });
1545
- }
1546
- }
1547
- _getDataFile(world = null) {
1548
- let dataFile = null;
1549
- if (world && world.reportFolder) {
1550
- dataFile = path.join(world.reportFolder, "data.json");
1551
- }
1552
- else if (this.reportFolder) {
1553
- dataFile = path.join(this.reportFolder, "data.json");
1554
- }
1555
- else if (this.context && this.context.reportFolder) {
1556
- dataFile = path.join(this.context.reportFolder, "data.json");
1652
+ await _commandFinally(state, this);
1557
1653
  }
1558
- else {
1559
- dataFile = "data.json";
1560
- }
1561
- return dataFile;
1562
1654
  }
1563
1655
  async waitForUserInput(message, world = null) {
1564
1656
  if (!message) {
@@ -1588,13 +1680,22 @@ class StableBrowser {
1588
1680
  return;
1589
1681
  }
1590
1682
  // if data file exists, load it
1591
- const dataFile = this._getDataFile(world);
1683
+ const dataFile = _getDataFile(world, this.context, this);
1592
1684
  let data = this.getTestData(world);
1593
1685
  // merge the testData with the existing data
1594
1686
  Object.assign(data, testData);
1595
1687
  // save the data to the file
1596
1688
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1597
1689
  }
1690
+ overwriteTestData(testData, world = null) {
1691
+ if (!testData) {
1692
+ return;
1693
+ }
1694
+ // if data file exists, load it
1695
+ const dataFile = _getDataFile(world, this.context, this);
1696
+ // save the data to the file
1697
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1698
+ }
1598
1699
  _getDataFilePath(fileName) {
1599
1700
  let dataFile = path.join(this.project_path, "data", fileName);
1600
1701
  if (fs.existsSync(dataFile)) {
@@ -1691,7 +1792,7 @@ class StableBrowser {
1691
1792
  }
1692
1793
  }
1693
1794
  getTestData(world = null) {
1694
- const dataFile = this._getDataFile(world);
1795
+ const dataFile = _getDataFile(world, this.context, this);
1695
1796
  let data = {};
1696
1797
  if (fs.existsSync(dataFile)) {
1697
1798
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1723,11 +1824,9 @@ class StableBrowser {
1723
1824
  if (!fs.existsSync(world.screenshotPath)) {
1724
1825
  fs.mkdirSync(world.screenshotPath, { recursive: true });
1725
1826
  }
1726
- let nextIndex = 1;
1727
- while (fs.existsSync(path.join(world.screenshotPath, nextIndex + ".png"))) {
1728
- nextIndex++;
1729
- }
1730
- const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
1827
+ // to make sure the path doesn't start with -
1828
+ const uuidStr = "id_" + randomUUID();
1829
+ const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1731
1830
  try {
1732
1831
  await this.takeScreenshot(screenshotPath);
1733
1832
  // let buffer = await this.page.screenshot({ timeout: 4000 });
@@ -1737,15 +1836,15 @@ class StableBrowser {
1737
1836
  // this.logger.info("unable to save screenshot " + screenshotPath);
1738
1837
  // }
1739
1838
  // });
1839
+ result.screenshotId = uuidStr;
1840
+ result.screenshotPath = screenshotPath;
1841
+ if (info && info.box) {
1842
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1843
+ }
1740
1844
  }
1741
1845
  catch (e) {
1742
1846
  this.logger.info("unable to take screenshot, ignored");
1743
1847
  }
1744
- result.screenshotId = nextIndex;
1745
- result.screenshotPath = screenshotPath;
1746
- if (info && info.box) {
1747
- await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1748
- }
1749
1848
  }
1750
1849
  else if (options && options.screenshot) {
1751
1850
  result.screenshotPath = options.screenshotPath;
@@ -1770,7 +1869,6 @@ class StableBrowser {
1770
1869
  }
1771
1870
  async takeScreenshot(screenshotPath) {
1772
1871
  const playContext = this.context.playContext;
1773
- const client = await playContext.newCDPSession(this.page);
1774
1872
  // Using CDP to capture the screenshot
1775
1873
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
1776
1874
  document.body.scrollWidth,
@@ -1780,164 +1878,198 @@ class StableBrowser {
1780
1878
  document.body.clientWidth,
1781
1879
  document.documentElement.clientWidth,
1782
1880
  ])));
1783
- const viewportHeight = Math.max(...(await this.page.evaluate(() => [
1784
- document.body.scrollHeight,
1785
- document.documentElement.scrollHeight,
1786
- document.body.offsetHeight,
1787
- document.documentElement.offsetHeight,
1788
- document.body.clientHeight,
1789
- document.documentElement.clientHeight,
1790
- ])));
1791
- const { data } = await client.send("Page.captureScreenshot", {
1792
- format: "png",
1793
- // clip: {
1794
- // x: 0,
1795
- // y: 0,
1796
- // width: viewportWidth,
1797
- // height: viewportHeight,
1798
- // scale: 1,
1799
- // },
1800
- });
1801
- if (!screenshotPath) {
1802
- return data;
1803
- }
1804
- let screenshotBuffer = Buffer.from(data, "base64");
1805
- const sharpBuffer = sharp(screenshotBuffer);
1806
- const metadata = await sharpBuffer.metadata();
1807
- //check if you are on retina display and reduce the quality of the image
1808
- if (metadata.width > viewportWidth || metadata.height > viewportHeight) {
1809
- screenshotBuffer = await sharpBuffer
1810
- .resize(viewportWidth, viewportHeight, {
1811
- fit: sharp.fit.inside,
1812
- withoutEnlargement: true,
1813
- })
1814
- .toBuffer();
1815
- }
1816
- fs.writeFileSync(screenshotPath, screenshotBuffer);
1817
- await client.detach();
1881
+ let screenshotBuffer = null;
1882
+ // if (focusedElement) {
1883
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1884
+ // await this._unhighlightElements(focusedElement);
1885
+ // await new Promise((resolve) => setTimeout(resolve, 100));
1886
+ // console.log(`Unhighlighted previous element`);
1887
+ // }
1888
+ // if (focusedElement) {
1889
+ // await this._highlightElements(focusedElement);
1890
+ // }
1891
+ if (this.context.browserName === "chromium") {
1892
+ const client = await playContext.newCDPSession(this.page);
1893
+ const { data } = await client.send("Page.captureScreenshot", {
1894
+ format: "png",
1895
+ // clip: {
1896
+ // x: 0,
1897
+ // y: 0,
1898
+ // width: viewportWidth,
1899
+ // height: viewportHeight,
1900
+ // scale: 1,
1901
+ // },
1902
+ });
1903
+ await client.detach();
1904
+ if (!screenshotPath) {
1905
+ return data;
1906
+ }
1907
+ screenshotBuffer = Buffer.from(data, "base64");
1908
+ }
1909
+ else {
1910
+ screenshotBuffer = await this.page.screenshot();
1911
+ }
1912
+ // if (focusedElement) {
1913
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1914
+ // await this._unhighlightElements(focusedElement);
1915
+ // }
1916
+ let image = await Jimp.read(screenshotBuffer);
1917
+ // Get the image dimensions
1918
+ const { width, height } = image.bitmap;
1919
+ const resizeRatio = viewportWidth / width;
1920
+ // Resize the image to fit within the viewport dimensions without enlarging
1921
+ if (width > viewportWidth) {
1922
+ image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
1923
+ await image.write(screenshotPath);
1924
+ }
1925
+ else {
1926
+ fs.writeFileSync(screenshotPath, screenshotBuffer);
1927
+ }
1928
+ return screenshotBuffer;
1818
1929
  }
1819
1930
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1820
- this._validateSelectors(selectors);
1821
- const startTime = Date.now();
1822
- let error = null;
1823
- let screenshotId = null;
1824
- let screenshotPath = null;
1931
+ const state = {
1932
+ selectors,
1933
+ _params,
1934
+ options,
1935
+ world,
1936
+ type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1937
+ text: `Verify element exists in page`,
1938
+ operation: "verifyElementExistInPage",
1939
+ log: "***** verify element " + selectors.element_name + " exists in page *****\n",
1940
+ };
1825
1941
  await new Promise((resolve) => setTimeout(resolve, 2000));
1826
- const info = {};
1827
- info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
1828
- info.operation = "verify";
1829
- info.selectors = selectors;
1830
1942
  try {
1831
- const element = await this._locate(selectors, info, _params);
1832
- if (element) {
1833
- await this.scrollIfNeeded(element, info);
1834
- }
1835
- await this._highlightElements(element);
1836
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1837
- await expect(element).toHaveCount(1, { timeout: 10000 });
1838
- return info;
1943
+ await _preCommand(state, this);
1944
+ await expect(state.element).toHaveCount(1, { timeout: 10000 });
1945
+ return state.info;
1839
1946
  }
1840
1947
  catch (e) {
1841
- //await this.closeUnexpectedPopups();
1842
- this.logger.error("verify failed " + info.log);
1843
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1844
- info.screenshotPath = screenshotPath;
1845
- Object.assign(e, { info: info });
1846
- error = e;
1847
- throw e;
1948
+ await _commandError(state, e, this);
1848
1949
  }
1849
1950
  finally {
1850
- const endTime = Date.now();
1851
- this._reportToWorld(world, {
1852
- element_name: selectors.element_name,
1853
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1854
- text: "Verify element exists in page",
1855
- screenshotId,
1856
- result: error
1857
- ? {
1858
- status: "FAILED",
1859
- startTime,
1860
- endTime,
1861
- message: error === null || error === void 0 ? void 0 : error.message,
1862
- }
1863
- : {
1864
- status: "PASSED",
1865
- startTime,
1866
- endTime,
1867
- },
1868
- info: info,
1869
- });
1951
+ await _commandFinally(state, this);
1870
1952
  }
1871
1953
  }
1872
1954
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
1873
- this._validateSelectors(selectors);
1874
- const startTime = Date.now();
1875
- let error = null;
1876
- let screenshotId = null;
1877
- let screenshotPath = null;
1955
+ const state = {
1956
+ selectors,
1957
+ _params,
1958
+ attribute,
1959
+ variable,
1960
+ options,
1961
+ world,
1962
+ type: Types.EXTRACT,
1963
+ text: `Extract attribute from element`,
1964
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1965
+ operation: "extractAttribute",
1966
+ log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1967
+ allowDisabled: true,
1968
+ };
1878
1969
  await new Promise((resolve) => setTimeout(resolve, 2000));
1879
- const info = {};
1880
- info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
1881
- info.operation = "extract";
1882
- info.selectors = selectors;
1883
1970
  try {
1884
- const element = await this._locate(selectors, info, _params);
1885
- await this._highlightElements(element);
1886
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1971
+ await _preCommand(state, this);
1887
1972
  switch (attribute) {
1888
1973
  case "inner_text":
1889
- info.value = await element.innerText();
1974
+ state.value = await state.element.innerText();
1890
1975
  break;
1891
1976
  case "href":
1892
- info.value = await element.getAttribute("href");
1977
+ state.value = await state.element.getAttribute("href");
1893
1978
  break;
1894
1979
  case "value":
1895
- info.value = await element.inputValue();
1980
+ state.value = await state.element.inputValue();
1981
+ break;
1982
+ case "text":
1983
+ state.value = await state.element.textContent();
1896
1984
  break;
1897
1985
  default:
1898
- info.value = await element.getAttribute(attribute);
1986
+ state.value = await state.element.getAttribute(attribute);
1899
1987
  break;
1900
1988
  }
1901
- this[variable] = info.value;
1902
- if (world) {
1903
- world[variable] = info.value;
1904
- }
1905
- this.setTestData({ [variable]: info.value }, world);
1906
- this.logger.info("set test data: " + variable + "=" + info.value);
1907
- return info;
1989
+ state.info.value = state.value;
1990
+ this.setTestData({ [variable]: state.value }, world);
1991
+ this.logger.info("set test data: " + variable + "=" + state.value);
1992
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1993
+ return state.info;
1908
1994
  }
1909
1995
  catch (e) {
1910
- //await this.closeUnexpectedPopups();
1911
- this.logger.error("extract failed " + info.log);
1912
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1913
- info.screenshotPath = screenshotPath;
1914
- Object.assign(e, { info: info });
1915
- error = e;
1916
- throw e;
1996
+ await _commandError(state, e, this);
1917
1997
  }
1918
1998
  finally {
1919
- const endTime = Date.now();
1920
- this._reportToWorld(world, {
1921
- element_name: selectors.element_name,
1922
- type: Types.EXTRACT_ATTRIBUTE,
1923
- variable: variable,
1924
- value: info.value,
1925
- text: "Extract attribute from element",
1926
- screenshotId,
1927
- result: error
1928
- ? {
1929
- status: "FAILED",
1930
- startTime,
1931
- endTime,
1932
- message: error === null || error === void 0 ? void 0 : error.message,
1933
- }
1934
- : {
1935
- status: "PASSED",
1936
- startTime,
1937
- endTime,
1938
- },
1939
- info: info,
1940
- });
1999
+ await _commandFinally(state, this);
2000
+ }
2001
+ }
2002
+ async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2003
+ const state = {
2004
+ selectors,
2005
+ _params,
2006
+ attribute,
2007
+ value,
2008
+ options,
2009
+ world,
2010
+ type: Types.VERIFY_ATTRIBUTE,
2011
+ highlight: true,
2012
+ screenshot: true,
2013
+ text: `Verify element attribute`,
2014
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
2015
+ operation: "verifyAttribute",
2016
+ log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
2017
+ allowDisabled: true,
2018
+ };
2019
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2020
+ let val;
2021
+ let expectedValue;
2022
+ try {
2023
+ await _preCommand(state, this);
2024
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2025
+ state.info.expectedValue = expectedValue;
2026
+ switch (attribute) {
2027
+ case "innerText":
2028
+ val = String(await state.element.innerText());
2029
+ break;
2030
+ case "text":
2031
+ val = String(await state.element.textContent());
2032
+ break;
2033
+ case "value":
2034
+ val = String(await state.element.inputValue());
2035
+ break;
2036
+ case "checked":
2037
+ val = String(await state.element.isChecked());
2038
+ break;
2039
+ case "disabled":
2040
+ val = String(await state.element.isDisabled());
2041
+ break;
2042
+ case "readOnly":
2043
+ const isEditable = await state.element.isEditable();
2044
+ val = String(!isEditable);
2045
+ break;
2046
+ default:
2047
+ val = String(await state.element.getAttribute(attribute));
2048
+ break;
2049
+ }
2050
+ state.info.value = val;
2051
+ let regex;
2052
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2053
+ const patternBody = expectedValue.slice(1, -1);
2054
+ regex = new RegExp(patternBody, "g");
2055
+ }
2056
+ else {
2057
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2058
+ regex = new RegExp(escapedPattern, "g");
2059
+ }
2060
+ if (!val.match(regex)) {
2061
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2062
+ state.info.failCause.assertionFailed = true;
2063
+ state.info.failCause.lastError = errorMessage;
2064
+ throw new Error(errorMessage);
2065
+ }
2066
+ return state.info;
2067
+ }
2068
+ catch (e) {
2069
+ await _commandError(state, e, this);
2070
+ }
2071
+ finally {
2072
+ await _commandFinally(state, this);
1941
2073
  }
1942
2074
  }
1943
2075
  async extractEmailData(emailAddress, options, world) {
@@ -1958,7 +2090,7 @@ class StableBrowser {
1958
2090
  if (options && options.timeout) {
1959
2091
  timeout = options.timeout;
1960
2092
  }
1961
- const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
2093
+ const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
1962
2094
  const request = {
1963
2095
  method: "POST",
1964
2096
  url: serviceUrl,
@@ -2014,7 +2146,8 @@ class StableBrowser {
2014
2146
  catch (e) {
2015
2147
  errorCount++;
2016
2148
  if (errorCount > 3) {
2017
- throw e;
2149
+ // throw e;
2150
+ await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
2018
2151
  }
2019
2152
  // ignore
2020
2153
  }
@@ -2028,27 +2161,32 @@ class StableBrowser {
2028
2161
  async _highlightElements(scope, css) {
2029
2162
  try {
2030
2163
  if (!scope) {
2164
+ // console.log(`Scope is not defined`);
2031
2165
  return;
2032
2166
  }
2033
2167
  if (!css) {
2034
2168
  scope
2035
2169
  .evaluate((node) => {
2036
2170
  if (node && node.style) {
2037
- let originalBorder = node.style.border;
2038
- node.style.border = "2px solid red";
2171
+ let originalOutline = node.style.outline;
2172
+ // console.log(`Original outline was: ${originalOutline}`);
2173
+ // node.__previousOutline = originalOutline;
2174
+ node.style.outline = "2px solid red";
2175
+ // console.log(`New outline is: ${node.style.outline}`);
2039
2176
  if (window) {
2040
2177
  window.addEventListener("beforeunload", function (e) {
2041
- node.style.border = originalBorder;
2178
+ node.style.outline = originalOutline;
2042
2179
  });
2043
2180
  }
2044
2181
  setTimeout(function () {
2045
- node.style.border = originalBorder;
2182
+ node.style.outline = originalOutline;
2046
2183
  }, 2000);
2047
2184
  }
2048
2185
  })
2049
2186
  .then(() => { })
2050
2187
  .catch((e) => {
2051
2188
  // ignore
2189
+ // console.error(`Could not highlight node : ${e}`);
2052
2190
  });
2053
2191
  }
2054
2192
  else {
@@ -2064,17 +2202,18 @@ class StableBrowser {
2064
2202
  if (!element.style) {
2065
2203
  return;
2066
2204
  }
2067
- var originalBorder = element.style.border;
2205
+ let originalOutline = element.style.outline;
2206
+ element.__previousOutline = originalOutline;
2068
2207
  // Set the new border to be red and 2px solid
2069
- element.style.border = "2px solid red";
2208
+ element.style.outline = "2px solid red";
2070
2209
  if (window) {
2071
2210
  window.addEventListener("beforeunload", function (e) {
2072
- element.style.border = originalBorder;
2211
+ element.style.outline = originalOutline;
2073
2212
  });
2074
2213
  }
2075
2214
  // Set a timeout to revert to the original border after 2 seconds
2076
2215
  setTimeout(function () {
2077
- element.style.border = originalBorder;
2216
+ element.style.outline = originalOutline;
2078
2217
  }, 2000);
2079
2218
  }
2080
2219
  return;
@@ -2082,6 +2221,7 @@ class StableBrowser {
2082
2221
  .then(() => { })
2083
2222
  .catch((e) => {
2084
2223
  // ignore
2224
+ // console.error(`Could not highlight css: ${e}`);
2085
2225
  });
2086
2226
  }
2087
2227
  }
@@ -2089,6 +2229,54 @@ class StableBrowser {
2089
2229
  console.debug(error);
2090
2230
  }
2091
2231
  }
2232
+ // async _unhighlightElements(scope, css) {
2233
+ // try {
2234
+ // if (!scope) {
2235
+ // return;
2236
+ // }
2237
+ // if (!css) {
2238
+ // scope
2239
+ // .evaluate((node) => {
2240
+ // if (node && node.style) {
2241
+ // if (!node.__previousOutline) {
2242
+ // node.style.outline = "";
2243
+ // } else {
2244
+ // node.style.outline = node.__previousOutline;
2245
+ // }
2246
+ // }
2247
+ // })
2248
+ // .then(() => {})
2249
+ // .catch((e) => {
2250
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2251
+ // });
2252
+ // } else {
2253
+ // scope
2254
+ // .evaluate(([css]) => {
2255
+ // if (!css) {
2256
+ // return;
2257
+ // }
2258
+ // let elements = Array.from(document.querySelectorAll(css));
2259
+ // for (i = 0; i < elements.length; i++) {
2260
+ // let element = elements[i];
2261
+ // if (!element.style) {
2262
+ // return;
2263
+ // }
2264
+ // if (!element.__previousOutline) {
2265
+ // element.style.outline = "";
2266
+ // } else {
2267
+ // element.style.outline = element.__previousOutline;
2268
+ // }
2269
+ // }
2270
+ // })
2271
+ // .then(() => {})
2272
+ // .catch((e) => {
2273
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2274
+ // });
2275
+ // }
2276
+ // } catch (error) {
2277
+ // // console.debug(error);
2278
+ // }
2279
+ // }
2092
2280
  async verifyPagePath(pathPart, options = {}, world = null) {
2093
2281
  const startTime = Date.now();
2094
2282
  let error = null;
@@ -2125,20 +2313,22 @@ class StableBrowser {
2125
2313
  info.screenshotPath = screenshotPath;
2126
2314
  Object.assign(e, { info: info });
2127
2315
  error = e;
2128
- throw e;
2316
+ // throw e;
2317
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2129
2318
  }
2130
2319
  finally {
2131
2320
  const endTime = Date.now();
2132
- this._reportToWorld(world, {
2321
+ _reportToWorld(world, {
2133
2322
  type: Types.VERIFY_PAGE_PATH,
2134
2323
  text: "Verify page path",
2324
+ _text: "Verify the page path contains " + pathPart,
2135
2325
  screenshotId,
2136
2326
  result: error
2137
2327
  ? {
2138
2328
  status: "FAILED",
2139
2329
  startTime,
2140
2330
  endTime,
2141
- message: error === null || error === void 0 ? void 0 : error.message,
2331
+ message: error?.message,
2142
2332
  }
2143
2333
  : {
2144
2334
  status: "PASSED",
@@ -2149,94 +2339,58 @@ class StableBrowser {
2149
2339
  });
2150
2340
  }
2151
2341
  }
2152
- async verifyTextExistInPage(text, options = {}, world = null) {
2342
+ async verifyPageTitle(title, options = {}, world = null) {
2153
2343
  const startTime = Date.now();
2154
- const timeout = this._getLoadTimeout(options);
2155
2344
  let error = null;
2156
2345
  let screenshotId = null;
2157
2346
  let screenshotPath = null;
2158
2347
  await new Promise((resolve) => setTimeout(resolve, 2000));
2159
2348
  const info = {};
2160
- info.log = "***** verify text " + text + " exists in page *****\n";
2161
- info.operation = "verifyTextExistInPage";
2162
- const newValue = await this._replaceWithLocalData(text, world);
2163
- if (newValue !== text) {
2164
- this.logger.info(text + "=" + newValue);
2165
- text = newValue;
2166
- }
2167
- info.text = text;
2168
- let dateAlternatives = findDateAlternatives(text);
2169
- let numberAlternatives = findNumberAlternatives(text);
2349
+ info.log = "***** verify page title " + title + " *****\n";
2350
+ info.operation = "verifyPageTitle";
2351
+ const newValue = await this._replaceWithLocalData(title, world);
2352
+ if (newValue !== title) {
2353
+ this.logger.info(title + "=" + newValue);
2354
+ title = newValue;
2355
+ }
2356
+ info.title = title;
2170
2357
  try {
2171
- while (true) {
2172
- const frames = this.page.frames();
2173
- let results = [];
2174
- for (let i = 0; i < frames.length; i++) {
2175
- if (dateAlternatives.date) {
2176
- for (let j = 0; j < dateAlternatives.dates.length; j++) {
2177
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
2178
- result.frame = frames[i];
2179
- results.push(result);
2180
- }
2181
- }
2182
- else if (numberAlternatives.number) {
2183
- for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2184
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
2185
- result.frame = frames[i];
2186
- results.push(result);
2187
- }
2188
- }
2189
- else {
2190
- const result = await this._locateElementByText(frames[i], text, "*", true, {});
2191
- result.frame = frames[i];
2192
- results.push(result);
2193
- }
2194
- }
2195
- info.results = results;
2196
- const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2197
- if (resultWithElementsFound.length === 0) {
2198
- if (Date.now() - startTime > timeout) {
2199
- throw new Error(`Text ${text} not found in page`);
2358
+ for (let i = 0; i < 30; i++) {
2359
+ const foundTitle = await this.page.title();
2360
+ if (!foundTitle.includes(title)) {
2361
+ if (i === 29) {
2362
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2200
2363
  }
2201
2364
  await new Promise((resolve) => setTimeout(resolve, 1000));
2202
2365
  continue;
2203
2366
  }
2204
- if (resultWithElementsFound[0].randomToken) {
2205
- const frame = resultWithElementsFound[0].frame;
2206
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2207
- await this._highlightElements(frame, dataAttribute);
2208
- const element = await frame.$(dataAttribute);
2209
- if (element) {
2210
- await this.scrollIfNeeded(element, info);
2211
- await element.dispatchEvent("bvt_verify_page_contains_text");
2212
- }
2213
- }
2214
2367
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2215
2368
  return info;
2216
2369
  }
2217
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2218
2370
  }
2219
2371
  catch (e) {
2220
2372
  //await this.closeUnexpectedPopups();
2221
- this.logger.error("verify text exist in page failed " + info.log);
2373
+ this.logger.error("verify page title failed " + info.log);
2222
2374
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2223
2375
  info.screenshotPath = screenshotPath;
2224
2376
  Object.assign(e, { info: info });
2225
2377
  error = e;
2226
- throw e;
2378
+ // throw e;
2379
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2227
2380
  }
2228
2381
  finally {
2229
2382
  const endTime = Date.now();
2230
- this._reportToWorld(world, {
2231
- type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
2232
- text: "Verify text exists in page",
2383
+ _reportToWorld(world, {
2384
+ type: Types.VERIFY_PAGE_PATH,
2385
+ text: "Verify page title",
2386
+ _text: "Verify the page title contains " + title,
2233
2387
  screenshotId,
2234
2388
  result: error
2235
2389
  ? {
2236
2390
  status: "FAILED",
2237
2391
  startTime,
2238
2392
  endTime,
2239
- message: error === null || error === void 0 ? void 0 : error.message,
2393
+ message: error?.message,
2240
2394
  }
2241
2395
  : {
2242
2396
  status: "PASSED",
@@ -2247,15 +2401,317 @@ class StableBrowser {
2247
2401
  });
2248
2402
  }
2249
2403
  }
2250
- _getServerUrl() {
2251
- let serviceUrl = "https://api.blinq.io";
2252
- if (process.env.NODE_ENV_BLINQ === "dev") {
2253
- serviceUrl = "https://dev.api.blinq.io";
2404
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2405
+ const frames = this.page.frames();
2406
+ let results = [];
2407
+ // let ignoreCase = false;
2408
+ for (let i = 0; i < frames.length; i++) {
2409
+ if (dateAlternatives.date) {
2410
+ for (let j = 0; j < dateAlternatives.dates.length; j++) {
2411
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2412
+ result.frame = frames[i];
2413
+ results.push(result);
2414
+ }
2415
+ }
2416
+ else if (numberAlternatives.number) {
2417
+ for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2418
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2419
+ result.frame = frames[i];
2420
+ results.push(result);
2421
+ }
2422
+ }
2423
+ else {
2424
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2425
+ result.frame = frames[i];
2426
+ results.push(result);
2427
+ }
2428
+ }
2429
+ state.info.results = results;
2430
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2431
+ return resultWithElementsFound;
2432
+ }
2433
+ async verifyTextExistInPage(text, options = {}, world = null) {
2434
+ text = unEscapeString(text);
2435
+ const state = {
2436
+ text_search: text,
2437
+ options,
2438
+ world,
2439
+ locate: false,
2440
+ scroll: false,
2441
+ highlight: false,
2442
+ type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2443
+ text: `Verify the text '${text}' exists in page`,
2444
+ _text: `Verify the text '${text}' exists in page`,
2445
+ operation: "verifyTextExistInPage",
2446
+ log: "***** verify text " + text + " exists in page *****\n",
2447
+ };
2448
+ if (testForRegex(text)) {
2449
+ text = text.replace(/\\"/g, '"');
2450
+ }
2451
+ const timeout = this._getFindElementTimeout(options);
2452
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2453
+ const newValue = await this._replaceWithLocalData(text, world);
2454
+ if (newValue !== text) {
2455
+ this.logger.info(text + "=" + newValue);
2456
+ text = newValue;
2457
+ }
2458
+ let dateAlternatives = findDateAlternatives(text);
2459
+ let numberAlternatives = findNumberAlternatives(text);
2460
+ try {
2461
+ await _preCommand(state, this);
2462
+ state.info.text = text;
2463
+ while (true) {
2464
+ let resultWithElementsFound = {
2465
+ length: 0,
2466
+ };
2467
+ try {
2468
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2469
+ }
2470
+ catch (error) {
2471
+ // ignore
2472
+ }
2473
+ if (resultWithElementsFound.length === 0) {
2474
+ if (Date.now() - state.startTime > timeout) {
2475
+ throw new Error(`Text ${text} not found in page`);
2476
+ }
2477
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2478
+ continue;
2479
+ }
2480
+ try {
2481
+ if (resultWithElementsFound[0].randomToken) {
2482
+ const frame = resultWithElementsFound[0].frame;
2483
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2484
+ await this._highlightElements(frame, dataAttribute);
2485
+ // if (world && world.screenshot && !world.screenshotPath) {
2486
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2487
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2488
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2489
+ // this._unhighlightElements(frame, dataAttribute)
2490
+ // .then(async () => {
2491
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2492
+ // })
2493
+ // .catch(
2494
+ // (e) => {}
2495
+ // console.error(e)
2496
+ // );
2497
+ // });
2498
+ // }
2499
+ const element = await frame.locator(dataAttribute).first();
2500
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2501
+ // await this._unhighlightElements(frame, dataAttribute);
2502
+ if (element) {
2503
+ await this.scrollIfNeeded(element, state.info);
2504
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2505
+ // await _screenshot(state, this, element);
2506
+ }
2507
+ }
2508
+ await _screenshot(state, this);
2509
+ return state.info;
2510
+ }
2511
+ catch (error) {
2512
+ console.error(error);
2513
+ }
2514
+ }
2515
+ // await expect(element).toHaveCount(1, { timeout: 10000 });
2516
+ }
2517
+ catch (e) {
2518
+ await _commandError(state, e, this);
2519
+ }
2520
+ finally {
2521
+ await _commandFinally(state, this);
2522
+ }
2523
+ }
2524
+ async waitForTextToDisappear(text, options = {}, world = null) {
2525
+ text = unEscapeString(text);
2526
+ const state = {
2527
+ text_search: text,
2528
+ options,
2529
+ world,
2530
+ locate: false,
2531
+ scroll: false,
2532
+ highlight: false,
2533
+ type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2534
+ text: `Verify the text '${text}' does not exist in page`,
2535
+ _text: `Verify the text '${text}' does not exist in page`,
2536
+ operation: "verifyTextNotExistInPage",
2537
+ log: "***** verify text " + text + " does not exist in page *****\n",
2538
+ };
2539
+ if (testForRegex(text)) {
2540
+ text = text.replace(/\\"/g, '"');
2541
+ }
2542
+ const timeout = this._getFindElementTimeout(options);
2543
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2544
+ const newValue = await this._replaceWithLocalData(text, world);
2545
+ if (newValue !== text) {
2546
+ this.logger.info(text + "=" + newValue);
2547
+ text = newValue;
2548
+ }
2549
+ let dateAlternatives = findDateAlternatives(text);
2550
+ let numberAlternatives = findNumberAlternatives(text);
2551
+ try {
2552
+ await _preCommand(state, this);
2553
+ state.info.text = text;
2554
+ let resultWithElementsFound = {
2555
+ length: null, // initial cannot be 0
2556
+ };
2557
+ while (true) {
2558
+ try {
2559
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2560
+ }
2561
+ catch (error) {
2562
+ // ignore
2563
+ }
2564
+ if (resultWithElementsFound.length === 0) {
2565
+ await _screenshot(state, this);
2566
+ return state.info;
2567
+ }
2568
+ if (Date.now() - state.startTime > timeout) {
2569
+ throw new Error(`Text ${text} found in page`);
2570
+ }
2571
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2572
+ }
2573
+ }
2574
+ catch (e) {
2575
+ await _commandError(state, e, this);
2576
+ }
2577
+ finally {
2578
+ await _commandFinally(state, this);
2579
+ }
2580
+ }
2581
+ async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
2582
+ textAnchor = unEscapeString(textAnchor);
2583
+ textToVerify = unEscapeString(textToVerify);
2584
+ const state = {
2585
+ text_search: textToVerify,
2586
+ options,
2587
+ world,
2588
+ locate: false,
2589
+ scroll: false,
2590
+ highlight: false,
2591
+ type: Types.VERIFY_TEXT_WITH_RELATION,
2592
+ text: `Verify text with relation to another text`,
2593
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2594
+ operation: "verify_text_with_relation",
2595
+ log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2596
+ };
2597
+ const timeout = this._getFindElementTimeout(options);
2598
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2599
+ let newValue = await this._replaceWithLocalData(textAnchor, world);
2600
+ if (newValue !== textAnchor) {
2601
+ this.logger.info(textAnchor + "=" + newValue);
2602
+ textAnchor = newValue;
2603
+ }
2604
+ newValue = await this._replaceWithLocalData(textToVerify, world);
2605
+ if (newValue !== textToVerify) {
2606
+ this.logger.info(textToVerify + "=" + newValue);
2607
+ textToVerify = newValue;
2608
+ }
2609
+ let dateAlternatives = findDateAlternatives(textToVerify);
2610
+ let numberAlternatives = findNumberAlternatives(textToVerify);
2611
+ let foundAncore = false;
2612
+ try {
2613
+ await _preCommand(state, this);
2614
+ state.info.text = textToVerify;
2615
+ let resultWithElementsFound = {
2616
+ length: 0,
2617
+ };
2618
+ while (true) {
2619
+ try {
2620
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2621
+ }
2622
+ catch (error) {
2623
+ // ignore
2624
+ }
2625
+ if (resultWithElementsFound.length === 0) {
2626
+ if (Date.now() - state.startTime > timeout) {
2627
+ throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
2628
+ }
2629
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2630
+ continue;
2631
+ }
2632
+ try {
2633
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2634
+ foundAncore = true;
2635
+ const result = resultWithElementsFound[i];
2636
+ const token = result.randomToken;
2637
+ const frame = result.frame;
2638
+ let css = `[data-blinq-id-${token}]`;
2639
+ const climbArray1 = [];
2640
+ for (let i = 0; i < climb; i++) {
2641
+ climbArray1.push("..");
2642
+ }
2643
+ let climbXpath = "xpath=" + climbArray1.join("/");
2644
+ css = css + " >> " + climbXpath;
2645
+ const count = await frame.locator(css).count();
2646
+ for (let j = 0; j < count; j++) {
2647
+ const continer = await frame.locator(css).nth(j);
2648
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2649
+ if (result.elementCount > 0) {
2650
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2651
+ await this._highlightElements(frame, dataAttribute);
2652
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2653
+ // if (world && world.screenshot && !world.screenshotPath) {
2654
+ // console.log(`Highlighting for vtrt while running from recorder`);
2655
+ // this._highlightElements(frame, dataAttribute)
2656
+ // .then(async () => {
2657
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2658
+ // this._unhighlightElements(frame, dataAttribute).then(
2659
+ // () => {}
2660
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2661
+ // );
2662
+ // })
2663
+ // .catch(e);
2664
+ // }
2665
+ //await this._highlightElements(frame, cssAnchor);
2666
+ const element = await frame.locator(dataAttribute).first();
2667
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2668
+ // await this._unhighlightElements(frame, dataAttribute);
2669
+ if (element) {
2670
+ await this.scrollIfNeeded(element, state.info);
2671
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2672
+ }
2673
+ await _screenshot(state, this);
2674
+ return state.info;
2675
+ }
2676
+ }
2677
+ }
2678
+ }
2679
+ catch (error) {
2680
+ console.error(error);
2681
+ }
2682
+ }
2683
+ // await expect(element).toHaveCount(1, { timeout: 10000 });
2684
+ }
2685
+ catch (e) {
2686
+ await _commandError(state, e, this);
2687
+ }
2688
+ finally {
2689
+ await _commandFinally(state, this);
2254
2690
  }
2255
- else if (process.env.NODE_ENV_BLINQ === "stage") {
2256
- serviceUrl = "https://stage.api.blinq.io";
2691
+ }
2692
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2693
+ const frames = this.page.frames();
2694
+ let results = [];
2695
+ let ignoreCase = false;
2696
+ for (let i = 0; i < frames.length; i++) {
2697
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2698
+ result.frame = frames[i];
2699
+ const climbArray = [];
2700
+ for (let i = 0; i < climb; i++) {
2701
+ climbArray.push("..");
2702
+ }
2703
+ let climbXpath = "xpath=" + climbArray.join("/");
2704
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2705
+ const count = await frames[i].locator(newLocator).count();
2706
+ if (count > 0) {
2707
+ result.elementCount = count;
2708
+ result.locator = newLocator;
2709
+ results.push(result);
2710
+ }
2257
2711
  }
2258
- return serviceUrl;
2712
+ // state.info.results = results;
2713
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2714
+ return resultWithElementsFound;
2259
2715
  }
2260
2716
  async visualVerification(text, options = {}, world = null) {
2261
2717
  const startTime = Date.now();
@@ -2271,14 +2727,17 @@ class StableBrowser {
2271
2727
  throw new Error("TOKEN is not set");
2272
2728
  }
2273
2729
  try {
2274
- let serviceUrl = this._getServerUrl();
2730
+ let serviceUrl = _getServerUrl();
2275
2731
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2276
2732
  info.screenshotPath = screenshotPath;
2277
2733
  const screenshot = await this.takeScreenshot();
2278
- const request = {
2279
- method: "POST",
2734
+ let request = {
2735
+ method: "post",
2736
+ maxBodyLength: Infinity,
2280
2737
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2281
2738
  headers: {
2739
+ "x-bvt-project-id": path.basename(this.project_path),
2740
+ "x-source": "aaa",
2282
2741
  "Content-Type": "application/json",
2283
2742
  Authorization: `Bearer ${process.env.TOKEN}`,
2284
2743
  },
@@ -2287,7 +2746,7 @@ class StableBrowser {
2287
2746
  screenshot: screenshot,
2288
2747
  }),
2289
2748
  };
2290
- let result = await this.context.api.request(request);
2749
+ const result = await axios.request(request);
2291
2750
  if (result.data.status !== true) {
2292
2751
  throw new Error("Visual validation failed");
2293
2752
  }
@@ -2307,20 +2766,22 @@ class StableBrowser {
2307
2766
  info.screenshotPath = screenshotPath;
2308
2767
  Object.assign(e, { info: info });
2309
2768
  error = e;
2310
- throw e;
2769
+ // throw e;
2770
+ await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
2311
2771
  }
2312
2772
  finally {
2313
2773
  const endTime = Date.now();
2314
- this._reportToWorld(world, {
2774
+ _reportToWorld(world, {
2315
2775
  type: Types.VERIFY_VISUAL,
2316
2776
  text: "Visual verification",
2777
+ _text: "Visual verification of " + text,
2317
2778
  screenshotId,
2318
2779
  result: error
2319
2780
  ? {
2320
2781
  status: "FAILED",
2321
2782
  startTime,
2322
2783
  endTime,
2323
- message: error === null || error === void 0 ? void 0 : error.message,
2784
+ message: error?.message,
2324
2785
  }
2325
2786
  : {
2326
2787
  status: "PASSED",
@@ -2352,13 +2813,14 @@ class StableBrowser {
2352
2813
  this.logger.info("Table data verified");
2353
2814
  }
2354
2815
  async getTableData(selectors, _params = null, options = {}, world = null) {
2355
- this._validateSelectors(selectors);
2816
+ _validateSelectors(selectors);
2356
2817
  const startTime = Date.now();
2357
2818
  let error = null;
2358
2819
  let screenshotId = null;
2359
2820
  let screenshotPath = null;
2360
2821
  const info = {};
2361
2822
  info.log = "";
2823
+ info.locatorLog = new LocatorLog(selectors);
2362
2824
  info.operation = "getTableData";
2363
2825
  info.selectors = selectors;
2364
2826
  try {
@@ -2374,11 +2836,12 @@ class StableBrowser {
2374
2836
  info.screenshotPath = screenshotPath;
2375
2837
  Object.assign(e, { info: info });
2376
2838
  error = e;
2377
- throw e;
2839
+ // throw e;
2840
+ await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
2378
2841
  }
2379
2842
  finally {
2380
2843
  const endTime = Date.now();
2381
- this._reportToWorld(world, {
2844
+ _reportToWorld(world, {
2382
2845
  element_name: selectors.element_name,
2383
2846
  type: Types.GET_TABLE_DATA,
2384
2847
  text: "Get table data",
@@ -2388,7 +2851,7 @@ class StableBrowser {
2388
2851
  status: "FAILED",
2389
2852
  startTime,
2390
2853
  endTime,
2391
- message: error === null || error === void 0 ? void 0 : error.message,
2854
+ message: error?.message,
2392
2855
  }
2393
2856
  : {
2394
2857
  status: "PASSED",
@@ -2400,7 +2863,7 @@ class StableBrowser {
2400
2863
  }
2401
2864
  }
2402
2865
  async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
2403
- this._validateSelectors(selectors);
2866
+ _validateSelectors(selectors);
2404
2867
  if (!query) {
2405
2868
  throw new Error("query is null");
2406
2869
  }
@@ -2433,7 +2896,7 @@ class StableBrowser {
2433
2896
  info.operation = "analyzeTable";
2434
2897
  info.selectors = selectors;
2435
2898
  info.query = query;
2436
- query = this._fixUsingParams(query, _params);
2899
+ query = _fixUsingParams(query, _params);
2437
2900
  info.query_fixed = query;
2438
2901
  info.operator = operator;
2439
2902
  info.value = value;
@@ -2539,11 +3002,12 @@ class StableBrowser {
2539
3002
  info.screenshotPath = screenshotPath;
2540
3003
  Object.assign(e, { info: info });
2541
3004
  error = e;
2542
- throw e;
3005
+ // throw e;
3006
+ await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
2543
3007
  }
2544
3008
  finally {
2545
3009
  const endTime = Date.now();
2546
- this._reportToWorld(world, {
3010
+ _reportToWorld(world, {
2547
3011
  element_name: selectors.element_name,
2548
3012
  type: Types.ANALYZE_TABLE,
2549
3013
  text: "Analyze table",
@@ -2553,7 +3017,7 @@ class StableBrowser {
2553
3017
  status: "FAILED",
2554
3018
  startTime,
2555
3019
  endTime,
2556
- message: error === null || error === void 0 ? void 0 : error.message,
3020
+ message: error?.message,
2557
3021
  }
2558
3022
  : {
2559
3023
  status: "PASSED",
@@ -2565,27 +3029,7 @@ class StableBrowser {
2565
3029
  }
2566
3030
  }
2567
3031
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2568
- if (!value) {
2569
- return value;
2570
- }
2571
- // find all the accurance of {{(.*?)}} and replace with the value
2572
- let regex = /{{(.*?)}}/g;
2573
- let matches = value.match(regex);
2574
- if (matches) {
2575
- const testData = this.getTestData(world);
2576
- for (let i = 0; i < matches.length; i++) {
2577
- let match = matches[i];
2578
- let key = match.substring(2, match.length - 2);
2579
- let newValue = objectPath.get(testData, key, null);
2580
- if (newValue !== null) {
2581
- value = value.replace(match, newValue);
2582
- }
2583
- }
2584
- }
2585
- if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
2586
- return await decrypt(value, null, totpWait);
2587
- }
2588
- return value;
3032
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
2589
3033
  }
2590
3034
  _getLoadTimeout(options) {
2591
3035
  let timeout = 15000;
@@ -2597,6 +3041,32 @@ class StableBrowser {
2597
3041
  }
2598
3042
  return timeout;
2599
3043
  }
3044
+ _getFindElementTimeout(options) {
3045
+ if (options && options.timeout) {
3046
+ return options.timeout;
3047
+ }
3048
+ if (this.configuration.find_element_timeout) {
3049
+ return this.configuration.find_element_timeout;
3050
+ }
3051
+ return 30000;
3052
+ }
3053
+ async saveStoreState(path = null, world = null) {
3054
+ const storageState = await this.page.context().storageState();
3055
+ //const testDataFile = _getDataFile(world, this.context, this);
3056
+ if (path) {
3057
+ // save { storageState: storageState } into the path
3058
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3059
+ }
3060
+ else {
3061
+ await this.setTestData({ storageState: storageState }, world);
3062
+ }
3063
+ }
3064
+ async restoreSaveState(path = null, world = null) {
3065
+ await refreshBrowser(this, path, world);
3066
+ this.registerEventListeners(this.context);
3067
+ registerNetworkEvents(this.world, this, this.context, this.page);
3068
+ registerDownloadEvent(this.page, this.world, this.context);
3069
+ }
2600
3070
  async waitForPageLoad(options = {}, world = null) {
2601
3071
  let timeout = this._getLoadTimeout(options);
2602
3072
  const promiseArray = [];
@@ -2636,7 +3106,7 @@ class StableBrowser {
2636
3106
  await new Promise((resolve) => setTimeout(resolve, 2000));
2637
3107
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2638
3108
  const endTime = Date.now();
2639
- this._reportToWorld(world, {
3109
+ _reportToWorld(world, {
2640
3110
  type: Types.GET_PAGE_STATUS,
2641
3111
  text: "Wait for page load",
2642
3112
  screenshotId,
@@ -2645,7 +3115,7 @@ class StableBrowser {
2645
3115
  status: "FAILED",
2646
3116
  startTime,
2647
3117
  endTime,
2648
- message: error === null || error === void 0 ? void 0 : error.message,
3118
+ message: error?.message,
2649
3119
  }
2650
3120
  : {
2651
3121
  status: "PASSED",
@@ -2656,41 +3126,123 @@ class StableBrowser {
2656
3126
  }
2657
3127
  }
2658
3128
  async closePage(options = {}, world = null) {
2659
- const startTime = Date.now();
2660
- let error = null;
2661
- let screenshotId = null;
2662
- let screenshotPath = null;
2663
- const info = {};
3129
+ const state = {
3130
+ options,
3131
+ world,
3132
+ locate: false,
3133
+ scroll: false,
3134
+ highlight: false,
3135
+ type: Types.CLOSE_PAGE,
3136
+ text: `Close page`,
3137
+ _text: `Close the page`,
3138
+ operation: "closePage",
3139
+ log: "***** close page *****\n",
3140
+ throwError: false,
3141
+ };
2664
3142
  try {
3143
+ await _preCommand(state, this);
2665
3144
  await this.page.close();
2666
3145
  }
2667
3146
  catch (e) {
2668
3147
  console.log(".");
3148
+ await _commandError(state, e, this);
2669
3149
  }
2670
3150
  finally {
2671
- await new Promise((resolve) => setTimeout(resolve, 2000));
2672
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2673
- const endTime = Date.now();
2674
- this._reportToWorld(world, {
2675
- type: Types.CLOSE_PAGE,
2676
- text: "close page",
2677
- screenshotId,
2678
- result: error
2679
- ? {
2680
- status: "FAILED",
2681
- startTime,
2682
- endTime,
2683
- message: error === null || error === void 0 ? void 0 : error.message,
3151
+ await _commandFinally(state, this);
3152
+ }
3153
+ }
3154
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3155
+ let operation = null;
3156
+ if (!options || !options.operation) {
3157
+ throw new Error("operation is not defined");
3158
+ }
3159
+ operation = options.operation;
3160
+ // validate operation is one of the supported operations
3161
+ if (operation != "click" && operation != "hover+click") {
3162
+ throw new Error("operation is not supported");
3163
+ }
3164
+ const state = {
3165
+ options,
3166
+ world,
3167
+ locate: false,
3168
+ scroll: false,
3169
+ highlight: false,
3170
+ type: Types.TABLE_OPERATION,
3171
+ text: `Table operation`,
3172
+ _text: `Table ${operation} operation`,
3173
+ operation: operation,
3174
+ log: "***** Table operation *****\n",
3175
+ };
3176
+ const timeout = this._getFindElementTimeout(options);
3177
+ try {
3178
+ await _preCommand(state, this);
3179
+ const start = Date.now();
3180
+ let cellArea = null;
3181
+ while (true) {
3182
+ try {
3183
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3184
+ if (cellArea) {
3185
+ break;
2684
3186
  }
2685
- : {
2686
- status: "PASSED",
2687
- startTime,
2688
- endTime,
2689
- },
2690
- info: info,
2691
- });
3187
+ }
3188
+ catch (e) {
3189
+ // ignore
3190
+ }
3191
+ if (Date.now() - start > timeout) {
3192
+ throw new Error(`Cell not found in table`);
3193
+ }
3194
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3195
+ }
3196
+ switch (operation) {
3197
+ case "click":
3198
+ if (!options.css) {
3199
+ // will click in the center of the cell
3200
+ let xOffset = 0;
3201
+ let yOffset = 0;
3202
+ if (options.xOffset) {
3203
+ xOffset = options.xOffset;
3204
+ }
3205
+ if (options.yOffset) {
3206
+ yOffset = options.yOffset;
3207
+ }
3208
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3209
+ }
3210
+ else {
3211
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3212
+ if (results.length === 0) {
3213
+ throw new Error(`Element not found in cell area`);
3214
+ }
3215
+ state.element = results[0];
3216
+ await performAction("click", state.element, options, this, state, _params);
3217
+ }
3218
+ break;
3219
+ case "hover+click":
3220
+ if (!options.css) {
3221
+ throw new Error("css is not defined");
3222
+ }
3223
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3224
+ if (results.length === 0) {
3225
+ throw new Error(`Element not found in cell area`);
3226
+ }
3227
+ state.element = results[0];
3228
+ await performAction("hover+click", state.element, options, this, state, _params);
3229
+ break;
3230
+ default:
3231
+ throw new Error("operation is not supported");
3232
+ }
3233
+ }
3234
+ catch (e) {
3235
+ await _commandError(state, e, this);
3236
+ }
3237
+ finally {
3238
+ await _commandFinally(state, this);
2692
3239
  }
2693
3240
  }
3241
+ saveTestDataAsGlobal(options, world) {
3242
+ const dataFile = _getDataFile(world, this.context, this);
3243
+ process.env.GLOBAL_TEST_DATA_FILE = dataFile;
3244
+ this.logger.info("Save the scenario test data as global for the following scenarios.");
3245
+ }
2694
3246
  async setViewportSize(width, hight, options = {}, world = null) {
2695
3247
  const startTime = Date.now();
2696
3248
  let error = null;
@@ -2708,21 +3260,23 @@ class StableBrowser {
2708
3260
  }
2709
3261
  catch (e) {
2710
3262
  console.log(".");
3263
+ await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
2711
3264
  }
2712
3265
  finally {
2713
3266
  await new Promise((resolve) => setTimeout(resolve, 2000));
2714
3267
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
2715
3268
  const endTime = Date.now();
2716
- this._reportToWorld(world, {
3269
+ _reportToWorld(world, {
2717
3270
  type: Types.SET_VIEWPORT,
2718
3271
  text: "set viewport size to " + width + "x" + hight,
3272
+ _text: "Set the viewport size to " + width + "x" + hight,
2719
3273
  screenshotId,
2720
3274
  result: error
2721
3275
  ? {
2722
3276
  status: "FAILED",
2723
3277
  startTime,
2724
3278
  endTime,
2725
- message: error === null || error === void 0 ? void 0 : error.message,
3279
+ message: error?.message,
2726
3280
  }
2727
3281
  : {
2728
3282
  status: "PASSED",
@@ -2744,12 +3298,13 @@ class StableBrowser {
2744
3298
  }
2745
3299
  catch (e) {
2746
3300
  console.log(".");
3301
+ await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
2747
3302
  }
2748
3303
  finally {
2749
3304
  await new Promise((resolve) => setTimeout(resolve, 2000));
2750
3305
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2751
3306
  const endTime = Date.now();
2752
- this._reportToWorld(world, {
3307
+ _reportToWorld(world, {
2753
3308
  type: Types.GET_PAGE_STATUS,
2754
3309
  text: "page relaod",
2755
3310
  screenshotId,
@@ -2758,7 +3313,7 @@ class StableBrowser {
2758
3313
  status: "FAILED",
2759
3314
  startTime,
2760
3315
  endTime,
2761
- message: error === null || error === void 0 ? void 0 : error.message,
3316
+ message: error?.message,
2762
3317
  }
2763
3318
  : {
2764
3319
  status: "PASSED",
@@ -2785,11 +3340,122 @@ class StableBrowser {
2785
3340
  console.log("#-#");
2786
3341
  }
2787
3342
  }
2788
- _reportToWorld(world, properties) {
2789
- if (!world || !world.attach) {
2790
- return;
3343
+ async beforeScenario(world, scenario) {
3344
+ this.beforeScenarioCalled = true;
3345
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3346
+ this.scenarioName = scenario.pickle.name;
3347
+ }
3348
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3349
+ this.featureName = scenario.gherkinDocument.feature.name;
3350
+ }
3351
+ if (this.context) {
3352
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3353
+ }
3354
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3355
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3356
+ // check if @global_test_data tag is present
3357
+ if (this.tags.includes("@global_test_data")) {
3358
+ this.saveTestDataAsGlobal({}, world);
3359
+ }
3360
+ }
3361
+ // update test data based on feature/scenario
3362
+ let envName = null;
3363
+ if (this.context && this.context.environment) {
3364
+ envName = this.context.environment.name;
3365
+ }
3366
+ if (!process.env.TEMP_RUN) {
3367
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3368
+ }
3369
+ await loadBrunoParams(this.context, this.context.environment.name);
3370
+ }
3371
+ async afterScenario(world, scenario) { }
3372
+ async beforeStep(world, step) {
3373
+ if (!this.beforeScenarioCalled) {
3374
+ this.beforeScenario(world, step);
3375
+ }
3376
+ if (this.stepIndex === undefined) {
3377
+ this.stepIndex = 0;
3378
+ }
3379
+ else {
3380
+ this.stepIndex++;
3381
+ }
3382
+ if (step && step.pickleStep && step.pickleStep.text) {
3383
+ this.stepName = step.pickleStep.text;
3384
+ this.logger.info("step: " + this.stepName);
3385
+ }
3386
+ else if (step && step.text) {
3387
+ this.stepName = step.text;
3388
+ }
3389
+ else {
3390
+ this.stepName = "step " + this.stepIndex;
3391
+ }
3392
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3393
+ if (this.context.browserObject.context) {
3394
+ await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3395
+ }
3396
+ }
3397
+ if (this.initSnapshotTaken === false) {
3398
+ this.initSnapshotTaken = true;
3399
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3400
+ const snapshot = await this.getAriaSnapshot();
3401
+ if (snapshot) {
3402
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3403
+ }
3404
+ }
3405
+ }
3406
+ }
3407
+ async getAriaSnapshot() {
3408
+ try {
3409
+ // find the page url
3410
+ const url = await this.page.url();
3411
+ // extract the path from the url
3412
+ const path = new URL(url).pathname;
3413
+ // get the page title
3414
+ const title = await this.page.title();
3415
+ // go over other frams
3416
+ const frames = this.page.frames();
3417
+ const snapshots = [];
3418
+ const content = [`- path: ${path}`, `- title: ${title}`];
3419
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3420
+ for (let i = 0; i < frames.length; i++) {
3421
+ content.push(`- frame: ${i}`);
3422
+ const frame = frames[i];
3423
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3424
+ content.push(snapshot);
3425
+ }
3426
+ return content.join("\n");
3427
+ }
3428
+ catch (e) {
3429
+ console.error(e);
3430
+ }
3431
+ return null;
3432
+ }
3433
+ async afterStep(world, step) {
3434
+ this.stepName = null;
3435
+ if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3436
+ if (this.context.browserObject.context) {
3437
+ await this.context.browserObject.context.tracing.stopChunk({
3438
+ path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3439
+ });
3440
+ if (world && world.attach) {
3441
+ await world.attach(JSON.stringify({
3442
+ type: "trace",
3443
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3444
+ }), "application/json+trace");
3445
+ }
3446
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3447
+ }
3448
+ }
3449
+ if (this.context) {
3450
+ this.context.examplesRow = null;
3451
+ }
3452
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3453
+ const snapshot = await this.getAriaSnapshot();
3454
+ if (snapshot) {
3455
+ const obj = {};
3456
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3457
+ }
2791
3458
  }
2792
- world.attach(JSON.stringify(properties), { mediaType: "application/json" });
2793
3459
  }
2794
3460
  }
2795
3461
  function createTimedPromise(promise, label) {
@@ -2797,151 +3463,5 @@ function createTimedPromise(promise, label) {
2797
3463
  .then((result) => ({ status: "fulfilled", label, result }))
2798
3464
  .catch((error) => Promise.reject({ status: "rejected", label, error }));
2799
3465
  }
2800
- const KEYBOARD_EVENTS = [
2801
- "ALT",
2802
- "AltGraph",
2803
- "CapsLock",
2804
- "Control",
2805
- "Fn",
2806
- "FnLock",
2807
- "Hyper",
2808
- "Meta",
2809
- "NumLock",
2810
- "ScrollLock",
2811
- "Shift",
2812
- "Super",
2813
- "Symbol",
2814
- "SymbolLock",
2815
- "Enter",
2816
- "Tab",
2817
- "ArrowDown",
2818
- "ArrowLeft",
2819
- "ArrowRight",
2820
- "ArrowUp",
2821
- "End",
2822
- "Home",
2823
- "PageDown",
2824
- "PageUp",
2825
- "Backspace",
2826
- "Clear",
2827
- "Copy",
2828
- "CrSel",
2829
- "Cut",
2830
- "Delete",
2831
- "EraseEof",
2832
- "ExSel",
2833
- "Insert",
2834
- "Paste",
2835
- "Redo",
2836
- "Undo",
2837
- "Accept",
2838
- "Again",
2839
- "Attn",
2840
- "Cancel",
2841
- "ContextMenu",
2842
- "Escape",
2843
- "Execute",
2844
- "Find",
2845
- "Finish",
2846
- "Help",
2847
- "Pause",
2848
- "Play",
2849
- "Props",
2850
- "Select",
2851
- "ZoomIn",
2852
- "ZoomOut",
2853
- "BrightnessDown",
2854
- "BrightnessUp",
2855
- "Eject",
2856
- "LogOff",
2857
- "Power",
2858
- "PowerOff",
2859
- "PrintScreen",
2860
- "Hibernate",
2861
- "Standby",
2862
- "WakeUp",
2863
- "AllCandidates",
2864
- "Alphanumeric",
2865
- "CodeInput",
2866
- "Compose",
2867
- "Convert",
2868
- "Dead",
2869
- "FinalMode",
2870
- "GroupFirst",
2871
- "GroupLast",
2872
- "GroupNext",
2873
- "GroupPrevious",
2874
- "ModeChange",
2875
- "NextCandidate",
2876
- "NonConvert",
2877
- "PreviousCandidate",
2878
- "Process",
2879
- "SingleCandidate",
2880
- "HangulMode",
2881
- "HanjaMode",
2882
- "JunjaMode",
2883
- "Eisu",
2884
- "Hankaku",
2885
- "Hiragana",
2886
- "HiraganaKatakana",
2887
- "KanaMode",
2888
- "KanjiMode",
2889
- "Katakana",
2890
- "Romaji",
2891
- "Zenkaku",
2892
- "ZenkakuHanaku",
2893
- "F1",
2894
- "F2",
2895
- "F3",
2896
- "F4",
2897
- "F5",
2898
- "F6",
2899
- "F7",
2900
- "F8",
2901
- "F9",
2902
- "F10",
2903
- "F11",
2904
- "F12",
2905
- "Soft1",
2906
- "Soft2",
2907
- "Soft3",
2908
- "Soft4",
2909
- "ChannelDown",
2910
- "ChannelUp",
2911
- "Close",
2912
- "MailForward",
2913
- "MailReply",
2914
- "MailSend",
2915
- "MediaFastForward",
2916
- "MediaPause",
2917
- "MediaPlay",
2918
- "MediaPlayPause",
2919
- "MediaRecord",
2920
- "MediaRewind",
2921
- "MediaStop",
2922
- "MediaTrackNext",
2923
- "MediaTrackPrevious",
2924
- "AudioBalanceLeft",
2925
- "AudioBalanceRight",
2926
- "AudioBassBoostDown",
2927
- "AudioBassBoostToggle",
2928
- "AudioBassBoostUp",
2929
- "AudioFaderFront",
2930
- "AudioFaderRear",
2931
- "AudioSurroundModeNext",
2932
- "AudioTrebleDown",
2933
- "AudioTrebleUp",
2934
- "AudioVolumeDown",
2935
- "AudioVolumeMute",
2936
- "AudioVolumeUp",
2937
- "MicrophoneToggle",
2938
- "MicrophoneVolumeDown",
2939
- "MicrophoneVolumeMute",
2940
- "MicrophoneVolumeUp",
2941
- "TV",
2942
- "TV3DMode",
2943
- "TVAntennaCable",
2944
- "TVAudioDescription",
2945
- ];
2946
3466
  export { StableBrowser };
2947
3467
  //# sourceMappingURL=stable_browser.js.map