@dev-blinq/cucumber_client 1.0.1382-dev → 1.0.1382-stage

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 (49) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +107 -107
  2. package/bin/assets/preload/css_gen.js +10 -10
  3. package/bin/assets/preload/toolbar.js +27 -29
  4. package/bin/assets/preload/unique_locators.js +1 -1
  5. package/bin/assets/preload/yaml.js +288 -275
  6. package/bin/assets/scripts/aria_snapshot.js +223 -220
  7. package/bin/assets/scripts/dom_attr.js +329 -329
  8. package/bin/assets/scripts/dom_parent.js +169 -174
  9. package/bin/assets/scripts/event_utils.js +94 -94
  10. package/bin/assets/scripts/pw.js +2050 -1949
  11. package/bin/assets/scripts/recorder.js +13 -23
  12. package/bin/assets/scripts/snapshot_capturer.js +147 -147
  13. package/bin/assets/scripts/unique_locators.js +163 -44
  14. package/bin/assets/scripts/yaml.js +796 -783
  15. package/bin/assets/templates/_hooks_template.txt +6 -2
  16. package/bin/assets/templates/utils_template.txt +2 -2
  17. package/bin/client/code_cleanup/utils.js +5 -1
  18. package/bin/client/code_gen/api_codegen.js +2 -2
  19. package/bin/client/code_gen/code_inversion.js +107 -2
  20. package/bin/client/code_gen/function_signature.js +4 -0
  21. package/bin/client/code_gen/page_reflection.js +846 -906
  22. package/bin/client/code_gen/playwright_codeget.js +27 -3
  23. package/bin/client/cucumber/feature.js +4 -0
  24. package/bin/client/cucumber/feature_data.js +2 -2
  25. package/bin/client/cucumber/project_to_document.js +8 -2
  26. package/bin/client/cucumber/steps_definitions.js +6 -3
  27. package/bin/client/cucumber_selector.js +17 -1
  28. package/bin/client/local_agent.js +3 -2
  29. package/bin/client/parse_feature_file.js +23 -26
  30. package/bin/client/playground/projects/env.json +2 -2
  31. package/bin/client/project.js +186 -202
  32. package/bin/client/recorderv3/bvt_init.js +345 -0
  33. package/bin/client/recorderv3/bvt_recorder.js +708 -100
  34. package/bin/client/recorderv3/implemented_steps.js +2 -0
  35. package/bin/client/recorderv3/index.js +4 -303
  36. package/bin/client/recorderv3/scriptTest.js +1 -1
  37. package/bin/client/recorderv3/services.js +694 -154
  38. package/bin/client/recorderv3/step_runner.js +315 -206
  39. package/bin/client/recorderv3/step_utils.js +473 -25
  40. package/bin/client/recorderv3/update_feature.js +9 -5
  41. package/bin/client/recorderv3/wbr_entry.js +61 -0
  42. package/bin/client/recording.js +1 -0
  43. package/bin/client/upload-service.js +3 -2
  44. package/bin/client/utils/socket_logger.js +132 -0
  45. package/bin/index.js +4 -1
  46. package/bin/logger.js +3 -2
  47. package/bin/min/consoleApi.min.cjs +2 -3
  48. package/bin/min/injectedScript.min.cjs +16 -16
  49. package/package.json +20 -10
@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
4
4
  import path from "path";
5
5
  import url from "url";
6
6
  import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
7
- import { NamesService } from "./services.js";
7
+ import { NamesService, PublishService, RemoteBrowserService } from "./services.js";
8
8
  import { BVTStepRunner } from "./step_runner.js";
9
9
  import { readFile, writeFile } from "fs/promises";
