@dev-blinq/cucumber_client 1.0.1379-dev โ†’ 1.0.1379-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 (50) 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 +41 -0
  16. package/bin/assets/templates/utils_template.txt +2 -45
  17. package/bin/client/apiTest/apiTest.js +6 -0
  18. package/bin/client/code_cleanup/utils.js +5 -1
  19. package/bin/client/code_gen/api_codegen.js +2 -2
  20. package/bin/client/code_gen/code_inversion.js +107 -2
  21. package/bin/client/code_gen/function_signature.js +4 -0
  22. package/bin/client/code_gen/page_reflection.js +846 -906
  23. package/bin/client/code_gen/playwright_codeget.js +27 -3
  24. package/bin/client/cucumber/feature.js +4 -0
  25. package/bin/client/cucumber/feature_data.js +2 -2
  26. package/bin/client/cucumber/project_to_document.js +8 -2
  27. package/bin/client/cucumber/steps_definitions.js +6 -3
  28. package/bin/client/cucumber_selector.js +17 -1
  29. package/bin/client/local_agent.js +3 -2
  30. package/bin/client/parse_feature_file.js +23 -26
  31. package/bin/client/playground/projects/env.json +2 -2
  32. package/bin/client/project.js +186 -196
  33. package/bin/client/recorderv3/bvt_init.js +325 -0
  34. package/bin/client/recorderv3/bvt_recorder.js +301 -99
  35. package/bin/client/recorderv3/implemented_steps.js +8 -0
  36. package/bin/client/recorderv3/index.js +4 -303
  37. package/bin/client/recorderv3/scriptTest.js +1 -1
  38. package/bin/client/recorderv3/services.js +431 -154
  39. package/bin/client/recorderv3/step_runner.js +315 -206
  40. package/bin/client/recorderv3/step_utils.js +479 -25
  41. package/bin/client/recorderv3/update_feature.js +9 -5
  42. package/bin/client/recorderv3/wbr_entry.js +61 -0
  43. package/bin/client/recording.js +1 -0
  44. package/bin/client/upload-service.js +3 -2
  45. package/bin/client/utils/socket_logger.js +132 -0
  46. package/bin/index.js +4 -1
  47. package/bin/logger.js +3 -2
  48. package/bin/min/consoleApi.min.cjs +2 -3
  49. package/bin/min/injectedScript.min.cjs +16 -16
  50. package/package.json +20 -10
