@dev-blinq/cucumber_client 1.0.1475-dev → 1.0.1475-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 (47) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +49 -49
  2. package/bin/assets/scripts/recorder.js +87 -34
  3. package/bin/assets/scripts/snapshot_capturer.js +10 -17
  4. package/bin/assets/scripts/unique_locators.js +78 -28
  5. package/bin/assets/templates/_hooks_template.txt +6 -2
  6. package/bin/assets/templates/utils_template.txt +16 -16
  7. package/bin/client/code_cleanup/codemod/find_harcoded_locators.js +173 -0
  8. package/bin/client/code_cleanup/codemod/fix_hardcoded_locators.js +247 -0
  9. package/bin/client/code_cleanup/utils.js +16 -7
  10. package/bin/client/code_gen/code_inversion.js +125 -1
  11. package/bin/client/code_gen/duplication_analysis.js +2 -1
  12. package/bin/client/code_gen/function_signature.js +8 -0
  13. package/bin/client/code_gen/index.js +4 -0
  14. package/bin/client/code_gen/page_reflection.js +90 -9
  15. package/bin/client/code_gen/playwright_codeget.js +173 -77
  16. package/bin/client/codemod/find_harcoded_locators.js +173 -0
  17. package/bin/client/codemod/fix_hardcoded_locators.js +247 -0
  18. package/bin/client/codemod/index.js +8 -0
  19. package/bin/client/codemod/locators_array/find_misstructured_elements.js +148 -0
  20. package/bin/client/codemod/locators_array/fix_misstructured_elements.js +144 -0
  21. package/bin/client/codemod/locators_array/index.js +114 -0
  22. package/bin/client/codemod/types.js +1 -0
  23. package/bin/client/cucumber/feature.js +4 -17
  24. package/bin/client/cucumber/steps_definitions.js +17 -12
  25. package/bin/client/recorderv3/bvt_init.js +310 -0
  26. package/bin/client/recorderv3/bvt_recorder.js +1560 -1183
  27. package/bin/client/recorderv3/constants.js +45 -0
  28. package/bin/client/recorderv3/implemented_steps.js +2 -0
  29. package/bin/client/recorderv3/index.js +3 -293
  30. package/bin/client/recorderv3/mixpanel.js +39 -0
  31. package/bin/client/recorderv3/services.js +839 -142
  32. package/bin/client/recorderv3/step_runner.js +36 -7
  33. package/bin/client/recorderv3/step_utils.js +316 -98
  34. package/bin/client/recorderv3/update_feature.js +85 -37
  35. package/bin/client/recorderv3/utils.js +80 -0
  36. package/bin/client/recorderv3/wbr_entry.js +61 -0
  37. package/bin/client/recording.js +1 -0
  38. package/bin/client/types/locators.js +2 -0
  39. package/bin/client/upload-service.js +2 -0
  40. package/bin/client/utils/app_dir.js +21 -0
  41. package/bin/client/utils/socket_logger.js +100 -125
  42. package/bin/index.js +5 -0
  43. package/package.json +21 -6
  44. package/bin/client/recorderv3/app_dir.js +0 -23
  45. package/bin/client/recorderv3/network.js +0 -299
  46. package/bin/client/recorderv3/scriptTest.js +0 -5
  47. package/bin/client/recorderv3/ws_server.js +0 -72
@@ -2,150 +2,847 @@ import { readdirSync, readFileSync } from "fs";
2
2
  import { getRunsServiceBaseURL } from "../utils/index.js";
3
3
  import { axiosClient } from "../utils/axiosClient.js";
4
4
  import path from "path";