10
10
  import { updateStepDefinitions, loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
@@ -12,11 +12,12 @@ import { updateFeatureFile } from "./update_feature.js";
12
12
  import { parseStepTextParameters } from "../cucumber/utils.js";
13
13
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
14
14
  import chokidar from "chokidar";
15
- import logger from "../../logger.js";
16
15
  import { unEscapeNonPrintables } from "../cucumber/utils.js";
17
16
  import { findAvailablePort } from "../utils/index.js";
18
- import { Step } from "../cucumber/feature.js";
19
-
17
+ import socketLogger from "../utils/socket_logger.js";
18
+ import { tmpdir } from "os";
19
+ import { faker } from "@faker-js/faker/locale/en_US";
20
+ import { chromium } from "playwright-core";
20
21
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
21
22
 
22
23
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -29,7 +30,139 @@ export function getInitScript(config, options) {
29
30
  path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
30
31
  "utf8"
31
32
  );
32
- return preScript + recorderScript;
33
+ const clipboardBridgeScript = `
34
+ ;(() => {
35
+ if (window.__bvtRecorderClipboardBridgeInitialized) {
36
+ return;
37
+ }
38
+ window.__bvtRecorderClipboardBridgeInitialized = true;
39
+
40
+ const emitPayload = (payload, attempt = 0) => {
41
+ const reporter = window.__bvt_reportClipboard;
42
+ if (typeof reporter === "function") {
43
+ try {
44
+ reporter(payload);
45
+ } catch (error) {
46
+ console.warn("Clipboard bridge failed to report payload", error);
47
+ }
48
+ return;
49
+ }
50
+ if (attempt < 5) {
51
+ setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
52
+ }
53
+ };
54
+
55
+ const fileToBase64 = (file) => {
56
+ return new Promise((resolve) => {
57
+ try {
58
+ const reader = new FileReader();
59
+ reader.onload = () => {
60
+ const { result } = reader;
61
+ if (typeof result === "string") {
62
+ const index = result.indexOf("base64,");
63
+ resolve(index !== -1 ? result.substring(index + 7) : result);
64
+ return;
65
+ }
66
+ if (result instanceof ArrayBuffer) {
67
+ const bytes = new Uint8Array(result);
68
+ let binary = "";
69
+ const chunk = 0x8000;
70
+ for (let i = 0; i < bytes.length; i += chunk) {
71
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
72
+ }
73
+ resolve(btoa(binary));
74
+ return;
75
+ }
76
+ resolve(null);
77
+ };
78
+ reader.onerror = () => resolve(null);
79
+ reader.readAsDataURL(file);
80
+ } catch (error) {
81
+ console.warn("Clipboard bridge failed to serialize file", error);
82
+ resolve(null);
83
+ }
84
+ });
85
+ };
86
+
87
+ const handleClipboardEvent = async (event) => {
88
+ try {
89
+ const payload = { trigger: event.type };
90
+ const clipboardData = event.clipboardData;
91
+
92
+ if (clipboardData) {
93
+ try {
94
+ const text = clipboardData.getData("text/plain");
95
+ if (text) {
96
+ payload.text = text;
97
+ }
98
+ } catch (error) {
99
+ console.warn("Clipboard bridge could not read text/plain", error);
100
+ }
101
+
102
+ try {
103
+ const html = clipboardData.getData("text/html");
104
+ if (html) {
105
+ payload.html = html;
106
+ }
107
+ } catch (error) {
108
+ console.warn("Clipboard bridge could not read text/html", error);
109
+ }
110
+
111
+ const files = clipboardData.files;
112
+ if (files && files.length > 0) {
113
+ const serialized = [];
114
+ for (const file of files) {
115
+ const data = await fileToBase64(file);
116
+ if (data) {
117
+ serialized.push({
118
+ name: file.name,
119
+ type: file.type,
120
+ lastModified: file.lastModified,
121
+ data,
122
+ });
123
+ }
124
+ }
125
+ if (serialized.length > 0) {
126
+ payload.files = serialized;
127
+ }
128
+ }
129
+ }
130
+
131
+ if (!payload.text) {
132
+ try {
133
+ const selection = window.getSelection?.();
134
+ const selectionText = selection?.toString?.();
135
+ if (selectionText) {
136
+ payload.text = selectionText;
137
+ }
138
+ } catch {
139
+ // Ignore selection access errors.
140
+ }
141
+ }
142
+
143
+ emitPayload(payload);
144
+ } catch (error) {
145
+ console.warn("Clipboard bridge could not process event", error);
146
+ }
147
+ };
148
+
149
+ document.addEventListener(
150
+ "copy",
151
+ (event) => {
152
+ void handleClipboardEvent(event);
153
+ },
154
+ true
155
+ );
156
+ document.addEventListener(
157
+ "cut",
158
+ (event) => {
159
+ void handleClipboardEvent(event);
160
+ },
161
+ true
162
+ );
163
+ })();
164
+ `;
165
+ return preScript + recorderScript + clipboardBridgeScript;
33
166
  }
34
167
 
35
168
  async function evaluate(frame, script) {
@@ -45,7 +178,6 @@ async function evaluate(frame, script) {
45
178
  async function findNestedFrameSelector(frame, obj) {
46
179
  try {
47
180
  const parent = frame.parentFrame();
48
- if (parent) console.log(`Parent frame: ${JSON.stringify(parent)}`);
49
181
  if (!parent) return { children: obj };
50
182
  const frameElement = await frame.frameElement();
51
183
  if (!frameElement) return;
@@ -54,6 +186,7 @@ async function findNestedFrameSelector(frame, obj) {
54
186
  }, frameElement);
55
187
  return findNestedFrameSelector(parent, { children: obj, selectors });
56
188
  } catch (e) {
189
+ socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
57
190
  console.error(e);
58
191
  }
59
192
  }
@@ -150,17 +283,30 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
150
283
  };
151
284
  }
152
285
  default: {
286
+ socketLogger.error(`Action not supported: ${action.name}`);
153
287
  console.log("action not supported", action);
154
288
  throw new Error("action not supported");
155
289
  }
156
290
  }
157
291
  };
292
+ const diffPaths = (currentPath, newPath) => {
293
+ const currentDomain = new URL(currentPath).hostname;
294
+ const newDomain = new URL(newPath).hostname;
295
+ if (currentDomain !== newDomain) {
296
+ return true;
297
+ } else {
298
+ const currentRoute = new URL(currentPath).pathname;
299
+ const newRoute = new URL(newPath).pathname;
300
+ return currentRoute !== newRoute;
301
+ }
302
+ };
158
303
  /**
159
304
  * @typedef {Object} BVTRecorderInput
160
305
  * @property {string} envName
161
306
  * @property {string} projectDir
162
307
  * @property {string} TOKEN
163
308
  * @property {(name:string, data:any)=> void} sendEvent
309
+ * @property {Object} logger
164
310
  */