@@ -2,162 +2,439 @@ 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
- this.logger.info(
19
- "data: " +
20
- JSON.stringify(
21
- {
22
- commands,
23
- stepsNames,
24
- parameters,
25
- map,
26
- },
27
- null,
28
- 2
29
- )
30
- );
31
- // get screenshot for the last command
32
- const url = `${getRunsServiceBaseURL()}/generate-step-information/generate`;
33
- const TIMEOUT = 120; // 2 minutes
34
- const { data } = await axiosClient({
35
- url,
36
- method: "POST",
37
- data: {
38
- commands,
39
- stepsNames,
40
- parameters,
41
- screenshot,
42
- map,
43
- },
44
- headers: {
45
- Authorization: `Bearer ${this.TOKEN}`,
46
- "X-Source": "recorder",
47
- },
48
- });
49
- const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
50
- let result = { status: 500 };
51
- for (let i = 0; i < TIMEOUT; i++) {
52
- try {
53
- const action = await axiosClient({
54
- url: actionUrl,
55
- method: "GET",
56
- headers: {
57
- Authorization: `Bearer ${this.TOKEN}`,
58
- "X-Source": "recorder",
59
- },
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
+ };
60
84
  });
61
-
62
- if (action.data.status) {
63
- result = action;
64
- break;
65
- }
66
- } catch (error) {
67
- if (i === TIMEOUT - 1) {
68
- console.error("Timeout while generating step details: ", error);
69
- }
70
- }
71
-
72
- await new Promise((resolve) => setTimeout(resolve, 1000));
73
- }
74
-
75
- if (result.status !== 200) {
76
- return { success: false, message: "Error while generating step details" };
77
- }
78
- return result.data;
79
- }
80
- async generateScenarioAndFeatureNames(scenarioAsText) {
81
- if (!this.projectDir) {
82
- throw new Error("Project directory not found");
83
- }
84
- // read all files with .feature extension into an object {files:[{name: "", content: ""}]}
85
- const featureFiles = readdirSync(path.join(this.projectDir, "features"));
86
- const featureFilesContent = featureFiles
87
- .filter((file) => file.endsWith(".feature"))
88
- .map((file) => {
89
- return {
90
- name: file,
91
- content: readFileSync(path.join(this.projectDir, "features", file), "utf8"),
85
+ const genObject = {
86
+ files: featureFilesContent,
87
+ scenario: scenarioAsText,
92
88
  };
93
- });
94
- const genObject = {
95
- files: featureFilesContent,
96
- scenario: scenarioAsText,
97
- };
98
-
99
- this.logger.info("data: " + JSON.stringify(genObject, null, 2));
100
- // get screenshot for the last command
101
- const url = `${getRunsServiceBaseURL()}/generate-step-information/generate_scenario_feature`;
102
- const TIMEOUT = 120; // 2 minutes
103
- const { data } = await axiosClient({
104
- url,
105
- method: "POST",
106
- data: {
107
- ...genObject,
108
- },
109
- headers: {
110
- Authorization: `Bearer ${this.TOKEN}`,
111
- "X-Source": "recorder",
112
- },
113
- });
114
- const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
115
- let result = { status: 500 };
116
- for (let i = 0; i < TIMEOUT; i++) {
117
- try {
118
- const action = await axiosClient({
119
- url: actionUrl,
120
- method: "GET",
121
- headers: {
122
- Authorization: `Bearer ${this.TOKEN}`,
123
- "X-Source": "recorder",
124
- },
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
+ },
125
102
  });
126
-
127
- if (action.data.status) {
128
- result = action;
129
- break;
130
- }
131
- } catch (error) {
132
- if (i === TIMEOUT - 1) {
133
- console.error("Timeout while generating step details: ", error);
134
- }
135
- }
136
-
137
- await new Promise((resolve) => setTimeout(resolve, 1000));
138
- }
139
-
140
- if (result.status !== 200) {
141
- return { success: false, message: "Error while generating step details" };
142
- }
143
- return result.data;
144
- }
145
-
146
- async generateCommandName({ command }) {
147
- const url = `${getRunsServiceBaseURL()}/generate-step-information/generate-element-name`;
148
- const screenshot = this.screenshotMap.get(command.inputID);
149
- const result = await axiosClient({
150
- url,
151
- method: "POST",
152
- data: { ...command, screenshot },
153
- headers: {
154
- Authorization: `Bearer ${this.TOKEN}`,
155
- "X-Source": "recorder",
156
- },
157
- });
158
- if (result.status !== 200) {
159
- return { success: false, message: "Error while generating command details" };
160
- }
161
- return result.data;
162
- }
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
+ }
151
+ export class PublishService {
152
+ TOKEN;
153
+ constructor(TOKEN) {
154
+ this.TOKEN = TOKEN;
155
+ }
156
+ async saveScenario({ scenario, featureName, override, branch, isEditing, projectId, env }) {
157
+ const runsURL = getRunsServiceBaseURL();
158
+ const workspaceURL = runsURL.replace("/runs", "/workspace");
159
+ const url = `${workspaceURL}/publish-recording`;
160
+ const result = await axiosClient({
161
+ url,
162
+ method: "POST",
163
+ data: {
164
+ scenario,
165
+ featureName,
166
+ override,
167
+ env,
168
+ },
169
+ params: {
170
+ branch,
171
+ },
172
+ headers: {
173
+ Authorization: `Bearer ${this.TOKEN}`,
174
+ "X-Source": "recorder",
175
+ },
176
+ });
177
+ if (result.status !== 200) {
178
+ return { success: false, message: "Error while saving scenario" };
179
+ }
180
+ try {
181
+ this.updateProjectMetadata({
182
+ featureName,
183
+ scenarioName: scenario.name,
184
+ projectId,
185
+ branch,
186
+ isEditing: override,
187
+ });
188
+ }
189
+ catch (error) { }
190
+ return { success: true, data: result.data };
191
+ }
192
+ async updateProjectMetadata({ featureName, scenarioName, projectId, branch, isEditing }) {
193
+ try {
194
+ await axiosClient({
195
+ method: "POST",
196
+ url: `${getRunsServiceBaseURL()}/project/updateProjectMetadata`,
197
+ data: {
198
+ featureName,
199
+ scenarioName,
200
+ eventType: isEditing ? "editScenario" : "createScenario",
201
+ projectId,
202
+ branch: branch,
203
+ },
204
+ headers: {
205
+ Authorization: `Bearer ${this.TOKEN}`,
206
+ "X-Source": "recorder",
207
+ },
208
+ });
209
+ }
210
+ catch (error) {
211
+ // logger.error("Failed to update project metadata: " + error.message);
212
+ // @ts-ignore
213
+ console.error("run_recorder", `Failed to update project metadata: ${error.message ?? error}`);
214
+ }
215
+ }
216
+ }
217
+ export class RemoteBrowserService extends EventEmitter {
218
+ CDP_CONNECT_URL;
219
+ context;
220
+ pages = new Map();
221
+ _selectedPageId = null;
222
+ wsUrlBase; // Store the base URL
223
+ constructor({ CDP_CONNECT_URL, context }) {
224
+ super();
225
+ this.CDP_CONNECT_URL = CDP_CONNECT_URL;
226
+ this.context = context;
227
+ this.wsUrlBase = this.CDP_CONNECT_URL.replace(/^http/, "ws") + "/devtools/page/";
228
+ this.log("๐Ÿš€ RemoteBrowserService initialized", { CDP_CONNECT_URL });
229
+ this.initializeListeners();
230
+ }
231
+ log(message, data) {
232
+ const timestamp = new Date().toISOString();
233
+ console.log(`[${timestamp}] [RemoteBrowserService] ${message}`, data ? JSON.stringify(data, null, 2) : "");
234
+ }
235
+ /**
236
+ * Gets the CDP Target ID for a page *directly* from the page.
237
+ * This is the only reliable method during restarts.
238
+ */
239
+ async getCdpTargetId(page) {
240
+ try {
241
+ if (page.isClosed()) {
242
+ this.log("โš ๏ธ Attempted to get CDP ID from a closed page");
243
+ return null;
244
+ }
245
+ const cdpSession = await page.context().newCDPSession(page);
246
+ const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
247
+ this.log("๐Ÿ” Retrieved TargetInfo via CDP session", targetInfo);
248
+ await cdpSession.detach();
249
+ if (targetInfo && targetInfo.targetId) {
250
+ this.log("โœ… Found CDP ID by session", { id: targetInfo.targetId, url: page.url() });
251
+ return targetInfo.targetId;
252
+ }
253
+ throw new Error("Target.getTargetInfo did not return a targetId");
254
+ }
255
+ catch (error) {
256
+ this.log("โŒ Error getting CDP ID by session", { url: page.url(), error });
257
+ return null;
258
+ }
259
+ }
260
+ async initializeListeners() {
261
+ this.log("๐Ÿ“ก Initializing listeners");
262
+ this.context.on("page", async (page) => {
263
+ const stableTabId = uuidv4();
264
+ this.log("๐Ÿ†• New page event triggered", { stableTabId, url: page.url() });
265
+ // We get the ID immediately, but it might be null if the page is too new
266
+ const cdpTargetId = await this.getCdpTargetId(page);
267
+ this.pages.set(stableTabId, { page, cdpTargetId });
268
+ if (cdpTargetId) {
269
+ this.log("โœ… Page mapped to CDP ID", { stableTabId, cdpTargetId });
270
+ }
271
+ else {
272
+ this.log("โš ๏ธ Could not find CDP ID for new page yet", { stableTabId });
273
+ }
274
+ if (!this._selectedPageId) {
275
+ // Select the first page that opens
276
+ this._selectedPageId = stableTabId;
277
+ this.log("๐ŸŽฏ Initial selected page set", { selectedPageId: this._selectedPageId });
278
+ }
279
+ await this.syncState();
280
+ // Add listeners
281
+ page.on("load", () => this.syncState());
282
+ page.on("framenavigated", () => this.syncState());
283
+ page.on("close", async () => {
284
+ this.log("๐Ÿ—‘๏ธ Page close event", { stableTabId });
285
+ this.pages.delete(stableTabId);
286
+ if (this._selectedPageId === stableTabId) {
287
+ this._selectedPageId = this.pages.size > 0 ? Array.from(this.pages.keys())[0] : null;
288
+ this.log("๐Ÿ”„ Selected page changed after close", { newSelectedId: this._selectedPageId });
289
+ }
290
+ await this.syncState();
291
+ });
292
+ });
293
+ // Initialize with existing pages
294
+ const existingPages = this.context.pages();
295
+ this.log("๐Ÿ“„ Found existing pages", { count: existingPages.length });
296
+ for (const page of existingPages) {
297
+ const stableTabId = uuidv4();
298
+ const cdpTargetId = await this.getCdpTargetId(page);
299
+ this.pages.set(stableTabId, { page, cdpTargetId });
300
+ this.log("โœ… Existing page added to map", { stableTabId, cdpTargetId, url: page.url() });
301
+ }
302
+ if (this.pages.size > 0 && !this._selectedPageId) {
303
+ this._selectedPageId = Array.from(this.pages.keys())[0];
304
+ }
305
+ await this.syncState();
306
+ }
307
+ async syncState() {
308
+ try {
309
+ this.log("๐Ÿ”„ Starting state sync");
310
+ const state = await this.getState();
311
+ this.log("โœ… State sync complete", { pagesCount: state.pages.length, selectedPageId: state.selectedPageId });
312
+ this.emit("BrowserService.stateSync", state);
313
+ }
314
+ catch (error) {
315
+ this.log("โŒ Error syncing state", { error });
316
+ }
317
+ }
318
+ async getState() {
319
+ this.log("๐Ÿ“Š Getting current state");
320
+ const pagesData = [];
321
+ const pagesToDelete = []; // To clean up closed pages
322
+ for (const [stableTabId, pageInfo] of this.pages.entries()) {
323
+ try {
324
+ if (pageInfo.page.isClosed()) {
325
+ this.log("๐Ÿงน Found closed page during getState, marking for deletion", { stableTabId });
326
+ pagesToDelete.push(stableTabId);
327
+ continue;
328
+ }
329
+ // Get the one, true, live CDP ID
330
+ const currentCdpId = await this.getCdpTargetId(pageInfo.page);
331
+ if (currentCdpId && pageInfo.cdpTargetId !== currentCdpId) {
332
+ this.log("๐Ÿ”„ CDP ID changed", { stableTabId, old: pageInfo.cdpTargetId, new: currentCdpId });
333
+ pageInfo.cdpTargetId = currentCdpId; // Update our internal reference
334
+ }
335
+ // Manually construct the WebSocket URL
336
+ const wsDebuggerUrl = currentCdpId ? `${this.wsUrlBase}${currentCdpId}` : "";
337
+ if (!wsDebuggerUrl) {
338
+ this.log("โš ๏ธ Could not get CDP ID, wsDebuggerUrl will be empty", { stableTabId });
339
+ }
340
+ pagesData.push({
341
+ id: stableTabId,
342
+ title: await pageInfo.page.title(),
343
+ url: pageInfo.page.url(),
344
+ wsDebuggerUrl: wsDebuggerUrl, // Use the constructed URL
345
+ });
346
+ }
347
+ catch (error) {
348
+ this.log("โŒ Error getting page data", { stableTabId, error });
349
+ pagesToDelete.push(stableTabId); // Mark for deletion
350
+ }
351
+ }
352
+ pagesToDelete.forEach((id) => this.pages.delete(id));
353
+ if (this._selectedPageId && !this.pages.has(this._selectedPageId)) {
354
+ this._selectedPageId = pagesData.length > 0 ? pagesData[0].id : null;
355
+ this.log("๐Ÿ”„ Corrected selectedPageId", { new: this._selectedPageId });
356
+ }
357
+ if (!this._selectedPageId && pagesData.length > 0) {
358
+ this._selectedPageId = pagesData[0].id;
359
+ this.log("๐ŸŽฏ Set default selectedPageId", { new: this._selectedPageId });
360
+ }
361
+ const state = {
362
+ pages: pagesData,
363
+ selectedPageId: this._selectedPageId,
364
+ };
365
+ this.log("๐Ÿ“ฆ Final state", state);
366
+ return state;
367
+ }
368
+ async createTab(url = "about:blank") {
369
+ try {
370
+ this.log("๐Ÿ†• Creating new tab", { url });
371
+ const page = await this.context.newPage(); // This will trigger the 'page' event
372
+ if (url !== "about:blank") {
373
+ await page.goto(url, { waitUntil: "domcontentloaded" });
374
+ }
375
+ for (const [stableTabId, pageInfo] of this.pages.entries()) {
376
+ if (pageInfo.page === page) {
377
+ this._selectedPageId = stableTabId;
378
+ this.log("โœ… New tab created and selected", { stableTabId, url });
379
+ break;
380
+ }
381
+ }
382
+ await this.syncState();
383
+ }
384
+ catch (error) {
385
+ this.log("โŒ Error creating tab", { error });
386
+ }
387
+ }
388
+ async closeTab(stableTabId) {
389
+ try {
390
+ this.log("๐Ÿ—‘๏ธ Closing tab", { stableTabId });
391
+ const pageInfo = this.pages.get(stableTabId);
392
+ if (pageInfo) {
393
+ await pageInfo.page.close(); // This will trigger the 'close' event
394
+ }
395
+ else {
396
+ this.log("โš ๏ธ Page not found for closing", { stableTabId });
397
+ }
398
+ }
399
+ catch (error) {
400
+ this.log("โŒ Error closing tab", { error });
401
+ }
402
+ }
403
+ async selectTab(stableTabId) {
404
+ try {
405
+ this.log("๐ŸŽฏ Selecting tab", { stableTabId });
406
+ const pageInfo = this.pages.get(stableTabId);
407
+ if (pageInfo) {
408
+ this._selectedPageId = stableTabId;
409
+ await pageInfo.page.bringToFront();
410
+ this.log("โœ… Tab selected successfully", { stableTabId });
411
+ await this.syncState();
412
+ }
413
+ else {
414
+ this.log("โš ๏ธ Page not found for selection", { stableTabId });
415
+ }
416
+ }
417
+ catch (error) {
418
+ this.log("โŒ Error selecting tab", { error });
419
+ }
420
+ }
421
+ getSelectedPage() {
422
+ const pageInfo = this._selectedPageId ? this.pages.get(this._selectedPageId) : null;
423
+ this.log("๐Ÿ” Getting selected page", {
424
+ selectedPageId: this._selectedPageId,
425
+ found: !!pageInfo,
426
+ url: pageInfo?.page.url(),
427
+ });
428
+ return pageInfo?.page || null;
429
+ }
430
+ destroy() {
431
+ this.log("๐Ÿ’ฅ Destroying RemoteBrowserService");
432
+ this.context.removeAllListeners("page");
433
+ for (const [, pageInfo] of this.pages.entries()) {
434
+ pageInfo.page.removeAllListeners(); // Remove all listeners from each page
435
+ }
436
+ this.pages.clear();
437
+ this._selectedPageId = null;
438
+ this.removeAllListeners();
439
+ }
163
440
  }