5
-
5
+ import { EventEmitter } from "events";
6
+ import { v4 as uuidv4 } from "uuid"; // Import uuid
6
7
  export class NamesService {
7
- constructor({ screenshotMap, TOKEN, projectDir, logger }) {
8
- this.screenshotMap = screenshotMap;
9
- this.TOKEN = TOKEN;
10
- this.projectDir = projectDir;
11
- this.logger = logger;
12
- }
13
- async generateStepName({ commands, stepsNames, parameters, map }) {
14
- let screenshot = this.screenshotMap.get(commands[commands.length - 1].inputID);
15
- if (!screenshot && commands.length > 1) {
16
- screenshot = this.screenshotMap.get(commands[commands.length - 2].inputID);
17
- }
18
-
19
- const url = `${getRunsServiceBaseURL()}/generate-step-information/generate`;
20
- const TIMEOUT = 120; // 2 minutes
21
- const { data } = await axiosClient({
22
- url,
23
- method: "POST",
24
- data: {
25
- commands,
26
- stepsNames,
27
- parameters,
28
- //screenshot,
29
- map,
30
- },
31
- headers: {
32
- Authorization: `Bearer ${this.TOKEN}`,
33
- "X-Source": "recorder",
34
- },
35
- });
36
- const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
37
- let result = { status: 500 };
38
- for (let i = 0; i < TIMEOUT; i++) {
39
- try {
40
- const action = await axiosClient({
41
- url: actionUrl,
42
- method: "GET",
43
- headers: {
44
- Authorization: `Bearer ${this.TOKEN}`,
45
- "X-Source": "recorder",
46
- },
8
+ screenshotMap;
9
+ TOKEN;
10
+ projectDir;
11
+ logger;
12
+ constructor({ screenshotMap, TOKEN, projectDir, logger }) {
13
+ this.screenshotMap = screenshotMap;
14
+ this.TOKEN = TOKEN;
15
+ this.projectDir = projectDir;
16
+ this.logger = logger;
17
+ }
18
+ //@ts-expect-error
19
+ async generateStepName({ commands, stepsNames, parameters, map }) {
20
+ let screenshot = this.screenshotMap.get(commands[commands.length - 1].inputID);
21
+ if (!screenshot && commands.length > 1) {
22
+ screenshot = this.screenshotMap.get(commands[commands.length - 2].inputID);
23
+ }
24
+ const url = `${getRunsServiceBaseURL()}/generate-step-information/generate`;
25
+ const TIMEOUT = 120; // 2 minutes
26
+ const { data } = await axiosClient({
27
+ url,
28
+ method: "POST",
29
+ data: {
30
+ commands,
31
+ stepsNames,
32
+ parameters,
33
+ //screenshot,
34
+ map,
35
+ },
36
+ headers: {
37
+ Authorization: `Bearer ${this.TOKEN}`,
38
+ "X-Source": "recorder",
39
+ },
40
+ });
41
+ const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
42
+ let result = { status: 500 };
43
+ for (let i = 0; i < TIMEOUT; i++) {
44
+ try {
45
+ const action = await axiosClient({
46
+ url: actionUrl,
47
+ method: "GET",
48
+ headers: {
49
+ Authorization: `Bearer ${this.TOKEN}`,
50
+ "X-Source": "recorder",
51
+ },
52
+ });
53
+ if (action.data.status) {
54
+ result = action;
55
+ break;
56
+ }
57
+ }
58
+ catch (error) {
59
+ if (i === TIMEOUT - 1) {
60
+ this.logger.error("Timeout while generating step details: ", error instanceof Error ? error : { message: JSON.stringify(error) });
61
+ console.error("Timeout while generating step details: ", error instanceof Error ? error.message : error);
62
+ }
63
+ }
64
+ await new Promise((resolve) => setTimeout(resolve, 1000));
65
+ }
66
+ if (result.status !== 200) {
67
+ return { success: false, message: "Error while generating step details" };
68
+ }
69
+ return result.data;
70
+ }
71
+ async generateScenarioAndFeatureNames(scenarioAsText) {
72
+ if (!this.projectDir) {
73
+ throw new Error("Project directory not found");
74
+ }
75
+ // read all files with .feature extension into an object {files:[{name: "", content: ""}]}
76
+ const featureFiles = readdirSync(path.join(this.projectDir, "features"));
77
+ const featureFilesContent = featureFiles
78
+ .filter((file) => file.endsWith(".feature"))
79
+ .map((file) => {
80
+ return {
81
+ name: file,
82
+ content: readFileSync(path.join(this.projectDir, "features", file), "utf8"),
83
+ };
47
84
  });
48
-
49
- if (action.data.status) {
50
- result = action;
51
- break;
52
- }
53
- } catch (error) {
54
- if (i === TIMEOUT - 1) {
55
- this.logger.error("Timeout while generating step details: ", error);
56
- console.error("Timeout while generating step details: ", error);
57
- }
58
- }
59
-
60
- await new Promise((resolve) => setTimeout(resolve, 1000));
61
- }
62
-
63
- if (result.status !== 200) {
64
- return { success: false, message: "Error while generating step details" };
65
- }
66
- return result.data;
67
- }
68
- async generateScenarioAndFeatureNames(scenarioAsText) {
69
- if (!this.projectDir) {
70
- throw new Error("Project directory not found");
71
- }
72
- // read all files with .feature extension into an object {files:[{name: "", content: ""}]}
73
- const featureFiles = readdirSync(path.join(this.projectDir, "features"));
74
- const featureFilesContent = featureFiles
75
- .filter((file) => file.endsWith(".feature"))
76
- .map((file) => {
77
- return {
78
- name: file,
79
- content: readFileSync(path.join(this.projectDir, "features", file), "utf8"),
85
+ const genObject = {
86
+ files: featureFilesContent,
87
+ scenario: scenarioAsText,
80
88
  };
81
- });
82
- const genObject = {
83
- files: featureFilesContent,
84
- scenario: scenarioAsText,
85
- };
86
-
87
- // get screenshot for the last command
88
- const url = `${getRunsServiceBaseURL()}/generate-step-information/generate_scenario_feature`;
89
- const TIMEOUT = 120; // 2 minutes
90
- const { data } = await axiosClient({
91
- url,
92
- method: "POST",
93
- data: {
94
- ...genObject,
95
- },
96
- headers: {
97
- Authorization: `Bearer ${this.TOKEN}`,
98
- "X-Source": "recorder",
99
- },
100
- });
101
- const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
102
- let result = { status: 500 };
103
- for (let i = 0; i < TIMEOUT; i++) {
104
- try {
105
- const action = await axiosClient({
106
- url: actionUrl,
107
- method: "GET",
108
- headers: {
109
- Authorization: `Bearer ${this.TOKEN}`,
110
- "X-Source": "recorder",
111
- },
89
+ // get screenshot for the last command
90
+ const url = `${getRunsServiceBaseURL()}/generate-step-information/generate_scenario_feature`;
91
+ const TIMEOUT = 120; // 2 minutes
92
+ const { data } = await axiosClient({
93
+ url,
94
+ method: "POST",
95
+ data: {
96
+ ...genObject,
97
+ },
98
+ headers: {
99
+ Authorization: `Bearer ${this.TOKEN}`,
100
+ "X-Source": "recorder",
101
+ },
102
+ });
103
+ const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
104
+ let result = { status: 500 };
105
+ for (let i = 0; i < TIMEOUT; i++) {
106
+ try {
107
+ const action = await axiosClient({
108
+ url: actionUrl,
109
+ method: "GET",
110
+ headers: {
111
+ Authorization: `Bearer ${this.TOKEN}`,
112
+ "X-Source": "recorder",
113
+ },
114
+ });
115
+ if (action.data.status) {
116
+ result = action;
117
+ break;
118
+ }
119
+ }
120
+ catch (error) {
121
+ if (i === TIMEOUT - 1) {
122
+ console.error("Timeout while generating step details: ", error);
123
+ }
124
+ }
125
+ await new Promise((resolve) => setTimeout(resolve, 1000));
126
+ }
127
+ if (result.status !== 200) {
128
+ return { success: false, message: "Error while generating step details" };
129
+ }
130
+ return result.data;
131
+ }
132
+ //@ts-expect-error
133
+ async generateCommandName({ command }) {
134
+ const url = `${getRunsServiceBaseURL()}/generate-step-information/generate-element-name`;
135
+ const screenshot = this.screenshotMap.get(command.inputID);
136
+ const result = await axiosClient({
137
+ url,
138
+ method: "POST",
139
+ data: { ...command, screenshot },
140
+ headers: {
141
+ Authorization: `Bearer ${this.TOKEN}`,
142
+ "X-Source": "recorder",
143
+ },
144
+ });
145
+ if (result.status !== 200) {
146
+ return { success: false, message: "Error while generating command details" };
147
+ }
148
+ return result.data;
149
+ }
150
+ async generateLocatorDescriptions({ locatorsObj, }) {
151
+ const url = `${getRunsServiceBaseURL()}/locate/generate_locator_summaries`;
152
+ const result = await axiosClient({
153
+ url,
154
+ method: "POST",
155
+ data: locatorsObj,
156
+ headers: {
157
+ Authorization: `Bearer ${this.TOKEN}`,
158
+ "X-Source": "recorder",
159
+ },
160
+ });
161
+ if (result.status !== 200) {
162
+ throw new Error("Error while generating locator descriptions");
163
+ }
164
+ if (!result.data.status) {
165
+ throw new Error(result.data.error || "Error while generating locator descriptions");
166
+ }
167
+ return result.data.locatorsObj;
168
+ }
169
+ }
170
+ export class PublishService {
171
+ TOKEN;
172
+ constructor(TOKEN) {
173
+ this.TOKEN = TOKEN;
174
+ }
175
+ //! deprecated
176
+ async saveScenario({ scenario, featureName, override, branch, isEditing, projectId, env, AICode }) {
177
+ const runsURL = getRunsServiceBaseURL();
178
+ const workspaceURL = runsURL.replace("/runs", "/workspace");
179
+ const url = `${workspaceURL}/publish-recording`;
180
+ try {
181
+ const result = await axiosClient({
182
+ url,
183
+ method: "POST",
184
+ data: {
185
+ scenario,
186
+ featureName,
187
+ override,
188
+ env,
189
+ AICode,
190
+ },
191
+ params: {
192
+ branch,
193
+ },
194
+ headers: {
195
+ Authorization: `Bearer ${this.TOKEN}`,
196
+ "X-Source": "recorder",
197
+ },
198
+ });
199
+ if (result.status !== 200) {
200
+ return { success: false, message: "Error while saving scenario" };
201
+ }
202
+ try {
203
+ this.updateProjectMetadata({
204
+ featureName,
205
+ scenarioName: scenario.name,
206
+ projectId,
207
+ branch,
208
+ isEditing: override,
209
+ });
210
+ }
211
+ catch (error) { }
212
+ return { success: true, data: result.data };
213
+ }
214
+ catch (error) {
215
+ // @ts-ignore
216
+ const reason = error?.response?.data?.error || "";
217
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
218
+ throw new Error(`Failed to save scenario: ${errorMessage} \n ${reason}`);
219
+ }
220
+ }
221
+ async updateProjectMetadata({ featureName, scenarioName, projectId, branch, isEditing }) {
222
+ try {
223
+ await axiosClient({
224
+ method: "POST",
225
+ url: `${getRunsServiceBaseURL()}/project/updateProjectMetadata`,
226
+ data: {
227
+ featureName,
228
+ scenarioName,
229
+ eventType: isEditing ? "editScenario" : "createScenario",
230
+ projectId,
231
+ branch: branch,
232
+ },
233
+ headers: {
234
+ Authorization: `Bearer ${this.TOKEN}`,
235
+ "X-Source": "recorder",
236
+ },
237
+ });
238
+ }
239
+ catch (error) {
240
+ // logger.error("Failed to update project metadata: " + error.message);
241
+ // @ts-ignore
242
+ console.error("run_recorder", `Failed to update project metadata: ${error.message ?? error}`);
243
+ }
244
+ }
245
+ }
246
+ export class RemoteBrowserService extends EventEmitter {
247
+ CDP_CONNECT_URL;
248
+ context;
249
+ pages = new Map();
250
+ _selectedPageId = null;
251
+ wsUrlBase; // Store the base URL
252
+ hasClipboardData(payload) {
253
+ return Boolean(payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0)));
254
+ }
255
+ getSelectedPageInfo() {
256
+ if (!this._selectedPageId) {
257
+ return null;
258
+ }
259
+ return this.pages.get(this._selectedPageId) ?? null;
260
+ }
261
+ constructor({ CDP_CONNECT_URL, context }) {
262
+ super();
263
+ this.CDP_CONNECT_URL = CDP_CONNECT_URL;
264
+ this.context = context;
265
+ this.wsUrlBase = this.CDP_CONNECT_URL.replace(/^http/, "ws") + "/devtools/page/";
266
+ this.log("🚀 RemoteBrowserService initialized", { CDP_CONNECT_URL });
267
+ this.context.grantPermissions(["clipboard-read", "clipboard-write"]).catch((error) => {
268
+ this.log("âš ī¸ Failed to pre-grant clipboard permissions", { error });
269
+ });
270
+ this.initializeListeners();
271
+ }
272
+ log(message, data) {
273
+ const timestamp = new Date().toISOString();
274
+ console.log(`[${timestamp}] [RemoteBrowserService] ${message}`, data ? JSON.stringify(data, null, 2) : "");
275
+ }
276
+ isTransientPageDataError(error) {
277
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
278
+ if (!message) {
279
+ return false;
280
+ }
281
+ const transientIndicators = [
282
+ "Execution context was destroyed",
283
+ "Execution context is not available",
284
+ "Cannot find context with specified id",
285
+ "Target closed",
286
+ "Navigation interrupted",
287
+ ];
288
+ return transientIndicators.some((indicator) => message.includes(indicator));
289
+ }
290
+ /**
291
+ * Gets the CDP Target ID for a page *directly* from the page.
292
+ * This is the only reliable method during restarts.
293
+ */
294
+ async getCdpTargetId(page) {
295
+ try {
296
+ if (page.isClosed()) {
297
+ this.log("âš ī¸ Attempted to get CDP ID from a closed page");
298
+ return null;
299
+ }
300
+ const cdpSession = await page.context().newCDPSession(page);
301
+ const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
302
+ this.log("🔍 Retrieved TargetInfo via CDP session", targetInfo);
303
+ await cdpSession.detach();
304
+ if (targetInfo && targetInfo.targetId) {
305
+ this.log("✅ Found CDP ID by session", { id: targetInfo.targetId, url: page.url() });
306
+ return targetInfo.targetId;
307
+ }
308
+ throw new Error("Target.getTargetInfo did not return a targetId");
309
+ }
310
+ catch (error) {
311
+ this.log("❌ Error getting CDP ID by session", { url: page.url(), error });
312
+ return null;
313
+ }
314
+ }
315
+ async initializeListeners() {
316
+ this.log("📡 Initializing listeners");
317
+ // Initialize with existing pages
318
+ const existingPages = this.context.pages();
319
+ this.log("📄 Found existing pages", { count: existingPages.length });
320
+ for (const page of existingPages) {
321
+ const stableTabId = uuidv4();
322
+ const cdpTargetId = await this.getCdpTargetId(page);
323
+ this.pages.set(stableTabId, {
324
+ page,
325
+ cdpTargetId,
326
+ lastKnownTitle: undefined,
327
+ lastKnownUrl: page.url(),
328
+ lastKnownWsDebuggerUrl: cdpTargetId ? `${this.wsUrlBase}${cdpTargetId}` : undefined,
329
+ });
330
+ page.on("framenavigated", async () => {
331
+ try {
332
+ await this.ensureClipboardScriptLoaded(page);
333
+ }
334
+ catch (err) {
335
+ this.log("❌ Error injecting clipboard script on navigation", { stableTabId, error: err });
336
+ }
337
+ });
338
+ // this.log("✅ Existing page added to map", { stableTabId, cdpTargetId, url: page.url() });
339
+ // this.attachPageLifecycleListeners(page, stableTabId);
340
+ // this.log("🔍 Attached lifecycle listeners to existing page", { stableTabId });
341
+ }
342
+ // if (this.pages.size > 0 && !this._selectedPageId) {
343
+ // this._selectedPageId = Array.from(this.pages.keys())[0];
344
+ // }
345
+ // await this.syncState();
346
+ this.context.on("page", async (page) => {
347
+ // const stableTabId = uuidv4();
348
+ // this.log("🆕 New page event triggered", { stableTabId, url: page.url() });
349
+ // // We get the ID immediately, but it might be null if the page is too new
350
+ // const cdpTargetId = await this.getCdpTargetId(page);
351
+ // this.pages.set(stableTabId, {
352
+ // page,
353
+ // cdpTargetId,
354
+ // lastKnownTitle: undefined,
355
+ // lastKnownUrl: page.url(),
356
+ // lastKnownWsDebuggerUrl: cdpTargetId ? `${this.wsUrlBase}${cdpTargetId}` : undefined,
357
+ // });
358
+ // if (cdpTargetId) {
359
+ // this.log("✅ Page mapped to CDP ID", { stableTabId, cdpTargetId });
360
+ // } else {
361
+ // this.log("âš ī¸ Could not find CDP ID for new page yet", { stableTabId });
362
+ // }
363
+ // if (!this._selectedPageId) {
364
+ // // Select the first page that opens
365
+ // this._selectedPageId = stableTabId;
366
+ // this.log("đŸŽ¯ Initial selected page set", { selectedPageId: this._selectedPageId });
367
+ // }
368
+ // this.attachPageLifecycleListeners(page, stableTabId);
369
+ // await this.syncState();
370
+ await this.ensureClipboardScriptLoaded(page);
371
+ });
372
+ }
373
+ // private attachPageLifecycleListeners(page: Page, stableTabId: string) {
374
+ // page.on("load", () => this.syncState());
375
+ // page.on("framenavigated", () => this.syncState());
376
+ // page.on("close", async () => {
377
+ // this.log("đŸ—‘ī¸ Page close event", { stableTabId });
378
+ // this.pages.delete(stableTabId);
379
+ // if (this._selectedPageId === stableTabId) {
380
+ // this._selectedPageId = this.pages.size > 0 ? Array.from(this.pages.keys())[0] : null;
381
+ // this.log("🔄 Selected page changed after close", { newSelectedId: this._selectedPageId });
382
+ // }
383
+ // await this.syncState();
384
+ // });
385
+ // }
386
+ // private async syncState() {
387
+ // try {
388
+ // this.log("🔄 Starting state sync");
389
+ // const state = await this.getState();
390
+ // this.log("✅ State sync complete", { pagesCount: state.pages.length, selectedPageId: state.selectedPageId });
391
+ // this.emit("BrowserService.stateSync", state);
392
+ // } catch (error) {
393
+ // this.log("❌ Error syncing state", { error });
394
+ // }
395
+ // }
396
+ // async getState(): Promise<BrowserState> {
397
+ // this.log("📊 Getting current state");
398
+ // const pagesData: PageData[] = [];
399
+ // const pagesToDelete: string[] = []; // To clean up closed pages
400
+ // for (const [stableTabId, pageInfo] of this.pages.entries()) {
401
+ // const pageData: PageData = {
402
+ // id: stableTabId,
403
+ // title: pageInfo.lastKnownTitle ?? "",
404
+ // url: pageInfo.lastKnownUrl ?? pageInfo.page.url(),
405
+ // wsDebuggerUrl: pageInfo.lastKnownWsDebuggerUrl ?? "",
406
+ // };
407
+ // try {
408
+ // if (pageInfo.page.isClosed()) {
409
+ // this.log("🧹 Found closed page during getState, marking for deletion", { stableTabId });
410
+ // pagesToDelete.push(stableTabId);
411
+ // continue;
412
+ // }
413
+ // // Get the one, true, live CDP ID
414
+ // const currentCdpId = await this.getCdpTargetId(pageInfo.page);
415
+ // if (currentCdpId && pageInfo.cdpTargetId !== currentCdpId) {
416
+ // this.log("🔄 CDP ID changed", { stableTabId, old: pageInfo.cdpTargetId, new: currentCdpId });
417
+ // pageInfo.cdpTargetId = currentCdpId; // Update our internal reference
418
+ // }
419
+ // // Manually construct the WebSocket URL
420
+ // const wsDebuggerUrl = currentCdpId
421
+ // ? `${this.wsUrlBase}${currentCdpId}`
422
+ // : (pageInfo.lastKnownWsDebuggerUrl ?? "");
423
+ // if (!wsDebuggerUrl) {
424
+ // this.log("âš ī¸ Could not get CDP ID, wsDebuggerUrl will be empty", { stableTabId });
425
+ // }
426
+ // const title = await pageInfo.page.title();
427
+ // const currentUrl = pageInfo.page.url();
428
+ // pageData.title = title;
429
+ // pageData.url = currentUrl;
430
+ // pageData.wsDebuggerUrl = wsDebuggerUrl; // Use the constructed URL
431
+ // pageInfo.lastKnownTitle = title;
432
+ // pageInfo.lastKnownUrl = currentUrl;
433
+ // pageInfo.lastKnownWsDebuggerUrl = wsDebuggerUrl;
434
+ // this.log("đŸŸĨPage data", pageData);
435
+ // } catch (error) {
436
+ // const message = error instanceof Error ? error.message : JSON.stringify(error);
437
+ // if (this.isTransientPageDataError(error)) {
438
+ // this.log("âŗ Transient error getting page data, will retry", { stableTabId, message });
439
+ // } else {
440
+ // this.log("❌ Error getting page data", { stableTabId, message });
441
+ // pagesToDelete.push(stableTabId); // Mark for deletion
442
+ // continue;
443
+ // }
444
+ // }
445
+ // if (!pageData.title) {
446
+ // pageData.title = pageInfo.page.url() === "about:blank" ? "" : "Loading...";
447
+ // }
448
+ // pagesData.push(pageData);
449
+ // }
450
+ // pagesToDelete.forEach((id) => this.pages.delete(id));
451
+ // if (this._selectedPageId && !this.pages.has(this._selectedPageId)) {
452
+ // this._selectedPageId = pagesData.length > 0 ? pagesData[0].id : null;
453
+ // this.log("🔄 Corrected selectedPageId", { new: this._selectedPageId });
454
+ // }
455
+ // if (!this._selectedPageId && pagesData.length > 0) {
456
+ // this._selectedPageId = pagesData[0].id;
457
+ // this.log("đŸŽ¯ Set default selectedPageId", { new: this._selectedPageId });
458
+ // }
459
+ // const state = {
460
+ // pages: pagesData,
461
+ // selectedPageId: this._selectedPageId,
462
+ // };
463
+ // this.log("đŸ“Ļ Final state", state);
464
+ // return state;
465
+ // }
466
+ // async createTab(url: string = "about:blank"): Promise<void> {
467
+ // try {
468
+ // this.log("🆕 Creating new tab", { url });
469
+ // const page = await this.context.newPage(); // This will trigger the 'page' event
470
+ // if (url !== "about:blank") {
471
+ // await page.goto(url, { waitUntil: "domcontentloaded" });
472
+ // }
473
+ // for (const [stableTabId, pageInfo] of this.pages.entries()) {
474
+ // if (pageInfo.page === page) {
475
+ // this._selectedPageId = stableTabId;
476
+ // this.log("✅ New tab created and selected", { stableTabId, url });
477
+ // break;
478
+ // }
479
+ // }
480
+ // await this.syncState();
481
+ // } catch (error) {
482
+ // this.log("❌ Error creating tab", { error });
483
+ // }
484
+ // }
485
+ // async closeTab(stableTabId: string): Promise<void> {
486
+ // try {
487
+ // this.log("đŸ—‘ī¸ Closing tab", { stableTabId });
488
+ // const pageInfo = this.pages.get(stableTabId);
489
+ // if (pageInfo) {
490
+ // await pageInfo.page.close(); // This will trigger the 'close' event
491
+ // } else {
492
+ // this.log("âš ī¸ Page not found for closing", { stableTabId });
493
+ // }
494
+ // } catch (error) {
495
+ // this.log("❌ Error closing tab", { error });
496
+ // }
497
+ // }
498
+ // async selectTab(stableTabId: string): Promise<void> {
499
+ // try {
500
+ // this.log("đŸŽ¯ Selecting tab", { stableTabId });
501
+ // const pageInfo = this.pages.get(stableTabId);
502
+ // if (pageInfo) {
503
+ // this._selectedPageId = stableTabId;
504
+ // await pageInfo.page.bringToFront();
505
+ // this.log("✅ Tab selected successfully", { stableTabId });
506
+ // await this.syncState();
507
+ // } else {
508
+ // this.log("âš ī¸ Page not found for selection", { stableTabId });
509
+ // }
510
+ // } catch (error) {
511
+ // this.log("❌ Error selecting tab", { error });
512
+ // }
513
+ // }
514
+ getPageInfo(stableTabId) {
515
+ if (!stableTabId) {
516
+ this.log("âš ī¸ Operation requested without a selected tab");
517
+ return null;
518
+ }
519
+ const pageInfo = this.pages.get(stableTabId);
520
+ if (!pageInfo) {
521
+ this.log("âš ī¸ Page not found for operation", { stableTabId });
522
+ return null;
523
+ }
524
+ return pageInfo;
525
+ }
526
+ // async navigateTab(stableTabId: string, url: string): Promise<void> {
527
+ // const pageInfo = this.getPageInfo(stableTabId);
528
+ // if (!pageInfo) {
529
+ // return;
530
+ // }
531
+ // try {
532
+ // this.log("🌐 Navigating tab", { stableTabId, url });
533
+ // await pageInfo.page.goto(url, { waitUntil: "domcontentloaded" });
534
+ // this._selectedPageId = stableTabId;
535
+ // await this.syncState();
536
+ // } catch (error) {
537
+ // this.log("❌ Error navigating tab", { stableTabId, url, error });
538
+ // throw error;
539
+ // }
540
+ // }
541
+ // async reloadTab(stableTabId: string): Promise<void> {
542
+ // const pageInfo = this.getPageInfo(stableTabId);
543
+ // if (!pageInfo) {
544
+ // return;
545
+ // }
546
+ // try {
547
+ // this.log("🔄 Reloading tab", { stableTabId, url: pageInfo.page.url() });
548
+ // await pageInfo.page.reload({ waitUntil: "domcontentloaded" });
549
+ // await this.syncState();
550
+ // } catch (error) {
551
+ // this.log("❌ Error reloading tab", { stableTabId, error });
552
+ // throw error;
553
+ // }
554
+ // }
555
+ // async goBack(stableTabId: string): Promise<void> {
556
+ // const pageInfo = this.getPageInfo(stableTabId);
557
+ // if (!pageInfo) {
558
+ // return;
559
+ // }
560
+ // try {
561
+ // this.log("âŦ…ī¸ Navigating back", { stableTabId });
562
+ // const response = await pageInfo.page.goBack({ waitUntil: "domcontentloaded" });
563
+ // if (!response) {
564
+ // this.log("â„šī¸ No history entry to go back to", { stableTabId });
565
+ // }
566
+ // await this.syncState();
567
+ // } catch (error) {
568
+ // this.log("❌ Error navigating back", { stableTabId, error });
569
+ // throw error;
570
+ // }
571
+ // }
572
+ // async goForward(stableTabId: string): Promise<void> {
573
+ // const pageInfo = this.getPageInfo(stableTabId);
574
+ // if (!pageInfo) {
575
+ // return;
576
+ // }
577
+ // try {
578
+ // this.log("âžĄī¸ Navigating forward", { stableTabId });
579
+ // const response = await pageInfo.page.goForward({ waitUntil: "domcontentloaded" });
580
+ // if (!response) {
581
+ // this.log("â„šī¸ No history entry to go forward to", { stableTabId });
582
+ // }
583
+ // await this.syncState();
584
+ // } catch (error) {
585
+ // this.log("❌ Error navigating forward", { stableTabId, error });
586
+ // throw error;
587
+ // }
588
+ // }
589
+ async applyClipboardPayload(payload) {
590
+ this.log("📋 applyClipboardPayload called", {
591
+ hasText: !!payload?.text,
592
+ hasHtml: !!payload?.html,
593
+ hasFiles: !!payload?.files,
594
+ textLength: payload?.text?.length,
595
+ htmlLength: payload?.html?.length,
596
+ filesCount: payload?.files?.length,
597
+ });
598
+ const pageInfo = this.getPageInfo(this._selectedPageId);
599
+ if (!pageInfo) {
600
+ this.log("âš ī¸ No active page available for clipboard payload", {
601
+ selectedPageId: this._selectedPageId,
602
+ totalPages: this.pages.size,
603
+ });
604
+ return;
605
+ }
606
+ this.log("📄 Target page info", {
607
+ stableTabId: this._selectedPageId,
608
+ url: pageInfo.page.url(),
609
+ isClosed: pageInfo.page.isClosed(),
610
+ title: await pageInfo.page.title().catch(() => "unknown"),
611
+ });
612
+ try {
613
+ // Grant permissions first
614
+ this.log("🔐 Attempting to grant clipboard permissions");
615
+ await pageInfo.page
616
+ .context()
617
+ .grantPermissions(["clipboard-read", "clipboard-write"])
618
+ .then(() => {
619
+ this.log("✅ Clipboard permissions granted");
620
+ })
621
+ .catch((error) => {
622
+ this.log("âš ī¸ Failed to grant clipboard permissions", { error: error.message });
623
+ });
624
+ // Apply the clipboard data using the injected function
625
+ this.log("🚀 Executing clipboard application script", {
626
+ payloadPreview: {
627
+ text: payload?.text?.substring(0, 100),
628
+ html: payload?.html?.substring(0, 100),
629
+ },
630
+ });
631
+ const result = await pageInfo.page.evaluate((clipboardData) => {
632
+ console.log("[RemoteBrowser->Page] Starting clipboard application");
633
+ console.log("[RemoteBrowser->Page] Payload:", {
634
+ hasText: !!clipboardData?.text,
635
+ hasHtml: !!clipboardData?.html,
636
+ text: clipboardData?.text?.substring(0, 50),
637
+ html: clipboardData?.html?.substring(0, 50),
638
+ });
639
+ // Check if the function exists
640
+ if (typeof window.__bvt_applyClipboardData !== "function") {
641
+ console.error("[RemoteBrowser->Page] ❌ __bvt_applyClipboardData function not found!");
642
+ console.log("[RemoteBrowser->Page] Available window properties:", Object.keys(window).filter((k) => k.includes("bvt")));
643
+ return false;
644
+ }
645
+ console.log("[RemoteBrowser->Page] ✅ __bvt_applyClipboardData function found, calling it");
646
+ try {
647
+ const result = window.__bvt_applyClipboardData(clipboardData);
648
+ console.log("[RemoteBrowser->Page] Function returned:", result);
649
+ return result;
650
+ }
651
+ catch (error) {
652
+ console.error("[RemoteBrowser->Page] ❌ Error calling __bvt_applyClipboardData:", error);
653
+ return false;
654
+ }
655
+ }, payload);
656
+ this.log("📊 Clipboard application result", {
657
+ success: result,
658
+ selectedPageId: this._selectedPageId,
659
+ });
660
+ if (!result) {
661
+ this.log("âš ī¸ Clipboard script returned false - data may not have been applied");
662
+ }
663
+ else {
664
+ this.log("✅ Clipboard payload applied successfully");
665
+ }
666
+ }
667
+ catch (error) {
668
+ this.log("❌ Error applying clipboard payload", {
669
+ error: error instanceof Error ? error.message : String(error),
670
+ stack: error instanceof Error ? error.stack : undefined,
671
+ selectedPageId: this._selectedPageId,
672
+ pageUrl: pageInfo.page.url(),
673
+ });
674
+ throw error;
675
+ }
676
+ }
677
+ async readClipboardPayload() {
678
+ const pageInfo = this.getSelectedPageInfo();
679
+ if (!pageInfo) {
680
+ this.log("âš ī¸ Cannot read clipboard - no active page", { selectedPageId: this._selectedPageId });
681
+ return null;
682
+ }
683
+ try {
684
+ await pageInfo.page
685
+ .context()
686
+ .grantPermissions(["clipboard-read", "clipboard-write"])
687
+ .catch((error) => {
688
+ this.log("âš ī¸ Failed to ensure clipboard permissions before read", { error });
689
+ });
690
+ const payload = await pageInfo.page.evaluate(async () => {
691
+ const result = {};
692
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
693
+ return result;
694
+ }
695
+ const arrayBufferToBase64 = (buffer) => {
696
+ let binary = "";
697
+ const bytes = new Uint8Array(buffer);
698
+ const chunkSize = 0x8000;
699
+ for (let index = 0; index < bytes.length; index += chunkSize) {
700
+ const chunk = bytes.subarray(index, index + chunkSize);
701
+ binary += String.fromCharCode(...chunk);
702
+ }
703
+ return btoa(binary);
704
+ };
705
+ const files = [];
706
+ if (typeof navigator.clipboard.read === "function") {
707
+ try {
708
+ const items = await navigator.clipboard.read();
709
+ for (const item of items) {
710
+ if (item.types.includes("text/html") && !result.html) {
711
+ const blob = await item.getType("text/html");
712
+ result.html = await blob.text();
713
+ }
714
+ if (item.types.includes("text/plain") && !result.text) {
715
+ const blob = await item.getType("text/plain");
716
+ result.text = await blob.text();
717
+ }
718
+ for (const type of item.types) {
719
+ if (type.startsWith("text/")) {
720
+ continue;
721
+ }
722
+ try {
723
+ const blob = await item.getType(type);
724
+ const buffer = await blob.arrayBuffer();
725
+ files.push({
726
+ name: `clipboard-file-${files.length + 1}`,
727
+ type,
728
+ lastModified: Date.now(),
729
+ data: arrayBufferToBase64(buffer),
730
+ });
731
+ }
732
+ catch (error) {
733
+ console.warn("[RemoteClipboard] Failed to serialize clipboard blob", { type, error });
734
+ }
735
+ }
736
+ }
737
+ }
738
+ catch (error) {
739
+ console.warn("[RemoteClipboard] navigator.clipboard.read failed", error);
740
+ }
741
+ }
742
+ if (!result.text && typeof navigator.clipboard.readText === "function") {
743
+ try {
744
+ const text = await navigator.clipboard.readText();
745
+ if (text) {
746
+ result.text = text;
747
+ }
748
+ }
749
+ catch (error) {
750
+ console.warn("[RemoteClipboard] navigator.clipboard.readText failed", error);
751
+ }
752
+ }
753
+ if (!result.text) {
754
+ const selection = window.getSelection?.()?.toString?.();
755
+ if (selection) {
756
+ result.text = selection;
757
+ }
758
+ }
759
+ if (files.length > 0) {
760
+ result.files = files;
761
+ }
762
+ return result;
763
+ });
764
+ if (this.hasClipboardData(payload)) {
765
+ this.log("📋 Remote clipboard payload captured", {
766
+ hasText: !!payload.text,
767
+ hasHtml: !!payload.html,
768
+ fileCount: payload.files?.length ?? 0,
769
+ });
770
+ return payload;
771
+ }
772
+ this.log("â„šī¸ Remote clipboard read returned empty payload", {
773
+ selectedPageId: this._selectedPageId,
774
+ });
775
+ return null;
776
+ }
777
+ catch (error) {
778
+ this.log("❌ Error reading remote clipboard", {
779
+ error: error instanceof Error ? error.message : String(error),
780
+ });
781
+ throw error;
782
+ }
783
+ }
784
+ // Add a helper method to verify clipboard script is loaded
785
+ async verifyClipboardScript(stableTabId) {
786
+ const pageInfo = this.pages.get(stableTabId);
787
+ if (!pageInfo) {
788
+ this.log("âš ī¸ Cannot verify clipboard script - page not found", { stableTabId });
789
+ return false;
790
+ }
791
+ try {
792
+ const isLoaded = await pageInfo.page.evaluate(() => {
793
+ return (typeof window.__bvt_applyClipboardData === "function" && typeof window.__bvt_reportClipboard === "function");
794
+ });
795
+ this.log("🔍 Clipboard script verification", {
796
+ stableTabId,
797
+ isLoaded,
798
+ url: pageInfo.page.url(),
799
+ });
800
+ return isLoaded;
801
+ }
802
+ catch (error) {
803
+ this.log("❌ Error verifying clipboard script", {
804
+ stableTabId,
805
+ error: error instanceof Error ? error.message : String(error),
806
+ });
807
+ return false;
808
+ }
809
+ }
810
+ // Call this after navigation to ensure script is loaded
811
+ async ensureClipboardScriptLoaded(page) {
812
+ try {
813
+ const isLoaded = await page.evaluate(() => {
814
+ return typeof window.__bvt_applyClipboardData === "function";
815
+ });
816
+ if (!isLoaded) {
817
+ this.log("âš ī¸ Clipboard script not loaded, attempting to reload");
818
+ // The script should be in initScripts, so it will load on next navigation
819
+ // or you could manually inject it here
820
+ }
821
+ else {
822
+ this.log("✅ Clipboard script confirmed loaded");
823
+ }
824
+ }
825
+ catch (error) {
826
+ this.log("❌ Error checking clipboard script", { error });
827
+ }
828
+ }
829
+ getSelectedPage() {
830
+ const pageInfo = this.getSelectedPageInfo();
831
+ this.log("🔍 Getting selected page", {
832
+ selectedPageId: this._selectedPageId,
833
+ found: !!pageInfo,
834
+ url: pageInfo?.page.url(),
112
835
  });
113
-
114
- if (action.data.status) {
115
- result = action;
116
- break;
117
- }
118
- } catch (error) {
119
- if (i === TIMEOUT - 1) {
120
- console.error("Timeout while generating step details: ", error);
121
- }
122
- }
123
-
124
- await new Promise((resolve) => setTimeout(resolve, 1000));
125
- }
126
-
127
- if (result.status !== 200) {
128
- return { success: false, message: "Error while generating step details" };
129
- }
130
-
131
- return result.data;
132
- }
133
-
134
- async generateCommandName({ command }) {
135
- const url = `${getRunsServiceBaseURL()}/generate-step-information/generate-element-name`;
136
- const screenshot = this.screenshotMap.get(command.inputID);
137
- const result = await axiosClient({
138
- url,
139
- method: "POST",
140
- data: { ...command, screenshot },
141
- headers: {
142
- Authorization: `Bearer ${this.TOKEN}`,
143
- "X-Source": "recorder",
144
- },
145
- });
146
- if (result.status !== 200) {
147
- return { success: false, message: "Error while generating command details" };
148
- }
149
- return result.data;
150
- }
836
+ return pageInfo?.page || null;
837
+ }
838
+ destroy() {
839
+ this.log("đŸ’Ĩ Destroying RemoteBrowserService");
840
+ this.context.removeAllListeners("page");
841
+ for (const [, pageInfo] of this.pages.entries()) {
842
+ pageInfo.page.removeAllListeners(); // Remove all listeners from each page
843
+ }
844
+ this.pages.clear();
845
+ this._selectedPageId = null;
846
+ this.removeAllListeners(); // Remove listeners on the emitter itself
847
+ }
151
848
  }