165
311
  export class BVTRecorder {
166
312
  #currentURL = "";
@@ -174,7 +320,6 @@ export class BVTRecorder {
174
320
  */
175
321
  constructor(initialState) {
176
322
  Object.assign(this, initialState);
177
- this.logger = logger;
178
323
  this.screenshotMap = new Map();
179
324
  this.snapshotMap = new Map();
180
325
  this.scenariosStepsMap = new Map();
@@ -184,40 +329,20 @@ export class BVTRecorder {
184
329
  projectDir: this.projectDir,
185
330
  logger: this.logger,
186
331
  });
187
- this.stepRunner = new BVTStepRunner({
188
- projectDir: this.projectDir,
189
- sendExecutionStatus: (data) => {
190
- if (data && data.type) {
191
- switch (data.type) {
192
- case "cmdExecutionStart":
193
- console.log("Sending cmdExecutionStart event for cmdId:", data);
194
- this.sendEvent(this.events.cmdExecutionStart, data);
195
- break;
196
- case "cmdExecutionSuccess":
197
- console.log("Sending cmdExecutionSuccess event for cmdId:", data);
198
- this.sendEvent(this.events.cmdExecutionSuccess, data);
199
- break;
200
- case "cmdExecutionError":
201
- console.log("Sending cmdExecutionError event for cmdId:", data);
202
- this.sendEvent(this.events.cmdExecutionError, data);
203
- break;
204
- case "interceptResults":
205
- console.log("Sending interceptResults event");
206
- this.sendEvent(this.events.interceptResults, data);
207
- break;
208
- default:
209
- console.warn("Unknown command execution status type:", data.type);
210
- break;
211
- }
212
- }
213
- },
214
- });
332
+ this.workspaceService = new PublishService(this.TOKEN);
215
333
  this.pageSet = new Set();
334
+ this.pageMetaDataSet = new Set();
216
335
  this.lastKnownUrlPath = "";
217
- // TODO: what is world?
218
336
  this.world = { attach: () => {} };
219
337
  this.shouldTakeScreenshot = true;
220
338
  this.watcher = null;
339
+ this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
340
+ this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
341
+ this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
342
+
343
+ if (existsSync(this.networkEventsFolder)) {
344
+ rmSync(this.networkEventsFolder, { recursive: true, force: true });
345
+ }
221
346
  }
222
347
  events = {
223
348
  onFrameNavigate: "BVTRecorder.onFrameNavigate",
@@ -232,12 +357,18 @@ export class BVTRecorder {
232
357
  cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
233
358
  cmdExecutionError: "BVTRecorder.cmdExecutionError",
234
359
  interceptResults: "BVTRecorder.interceptResults",
360
+ onDebugURLChange: "BVTRecorder.onDebugURLChange",
361
+ updateCommand: "BVTRecorder.updateCommand",
362
+ browserStateSync: "BrowserService.stateSync",
363
+ browserStateError: "BrowserService.stateError",
364
+ clipboardPush: "BrowserService.clipboardPush",
365
+ clipboardError: "BrowserService.clipboardError",
235
366
  };
236
367
  bindings = {
237
368
  __bvt_recordCommand: async ({ frame, page, context }, event) => {
238
369
  this.#activeFrame = frame;
239
370
  const nestFrmLoc = await findNestedFrameSelector(frame);
240
- console.log(`Time taken for action: ${event.statistics.time}`);
371
+ this.logger.info(`Time taken for action: ${event.statistics.time}`);
241
372
  await this.onAction({ ...event, nestFrmLoc });
242
373
  },
243
374
  __bvt_getMode: async () => {
@@ -256,12 +387,30 @@ export class BVTRecorder {
256
387
  await this.onClosePopup();
257
388
  },
258
389
  __bvt_log: async (src, message) => {
259
- // this.logger.info(message);
260
- console.log(`Inside Browser: ${message}`);
390
+ this.logger.info(`Inside Browser: ${message}`);
261
391
  },
262
392
  __bvt_getObject: (_src, obj) => {
263
393
  this.processObject(obj);
264
394
  },
395
+ __bvt_reportClipboard: async ({ page }, payload) => {
396
+ try {
397
+ if (!payload) {
398
+ return;
399
+ }
400
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
401
+ if (activePage && activePage !== page) {
402
+ return;
403
+ }
404
+ const pageUrl = typeof page?.url === "function" ? page.url() : null;
405
+ this.sendEvent(this.events.clipboardPush, {
406
+ data: payload,
407
+ trigger: payload?.trigger ?? "copy",
408
+ pageUrl,
409
+ });
410
+ } catch (error) {
411
+ this.logger.error("Error forwarding clipboard payload from page", error);
412
+ }
413
+ },
265
414
  };
266
415
 
267
416
  getSnapshot = async (attr) => {
@@ -319,10 +468,11 @@ export class BVTRecorder {
319
468
  }
320
469
 
321
470
  async _initBrowser({ url }) {
471
+ socketLogger.info("Only present in 1.0.1293-stage");
322
472
  this.#remoteDebuggerPort = await findAvailablePort();
323
473
  process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
324
474
 
325
- this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
475
+ // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
326
476
  this.world = { attach: () => {} };
327
477
 
328
478
  const ai_config_file = path.join(this.projectDir, "ai_config.json");
@@ -331,7 +481,7 @@ export class BVTRecorder {
331
481
  try {
332
482
  ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
333
483
  } catch (error) {
334
- console.error("Error reading ai_config.json", error);
484
+ this.logger.error("Error reading ai_config.json", error);
335
485
  }
336
486
  }
337
487
  this.config = ai_config;
@@ -343,18 +493,47 @@ export class BVTRecorder {
343
493
  ],
344
494
  };
345
495
 
346
- let startTime = Date.now();
347
496
  const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
348
- let stopTime = Date.now();
349
- this.logger.info(`Browser launched in ${(stopTime - startTime) / 1000} s`);
350
497
  this.bvtContext = bvtContext;
351
- const context = bvtContext.playContext;
352
- this.context = context;
498
+ this.stepRunner = new BVTStepRunner({
499
+ projectDir: this.projectDir,
500
+ sendExecutionStatus: (data) => {
501
+ if (data && data.type) {
502
+ switch (data.type) {
503
+ case "cmdExecutionStart":
504
+ this.sendEvent(this.events.cmdExecutionStart, data);
505
+ break;
506
+ case "cmdExecutionSuccess":
507
+ this.sendEvent(this.events.cmdExecutionSuccess, data);
508
+ break;
509
+ case "cmdExecutionError":
510
+ this.sendEvent(this.events.cmdExecutionError, data);
511
+ break;
512
+ case "interceptResults":
513
+ this.sendEvent(this.events.interceptResults, data);
514
+ break;
515
+ default:
516
+ break;
517
+ }
518
+ }
519
+ },
520
+ bvtContext: this.bvtContext,
521
+ });
522
+ this.context = bvtContext.playContext;
353
523
  this.web = bvtContext.stable || bvtContext.web;
354
524
  this.web.tryAllStrategies = true;
355
525
  this.page = bvtContext.page;
356
-
357
526
  this.pageSet.add(this.page);
527
+ if (process.env.REMOTE_RECORDER === "true") {
528
+ this.browserEmitter = new RemoteBrowserService({
529
+ CDP_CONNECT_URL: `http://localhost:${this.#remoteDebuggerPort}`,
530
+ context: this.context,
531
+ });
532
+ this.browserEmitter.on(this.events.browserStateSync, (state) => {
533
+ this.page = this.browserEmitter.getSelectedPage();
534
+ this.sendEvent(this.events.browserStateSync, state);
535
+ });
536
+ }
358
537
  this.lastKnownUrlPath = this._updateUrlPath();
359
538
  const browser = await this.context.browser();
360
539
  this.browser = browser;
@@ -367,6 +546,14 @@ export class BVTRecorder {
367
546
  this.web.onRestoreSaveState = (url) => {
368
547
  this._initBrowser({ url });
369
548
  };
549
+
550
+ // create a second browser for locator generation
551
+ this.backgroundBrowser = await chromium.launch({
552
+ headless: true,
553
+ });
554
+ this.backgroundContext = await this.backgroundBrowser.newContext({});
555
+ await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
556
+ await this.backgroundContext.newPage();
370
557
  }
371
558
  async onClosePopup() {
372
559
  // console.log("close popups");
@@ -381,13 +568,15 @@ export class BVTRecorder {
381
568
  }
382
569
  return;
383
570
  } catch (error) {
384
- console.error("Error evaluting in context:", error);
571
+ // console.error("Error evaluting in context:", error);
572
+ this.logger.error("Error evaluating in context:", error);
385
573
  }
386
574
  }
387
575
  }
388
576
 
389
577
  getMode() {
390
- console.log("getMode", this.#mode);
578
+ // console.log("getMode", this.#mode);
579
+ this.logger.info("Current mode:", this.#mode);
391
580
  return this.#mode;
392
581
  }
393
582
 
@@ -412,7 +601,7 @@ export class BVTRecorder {
412
601
 
413
602
  // eval init script on current tab
414
603
  // await this._initPage(this.page);
415
- this.#currentURL = new URL(url).pathname;
604
+ this.#currentURL = url;
416
605
 
417
606
  await this.page.dispatchEvent("html", "scroll");
418
607
  await delay(1000);
@@ -429,6 +618,8 @@ export class BVTRecorder {
429
618
  this.sendEvent(this.events.onBrowserClose);
430
619
  }
431
620
  } catch (error) {
621
+ this.logger.error("Error in page close event");
622
+ this.logger.error(error);
432
623
  console.error("Error in page close event");
433
624
  console.error(error);
434
625
  }
@@ -439,8 +630,10 @@ export class BVTRecorder {
439
630
  if (frame !== page.mainFrame()) return;
440
631
  this.handlePageTransition();
441
632
  } catch (error) {
633
+ this.logger.error("Error in handlePageTransition event");
634
+ this.logger.error(error);
442
635
  console.error("Error in handlePageTransition event");
443
- // console.error(error);
636
+ console.error(error);
444
637
  }
445
638
  try {
446
639
  if (frame !== this.#activeFrame) return;
@@ -450,15 +643,18 @@ export class BVTRecorder {
450
643
  element: { inputID: "frame" },
451
644
  });
452
645
 
453
- const newPath = new URL(frame.url()).pathname;
646
+ const newUrl = frame.url();
647
+ const newPath = new URL(newUrl).pathname;
454
648
  const newTitle = await frame.title();
455
- if (newPath !== this.#currentURL) {
649
+ const changed = diffPaths(this.#currentURL, newUrl);
650
+
651
+ if (changed) {
456
652
  this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
457
- this.#currentURL = newPath;
653
+ this.#currentURL = newUrl;
458
654
  }
459
- // await this._setRecordingMode(frame);
460
- // await this._initPage(page);
461
655
  } catch (error) {
656
+ this.logger.error("Error in frame navigate event");
657
+ this.logger.error(error);
462
658
  console.error("Error in frame navigate event");
463
659
  // console.error(error);
464
660
  }
@@ -541,13 +737,9 @@ export class BVTRecorder {
541
737
 
542
738
  try {
543
739
  const result = await client.send("Page.getNavigationHistory");
544
- // console.log("Navigation History:", result);
545
740
  const entries = result.entries;
546
741
  const currentIndex = result.currentIndex;
547
742
 
548
- // ignore if currentIndex is not the last entry
549
- // if (currentIndex !== entries.length - 1) return;
550
-
551
743
  const currentEntry = entries[currentIndex];
552
744
  const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
553
745
  this.previousIndex = currentIndex;
@@ -560,6 +752,8 @@ export class BVTRecorder {
560
752
  navigationAction: transitionInfo.action,
561
753
  };
562
754
  } catch (error) {
755
+ this.logger.error("Error in getCurrentTransition event");
756
+ this.logger.error(error);
563
757
  console.error("Error in getTransistionType event", error);
564
758
  } finally {
565
759
  await client.detach();
@@ -628,6 +822,8 @@ export class BVTRecorder {
628
822
  // add listener for frame navigation on new tab
629
823
  this._addFrameNavigateListener(page);
630
824
  } catch (error) {
825
+ this.logger.error("Error in page event");
826
+ this.logger.error(error);
631
827
  console.error("Error in page event");
632
828
  console.error(error);
633
829
  }
@@ -669,6 +865,7 @@ export class BVTRecorder {
669
865
  const { data } = await client.send("Page.captureScreenshot", { format: "png" });
670
866
  return data;
671
867
  } catch (error) {
868
+ this.logger.error("Error in taking browser screenshot");
672
869
  console.error("Error in taking browser screenshot", error);
673
870
  } finally {
674
871
  await client.detach();
@@ -684,6 +881,52 @@ export class BVTRecorder {
684
881
  console.error("Error in saving screenshot: ", error);
685
882
  }
686
883
  }
884
+ async generateLocators(event) {
885
+ const snapshotDetails = event.snapshotDetails;
886
+ if (!snapshotDetails) {
887
+ throw new Error("No snapshot details found");
888
+ }
889
+ const mode = event.mode;
890
+ const inputID = event.element.inputID;
891
+
892
+ const { id, contextId, doc } = snapshotDetails;
893
+ // const selector = `[data-blinq-id="${id}"]`;
894
+ const newPage = await this.backgroundContext.newPage();
895
+ await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
896
+ const locatorsObj = await newPage.evaluate(
897
+ ([id, contextId, mode]) => {
898
+ const recorder = window.__bvt_Recorder;
899
+ const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
900
+ const el = document.querySelector(`[data-blinq-id="${id}"]`);
901
+ if (contextElement) {
902
+ const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
903
+ return result;
904
+ }
905
+ const isRecordingText = mode === "recordingText";
906
+ return recorder.locatorGenerator.getElementLocators(el, {
907
+ excludeText: isRecordingText,
908
+ });
909
+ },
910
+ [id, contextId, mode]
911
+ );
912
+
913
+ console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
914
+ await newPage.close();
915
+ if (event.nestFrmLoc?.children) {
916
+ locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
917
+ }
918
+
919
+ this.sendEvent(this.events.updateCommand, {
920
+ locators: {
921
+ locators: locatorsObj.locators,
922
+ nestFrmLoc: locatorsObj.nestFrmLoc,
923
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
924
+ },
925
+ allStrategyLocators: locatorsObj.allStrategyLocators,
926
+ inputID,
927
+ });
928
+ // const
929
+ }
687
930
  async onAction(event) {
688
931
  this._updateUrlPath();
689
932
  // const locators = this.overlayLocators(event);
@@ -697,25 +940,26 @@ export class BVTRecorder {
697
940
  event.mode === "recordingHover",
698
941
  event.mode === "multiInspecting"
699
942
  ),
700
- locators: {
701
- locators: event.locators,
702
- iframe_src: !event.frame.isTop ? event.frame.url : undefined,
703
- },
704
- allStrategyLocators: event.allStrategyLocators,
943
+ // locators: {
944
+ // locators: event.locators,
945
+ // iframe_src: !event.frame.isTop ? event.frame.url : undefined,
946
+ // },
947
+ // allStrategyLocators: event.allStrategyLocators,
705
948
  url: event.frame.url,
706
949
  title: event.frame.title,
707
950
  extract: {},
708
951
  lastKnownUrlPath: this.lastKnownUrlPath,
709
952
  };
710
- if (event.nestFrmLoc?.children) {
711
- cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
712
- }
953
+ // if (event.nestFrmLoc?.children) {
954
+ // cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
955
+ // }
713
956
  // this.logger.info({ event });
714
957
  if (this.shouldTakeScreenshot) {
715
958
  await this.storeScreenshot(event);
716
959
  }
717
960
  this.sendEvent(this.events.onNewCommand, cmdEvent);
718
961
  this._updateUrlPath();
962
+ await this.generateLocators(event);
719
963
  }
720
964
  _updateUrlPath() {
721
965
  try {
@@ -737,7 +981,6 @@ export class BVTRecorder {
737
981
  this.previousHistoryLength = null;
738
982
  this.previousUrl = null;
739
983
  this.previousEntries = null;
740
-
741
984
  await closeContext();
742
985
  this.pageSet.clear();
743
986
  }
@@ -789,7 +1032,6 @@ export class BVTRecorder {
789
1032
  }
790
1033
 
791
1034
  async startRecordingInput() {
792
- console.log("startRecordingInput");
793
1035
  await this.setMode("recordingInput");
794
1036
  }
795
1037
  async stopRecordingInput() {
@@ -813,9 +1055,17 @@ export class BVTRecorder {
813
1055
  }
814
1056
 
815
1057
  async abortExecution() {
816
- this.bvtContext.web.abortedExecution = true;
817
1058
  await this.stepRunner.abortExecution();
818
1059
  }
1060
+
1061
+ async pauseExecution({ cmdId }) {
1062
+ await this.stepRunner.pauseExecution(cmdId);
1063
+ }
1064
+
1065
+ async resumeExecution({ cmdId }) {
1066
+ await this.stepRunner.resumeExecution(cmdId);
1067
+ }
1068
+
819
1069
  async dealyedRevertMode() {
820
1070
  const timerId = setTimeout(async () => {
821
1071
  await this.revertMode();
@@ -824,18 +1074,24 @@ export class BVTRecorder {
824
1074
  }
825
1075
  async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
826
1076
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1077
+
1078
+ const env = path.basename(this.envName, ".json");
827
1079
  const _env = {
828
1080
  TOKEN: this.TOKEN,
829
1081
  TEMP_RUN: true,
830
1082
  REPORT_FOLDER: this.bvtContext.reportFolder,
831
1083
  BLINQ_ENV: this.envName,
832
- STORE_DETAILED_NETWORK_DATA: listenNetwork ? "true" : "false",
833
- CURRENT_STEP_ID: step.id,
1084
+ DEBUG: "blinq:route",
1085
+ BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
834
1086
  };
835
1087
 
836
1088
  this.bvtContext.navigate = true;
837
1089
  this.bvtContext.loadedRoutes = null;
838
- this.bvtContext.web.abortedExecution = false;
1090
+ if (listenNetwork) {
1091
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
1092
+ } else {
1093
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1094
+ }
839
1095
  for (const [key, value] of Object.entries(_env)) {
840
1096
  process.env[key] = value;
841
1097
  }
@@ -871,13 +1127,26 @@ export class BVTRecorder {
871
1127
  delete process.env[key];
872
1128
  }
873
1129
  this.bvtContext.navigate = false;
874
- this.bvtContext.web.abortedExecution = false;
875
1130
  }
876
1131
  }
877
- async saveScenario({ scenario, featureName, override, isSingleStep }) {
878
- await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
879
- if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
880
- await this.cleanup({ tags: scenario.tags });
1132
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
1133
+ // await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
1134
+ // if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
1135
+ const res = await this.workspaceService.saveScenario({
1136
+ scenario,
1137
+ featureName,
1138
+ override,
1139
+ isSingleStep,
1140
+ branch,
1141
+ isEditing,
1142
+ projectId: path.basename(this.projectDir),
1143
+ env: env ?? this.envName,
1144
+ });
1145
+ if (res.success) {
1146
+ await this.cleanup({ tags: scenario.tags });
1147
+ } else {
1148
+ throw new Error(res.message || "Error saving scenario");
1149
+ }
881
1150
  }
882
1151
  async getImplementedSteps() {
883
1152
  const stepsAndScenarios = await getImplementedSteps(this.projectDir);
@@ -961,10 +1230,11 @@ export class BVTRecorder {
961
1230
  if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
962
1231
  try {
963
1232
  const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
964
- this.logger.info("Test data", testData);
1233
+ // this.logger.info("Test data", testData);
965
1234
  this.sendEvent(this.events.getTestData, testData);
966
1235
  } catch (e) {
967
- this.logger.error("Error reading test data file", e);
1236
+ // this.logger.error("Error reading test data file", e);
1237
+ console.log("Error reading test data file", e);
968
1238
  }
969
1239
  }
970
1240
 
@@ -973,10 +1243,12 @@ export class BVTRecorder {
973
1243
  this.watcher.on("all", async (event, path) => {
974
1244
  try {
975
1245
  const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
976
- this.logger.info("Test data", testData);
1246
+ // this.logger.info("Test data", testData);
1247
+ console.log("Test data changed", testData);
977
1248
  this.sendEvent(this.events.getTestData, testData);
978
1249
  } catch (e) {
979
- this.logger.error("Error reading test data file", e);
1250
+ // this.logger.error("Error reading test data file", e);
1251
+ console.log("Error reading test data file", e);
980
1252
  }
981
1253
  });
982
1254
  }
@@ -1009,7 +1281,7 @@ export class BVTRecorder {
1009
1281
  .filter((file) => file.endsWith(".feature"))
1010
1282
  .map((file) => path.join(this.projectDir, "features", file));
1011
1283
  try {
1012
- const parsedFiles = featureFiles.map((file) => parseFeatureFile(file));
1284
+ const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
1013
1285
  const output = {};
1014
1286
  parsedFiles.forEach((file) => {
1015
1287
  if (!file.feature) return;
@@ -1037,7 +1309,7 @@ export class BVTRecorder {
1037
1309
  loadExistingScenario({ featureName, scenarioName }) {
1038
1310
  const step_definitions = loadStepDefinitions(this.projectDir);
1039
1311
  const featureFilePath = path.join(this.projectDir, "features", featureName);
1040
- const gherkinDoc = parseFeatureFile(featureFilePath);
1312
+ const gherkinDoc = this.parseFeatureFile(featureFilePath);
1041
1313
  const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1042
1314
 
1043
1315
  const steps = [];
@@ -1196,20 +1468,356 @@ export class BVTRecorder {
1196
1468
  await this.cleanupExecution({ tags });
1197
1469
  await this.initExecution({ tags });
1198
1470
  }
1199
- }
1200
1471
 
1201
- const parseFeatureFile = (featureFilePath) => {
1202
- try {
1203
- let id = 0;
1204
- const uuidFn = () => (++id).toString(16);
1205
- const builder = new AstBuilder(uuidFn);
1206
- const matcher = new GherkinClassicTokenMatcher();
1207
- const parser = new Parser(builder, matcher);
1208
- const source = readFileSync(featureFilePath, "utf8");
1209
- const gherkinDocument = parser.parse(source);
1210
- return gherkinDocument;
1211
- } catch (e) {
1212
- console.log(e);
1472
+ parseFeatureFile(featureFilePath) {
1473
+ try {
1474
+ let id = 0;
1475
+ const uuidFn = () => (++id).toString(16);
1476
+ const builder = new AstBuilder(uuidFn);
1477
+ const matcher = new GherkinClassicTokenMatcher();
1478
+ const parser = new Parser(builder, matcher);
1479
+ const source = readFileSync(featureFilePath, "utf8");
1480
+ const gherkinDocument = parser.parse(source);
1481
+ return gherkinDocument;
1482
+ } catch (e) {
1483
+ this.logger.error(`Error parsing feature file: ${featureFilePath}`);
1484
+ console.log(e);
1485
+ }
1486
+ return {};
1213
1487
  }
1214
- return {};
1215
- };
1488
+
1489
+ stopRecordingNetwork(input) {
1490
+ if (this.bvtContext) {
1491
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1492
+ }
1493
+ }
1494
+
1495
+ async fakeParams(params) {
1496
+ const newFakeParams = {};
1497
+ Object.keys(params).forEach((key) => {
1498
+ if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
1499
+ newFakeParams[key] = params[key];
1500
+ return;
1501
+ }
1502
+
1503
+ try {
1504
+ const value = params[key].substring(2, params[key].length - 2).trim();
1505
+ const faking = value.split("(")[0].split(".");
1506
+ let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
1507
+ argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
1508
+ let fakeFunc = faker;
1509
+ faking.forEach((f) => {
1510
+ fakeFunc = fakeFunc[f];
1511
+ });
1512
+ const newValue = fakeFunc(argument);
1513
+ newFakeParams[key] = newValue;
1514
+ } catch (error) {
1515
+ newFakeParams[key] = params[key];
1516
+ }
1517
+ });
1518
+
1519
+ return newFakeParams;
1520
+ }
1521
+
1522
+ async getBrowserState() {
1523
+ try {
1524
+ const state = await this.browserEmitter?.getState();
1525
+ this.sendEvent(this.events.browserStateSync, state);
1526
+ } catch (error) {
1527
+ this.logger.error("Error getting browser state:", error);
1528
+ this.sendEvent(this.events.browserStateError, {
1529
+ message: "Error getting browser state",
1530
+ code: "GET_STATE_ERROR",
1531
+ });
1532
+ }
1533
+ }
1534
+
1535
+ async applyClipboardPayload(message) {
1536
+ const payload = message?.data ?? message;
1537
+ if (!payload) {
1538
+ return;
1539
+ }
1540
+
1541
+ try {
1542
+ if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
1543
+ await this.browserEmitter.applyClipboardPayload(payload);
1544
+ return;
1545
+ }
1546
+
1547
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1548
+ if (!activePage) {
1549
+ this.logger.warn("No active page available to apply clipboard payload");
1550
+ return;
1551
+ }
1552
+
1553
+ await this.injectClipboardIntoPage(activePage, payload);
1554
+ } catch (error) {
1555
+ this.logger.error("Error applying clipboard payload to page", error);
1556
+ this.sendEvent(this.events.clipboardError, {
1557
+ message: "Failed to apply clipboard contents to the remote session",
1558
+ trigger: message?.trigger ?? "paste",
1559
+ });
1560
+ }
1561
+ }
1562
+
1563
+ async injectClipboardIntoPage(page, payload) {
1564
+ if (!page) {
1565
+ return;
1566
+ }
1567
+
1568
+ try {
1569
+ await page
1570
+ .context()
1571
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1572
+ .catch(() => {});
1573
+ await page.evaluate(async (clipboardPayload) => {
1574
+ const toArrayBuffer = (base64) => {
1575
+ if (!base64) {
1576
+ return null;
1577
+ }
1578
+ const binaryString = atob(base64);
1579
+ const len = binaryString.length;
1580
+ const bytes = new Uint8Array(len);
1581
+ for (let i = 0; i < len; i += 1) {
1582
+ bytes[i] = binaryString.charCodeAt(i);
1583
+ }
1584
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1585
+ };
1586
+
1587
+ const createFileFromPayload = (filePayload) => {
1588
+ const buffer = toArrayBuffer(filePayload?.data);
1589
+ if (!buffer) {
1590
+ return null;
1591
+ }
1592
+ const name = filePayload?.name || "clipboard-file";
1593
+ const type = filePayload?.type || "application/octet-stream";
1594
+ const lastModified = filePayload?.lastModified ?? Date.now();
1595
+ try {
1596
+ return new File([buffer], name, { type, lastModified });
1597
+ } catch (error) {
1598
+ console.warn("Clipboard bridge could not recreate File object", error);
1599
+ return null;
1600
+ }
1601
+ };
1602
+
1603
+ let dataTransfer = null;
1604
+ try {
1605
+ dataTransfer = new DataTransfer();
1606
+ } catch (error) {
1607
+ console.warn("Clipboard bridge could not create DataTransfer", error);
1608
+ }
1609
+
1610
+ if (dataTransfer) {
1611
+ if (clipboardPayload?.text) {
1612
+ try {
1613
+ dataTransfer.setData("text/plain", clipboardPayload.text);
1614
+ } catch (error) {
1615
+ console.warn("Clipboard bridge failed to set text/plain", error);
1616
+ }
1617
+ }
1618
+ if (clipboardPayload?.html) {
1619
+ try {
1620
+ dataTransfer.setData("text/html", clipboardPayload.html);
1621
+ } catch (error) {
1622
+ console.warn("Clipboard bridge failed to set text/html", error);
1623
+ }
1624
+ }
1625
+ if (Array.isArray(clipboardPayload?.files)) {
1626
+ for (const filePayload of clipboardPayload.files) {
1627
+ const file = createFileFromPayload(filePayload);
1628
+ if (file) {
1629
+ try {
1630
+ dataTransfer.items.add(file);
1631
+ } catch (error) {
1632
+ console.warn("Clipboard bridge failed to append file", error);
1633
+ }
1634
+ }
1635
+ }
1636
+ }
1637
+ }
1638
+
1639
+ let target = document.activeElement || document.body;
1640
+ if (!target) {
1641
+ target = document.body || null;
1642
+ }
1643
+
1644
+ let pasteHandled = false;
1645
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
1646
+ try {
1647
+ const clipboardEvent = new ClipboardEvent("paste", {
1648
+ clipboardData: dataTransfer,
1649
+ bubbles: true,
1650
+ cancelable: true,
1651
+ });
1652
+ pasteHandled = target.dispatchEvent(clipboardEvent);
1653
+ } catch (error) {
1654
+ console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
1655
+ }
1656
+ }
1657
+
1658
+ if (pasteHandled) {
1659
+ return;
1660
+ }
1661
+
1662
+ const callLegacyExecCommand = (command, value) => {
1663
+ const execCommand = document && document["execCommand"];
1664
+ if (typeof execCommand === "function") {
1665
+ try {
1666
+ return execCommand.call(document, command, false, value);
1667
+ } catch (error) {
1668
+ console.warn("Clipboard bridge failed to execute legacy command", error);
1669
+ }
1670
+ }
1671
+ return false;
1672
+ };
1673
+
1674
+ if (clipboardPayload?.html) {
1675
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
1676
+ if (inserted) {
1677
+ return;
1678
+ }
1679
+ try {
1680
+ const selection = window.getSelection?.();
1681
+ if (selection && selection.rangeCount > 0) {
1682
+ const range = selection.getRangeAt(0);
1683
+ range.deleteContents();
1684
+ const fragment = range.createContextualFragment(clipboardPayload.html);
1685
+ range.insertNode(fragment);
1686
+ range.collapse(false);
1687
+ return;
1688
+ }
1689
+ } catch (error) {
1690
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
1691
+ }
1692
+ }
1693
+
1694
+ if (clipboardPayload?.text) {
1695
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
1696
+ if (inserted) {
1697
+ return;
1698
+ }
1699
+ try {
1700
+ const selection = window.getSelection?.();
1701
+ if (selection && selection.rangeCount > 0) {
1702
+ const range = selection.getRangeAt(0);
1703
+ range.deleteContents();
1704
+ range.insertNode(document.createTextNode(clipboardPayload.text));
1705
+ range.collapse(false);
1706
+ return;
1707
+ }
1708
+ } catch (error) {
1709
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
1710
+ }
1711
+ }
1712
+
1713
+ if (clipboardPayload?.text && target && "value" in target) {
1714
+ try {
1715
+ const input = target;
1716
+ const start = input.selectionStart ?? input.value.length ?? 0;
1717
+ const end = input.selectionEnd ?? input.value.length ?? 0;
1718
+ const value = input.value ?? "";
1719
+ const text = clipboardPayload.text;
1720
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
1721
+ const caret = start + text.length;
1722
+ if (typeof input.setSelectionRange === "function") {
1723
+ input.setSelectionRange(caret, caret);
1724
+ }
1725
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1726
+ } catch (error) {
1727
+ console.warn("Clipboard bridge failed to mutate input element", error);
1728
+ }
1729
+ }
1730
+ }, payload);
1731
+ } catch (error) {
1732
+ throw error;
1733
+ }
1734
+ }
1735
+
1736
+ async createTab(url) {
1737
+ try {
1738
+ await this.browserEmitter?.createTab(url);
1739
+ } catch (error) {
1740
+ this.logger.error("Error creating tab:", error);
1741
+ this.sendEvent(this.events.browserStateError, {
1742
+ message: "Error creating tab",
1743
+ code: "CREATE_TAB_ERROR",
1744
+ });
1745
+ }
1746
+ }
1747
+
1748
+ async closeTab({ pageId }) {
1749
+ try {
1750
+ await this.browserEmitter?.closeTab(pageId);
1751
+ } catch (error) {
1752
+ this.logger.error("Error closing tab:", error);
1753
+ this.sendEvent(this.events.browserStateError, {
1754
+ message: "Error closing tab",
1755
+ code: "CLOSE_TAB_ERROR",
1756
+ });
1757
+ }
1758
+ }
1759
+
1760
+ async selectTab({ pageId }) {
1761
+ try {
1762
+ await this.browserEmitter?.selectTab(pageId);
1763
+ } catch (error) {
1764
+ this.logger.error("Error selecting tab:", error);
1765
+ this.sendEvent(this.events.browserStateError, {
1766
+ message: "Error selecting tab",
1767
+ code: "SELECT_TAB_ERROR",
1768
+ });
1769
+ }
1770
+ }
1771
+
1772
+ async navigateTab({ pageId, url }) {
1773
+ try {
1774
+ if (!pageId || !url) {
1775
+ this.logger.error("navigateTab called without pageId or url", { pageId, url });
1776
+ return;
1777
+ }
1778
+ await this.browserEmitter?.navigateTab(pageId, url);
1779
+ } catch (error) {
1780
+ this.logger.error("Error navigating tab:", error);
1781
+ this.sendEvent(this.events.browserStateError, {
1782
+ message: "Error navigating tab",
1783
+ code: "NAVIGATE_TAB_ERROR",
1784
+ });
1785
+ }
1786
+ }
1787
+
1788
+ async reloadTab(pageId) {
1789
+ try {
1790
+ await this.browserEmitter?.reloadTab(pageId);
1791
+ } catch (error) {
1792
+ this.logger.error("Error reloading tab:", error);
1793
+ this.sendEvent(this.events.browserStateError, {
1794
+ message: "Error reloading tab",
1795
+ code: "RELOAD_TAB_ERROR",
1796
+ });
1797
+ }
1798
+ }
1799
+
1800
+ async goBack(pageId) {
1801
+ try {
1802
+ await this.browserEmitter?.goBack(pageId);
1803
+ } catch (error) {
1804
+ this.logger.error("Error navigating back:", error);
1805
+ this.sendEvent(this.events.browserStateError, {
1806
+ message: "Error navigating back",
1807
+ code: "GO_BACK_ERROR",
1808
+ });
1809
+ }
1810
+ }
1811
+
1812
+ async goForward(pageId) {
1813
+ try {
1814
+ await this.browserEmitter?.goForward(pageId);
1815
+ } catch (error) {
1816
+ this.logger.error("Error navigating forward:", error);
1817
+ this.sendEvent(this.events.browserStateError, {
1818
+ message: "Error navigating forward",
1819
+ code: "GO_FORWARD_ERROR",
1820
+ });
1821
+ }
1822
+ }
1823
+ }