automation_model 1.0.430-dev → 1.0.430

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