@dev-blinq/cucumber_client 1.0.1587-dev → 1.0.1589-dev

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.
@@ -3,6 +3,7 @@ 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
8
  screenshotMap;
8
9
  TOKEN;
@@ -213,8 +214,9 @@ export class PublishService {
213
214
  export class RemoteBrowserService extends EventEmitter {
214
215
  CDP_CONNECT_URL;
215
216
  context;
217
+ // stableTabId, Page Info
216
218
  pages = new Map();
217
- _selectedPageId = null;
219
+ _selectedPageId = null; // This will be a stableTabId
218
220
  constructor({ CDP_CONNECT_URL, context }) {
219
221
  super();
220
222
  this.CDP_CONNECT_URL = CDP_CONNECT_URL;
@@ -228,182 +230,156 @@ export class RemoteBrowserService extends EventEmitter {
228
230
  }
229
231
  async initializeListeners() {
230
232
  this.log("📡 Initializing listeners");
231
- // Listen for new pages
232
233
  this.context.on("page", async (page) => {
233
- this.log("🆕 New page event triggered", { url: page.url() });
234
- const id = await this.getPageId(page);
235
- this.log("🔍 Got page ID from CDP", { id, url: page.url() });
236
- if (id) {
237
- this.pages.set(id, page);
238
- this.log("✅ Page added to internal map", {
239
- id,
240
- url: page.url(),
241
- totalPages: this.pages.size,
242
- allPageIds: Array.from(this.pages.keys()),
243
- });
244
- await this.syncState();
234
+ const stableTabId = uuidv4(); // Create a stable ID immediately
235
+ this.log("🆕 New page event triggered", { stableTabId, url: page.url() });
236
+ // Add to map immediately with the stable ID
237
+ this.pages.set(stableTabId, { page, cdpTargetId: null });
238
+ // Asynchronously find its CDP ID
239
+ const cdpTargetId = await this.findCdpTargetId(page);
240
+ if (cdpTargetId) {
241
+ this.pages.get(stableTabId).cdpTargetId = cdpTargetId;
242
+ this.log("✅ Page mapped to CDP ID", { stableTabId, cdpTargetId });
245
243
  }
246
244
  else {
247
- this.log(" Failed to get page ID, page not added to map", { url: page.url() });
245
+ this.log("⚠️ Could not find CDP ID for new page", { stableTabId });
246
+ }
247
+ // If this is the first page, select it
248
+ if (this.pages.size === 1) {
249
+ this._selectedPageId = stableTabId;
248
250
  }
251
+ await this.syncState();
249
252
  // Listen for page updates
250
253
  page.on("load", async () => {
251
- this.log("🔄 Page load event", { id, url: page.url() });
254
+ this.log("🔄 Page load event", { stableTabId, url: page.url() });
255
+ // Navigation can *sometimes* change the CDP ID. We must re-verify.
256
+ const newCdpId = await this.findCdpTargetId(page);
257
+ const pageInfo = this.pages.get(stableTabId);
258
+ if (pageInfo && newCdpId && pageInfo.cdpTargetId !== newCdpId) {
259
+ this.log("🔄 CDP Target ID changed on navigation", {
260
+ stableTabId,
261
+ old: pageInfo.cdpTargetId,
262
+ new: newCdpId,
263
+ });
264
+ pageInfo.cdpTargetId = newCdpId;
265
+ }
252
266
  await this.syncState();
253
267
  });
254
268
  page.on("close", async () => {
255
- this.log("🗑️ Page close event", { id, url: page.url() });
256
- if (id) {
257
- this.pages.delete(id);
258
- this.log("✅ Page removed from internal map", {
259
- id,
260
- remainingPages: this.pages.size,
261
- allPageIds: Array.from(this.pages.keys()),
269
+ this.log("🗑️ Page close event", { stableTabId, url: page.url() });
270
+ this.pages.delete(stableTabId);
271
+ this.log("✅ Page removed from internal map", {
272
+ stableTabId,
273
+ remaining: this.pages.size,
274
+ });
275
+ if (this._selectedPageId === stableTabId) {
276
+ const firstPage = Array.from(this.pages.keys())[0];
277
+ this._selectedPageId = firstPage || null;
278
+ this.log("🔄 Selected page changed after close", {
279
+ oldSelectedId: stableTabId,
280
+ newSelectedId: this._selectedPageId,
262
281
  });
263
- if (this._selectedPageId === id) {
264
- const firstPage = Array.from(this.pages.keys())[0];
265
- this._selectedPageId = firstPage || null;
266
- this.log("🔄 Selected page changed after close", {
267
- oldSelectedId: id,
268
- newSelectedId: this._selectedPageId,
269
- });
270
- }
271
- await this.syncState();
272
282
  }
283
+ await this.syncState();
284
+ });
285
+ page.on("framenavigated", async (frame) => {
286
+ // This event fires for *all* frames (iframes and the main page)
287
+ // We'll sync state to capture any potential title changes
288
+ this.log("🔄 Frame navigated event", { stableTabId, url: frame.url() });
289
+ await this.syncState();
273
290
  });
274
291
  });
275
292
  // Initialize with existing pages
276
293
  const existingPages = this.context.pages();
277
294
  this.log("📄 Found existing pages", { count: existingPages.length });
278
295
  for (const page of existingPages) {
279
- const id = await this.getPageId(page);
280
- this.log("🔍 Processing existing page", { id, url: page.url() });
281
- if (id) {
282
- this.pages.set(id, page);
283
- this.log("✅ Existing page added to map", {
284
- id,
285
- url: page.url(),
286
- totalPages: this.pages.size,
287
- });
288
- }
296
+ const stableTabId = uuidv4();
297
+ const cdpTargetId = await this.findCdpTargetId(page);
298
+ this.pages.set(stableTabId, { page, cdpTargetId });
299
+ this.log("✅ Existing page added to map", {
300
+ stableTabId,
301
+ cdpTargetId,
302
+ url: page.url(),
303
+ });
289
304
  }
290
- // Set initial selected page
291
305
  if (this.pages.size > 0 && !this._selectedPageId) {
292
306
  this._selectedPageId = Array.from(this.pages.keys())[0];
293
307
  this.log("🎯 Initial selected page set", { selectedPageId: this._selectedPageId });
294
308
  }
295
- this.log("✅ Initialization complete", {
296
- totalPages: this.pages.size,
297
- selectedPageId: this._selectedPageId,
298
- allPageIds: Array.from(this.pages.keys()),
299
- });
300
309
  await this.syncState();
301
310
  }
302
- async getPageId(page) {
311
+ // Renamed from getPageId to be more descriptive
312
+ async findCdpTargetId(page) {
303
313
  try {
304
314
  const pageUrl = page.url();
305
- this.log("🔍 Getting page ID", { pageUrl });
306
- // Try to get the CDP target ID directly from the page
307
- // This is more reliable than URL matching
308
- try {
309
- const cdpSession = await page.context().newCDPSession(page);
310
- const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
311
- await cdpSession.detach();
312
- if (targetInfo && targetInfo.targetId) {
313
- this.log("✅ Got target ID from CDP session", {
314
- targetId: targetInfo.targetId,
315
- url: pageUrl,
316
- });
317
- return targetInfo.targetId;
318
- }
319
- }
320
- catch (cdpError) {
321
- this.log("⚠️ Failed to get target ID from CDP session, falling back to URL matching", {
322
- error: cdpError instanceof Error ? cdpError.message : String(cdpError),
323
- });
324
- }
315
+ this.log("🔍 Finding CDP target ID for page", { pageUrl });
325
316
  const debugData = await this.getDebugURLs();
326
- this.log("📊 CDP debug data received", {
327
- totalPages: debugData.length,
328
- pages: debugData.map((p) => ({ id: p.id, type: p.type, url: p.url })),
329
- });
330
- // Exact match
331
- for (const pageData of debugData) {
332
- if (pageData.type === "page" && pageData.url === pageUrl) {
333
- this.log("✅ Found exact URL match", {
334
- id: pageData.id,
335
- url: pageUrl,
336
- });
337
- return pageData.id;
317
+ // Try to find a page in CDP that is not already mapped
318
+ const unmappedCdpPages = debugData.filter((debugPage) => {
319
+ if (debugPage.type !== "page")
320
+ return false;
321
+ // Check if any page in our map *already* has this CDP ID
322
+ for (const info of this.pages.values()) {
323
+ if (info.cdpTargetId === debugPage.id)
324
+ return false;
338
325
  }
326
+ return true;
327
+ });
328
+ // 1. Exact URL match
329
+ let match = unmappedCdpPages.find((p) => p.url === pageUrl);
330
+ if (match) {
331
+ this.log("✅ Found CDP ID by exact URL", { id: match.id, url: pageUrl });
332
+ return match.id;
339
333
  }
340
- this.log("⚠️ No exact match found, trying normalized URLs", { pageUrl });
341
- // Normalized match
334
+ // 2. Normalized URL match
342
335
  const normalizeUrl = (url) => {
343
336
  try {
344
337
  const u = new URL(url);
345
- const normalized = u.hostname.replace(/^www\./, "") + u.pathname + u.search;
346
- this.log("🔧 Normalized URL", { original: url, normalized });
347
- return normalized;
338
+ return u.hostname.replace(/^www\./, "") + u.pathname + u.search;
348
339
  }
349
340
  catch {
350
- this.log("❌ Failed to normalize URL", { url });
351
341
  return url;
352
342
  }
353
343
  };
354
344
  const normalizedPageUrl = normalizeUrl(pageUrl);
355
- for (const pageData of debugData) {
356
- if (pageData.type === "page") {
357
- const normalizedDebugUrl = normalizeUrl(pageData.url);
358
- if (normalizedDebugUrl === normalizedPageUrl) {
359
- this.log("✅ Found normalized URL match", {
360
- id: pageData.id,
361
- pageUrl: normalizedPageUrl,
362
- debugUrl: normalizedDebugUrl,
363
- });
364
- return pageData.id;
365
- }
345
+ match = unmappedCdpPages.find((p) => normalizeUrl(p.url) === normalizedPageUrl);
346
+ if (match) {
347
+ this.log("✅ Found CDP ID by normalized URL", { id: match.id, url: pageUrl });
348
+ return match.id;
349
+ }
350
+ // 3. Fallback to CDP Session
351
+ try {
352
+ const cdpSession = await page.context().newCDPSession(page);
353
+ const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
354
+ await cdpSession.detach();
355
+ if (targetInfo && targetInfo.targetId) {
356
+ this.log("✅ Found CDP ID by session", { id: targetInfo.targetId });
357
+ return targetInfo.targetId;
366
358
  }
367
359
  }
368
- this.log("❌ No match found for page", {
369
- pageUrl,
370
- normalizedPageUrl,
371
- availablePages: debugData.filter((p) => p.type === "page").map((p) => p.url),
372
- });
360
+ catch (cdpError) {
361
+ this.log("⚠️ Failed to get target ID from CDP session", { error: cdpError });
362
+ }
363
+ this.log("❌ No match found for page", { pageUrl });
373
364
  return null;
374
365
  }
375
366
  catch (error) {
376
- this.log("❌ Error getting page ID", {
377
- error: error instanceof Error ? error.message : String(error),
378
- stack: error instanceof Error ? error.stack : undefined,
379
- });
367
+ this.log("❌ Error finding CDP target ID", { error });
380
368
  return null;
381
369
  }
382
370
  }
383
371
  async getDebugURLs() {
384
372
  const url = `${this.CDP_CONNECT_URL}/json`;
385
- this.log("📡 Fetching debug URLs", { url });
386
373
  try {
387
374
  const response = await fetch(url);
388
375
  if (!response.ok) {
389
- this.log("❌ Failed to fetch debug URLs", {
390
- status: response.status,
391
- statusText: response.statusText,
392
- });
393
376
  throw new Error("Error while fetching debug URL");
394
377
  }
395
- const data = await response.json();
396
- this.log("✅ Debug URLs fetched successfully", {
397
- count: data.length,
398
- pages: data.map((p) => ({ id: p.id, type: p.type, url: p.url })),
399
- });
400
- return data;
378
+ return await response.json();
401
379
  }
402
380
  catch (error) {
403
- this.log("❌ Exception while fetching debug URLs", {
404
- error: error instanceof Error ? error.message : String(error),
405
- });
406
- throw error;
381
+ this.log("❌ Exception while fetching debug URLs", { error });
382
+ return []; // Return empty array on failure
407
383
  }
408
384
  }
409
385
  async syncState() {
@@ -413,134 +389,48 @@ export class RemoteBrowserService extends EventEmitter {
413
389
  this.log("✅ State sync complete", {
414
390
  pagesCount: state.pages.length,
415
391
  selectedPageId: state.selectedPageId,
416
- pages: state.pages.map((p) => ({ id: p.id, title: p.title, url: p.url })),
417
392
  });
418
393
  this.emit("BrowserService.stateSync", state);
419
394
  }
420
395
  catch (error) {
421
- this.log("❌ Error syncing state", {
422
- error: error instanceof Error ? error.message : String(error),
423
- stack: error instanceof Error ? error.stack : undefined,
424
- });
396
+ this.log("❌ Error syncing state", { error });
425
397
  }
426
398
  }
427
399
  async getState() {
428
- this.log("📊 Getting current state", {
429
- internalPagesCount: this.pages.size,
430
- internalPageIds: Array.from(this.pages.keys()),
431
- selectedPageId: this._selectedPageId,
432
- });
400
+ this.log("📊 Getting current state");
433
401
  const debugData = await this.getDebugURLs();
434
- this.log("📊 Debug data for state", {
435
- debugPagesCount: debugData.length,
436
- debugPages: debugData.map((p) => ({ id: p.id, type: p.type, url: p.url })),
437
- });
438
402
  const pagesData = [];
439
- const updatedPages = new Map();
440
- const matchedDebugIds = new Set(); // Track which CDP IDs we've already matched
441
- let updatedSelectedPageId = this._selectedPageId;
442
- for (const [oldId, page] of this.pages.entries()) {
443
- this.log("🔍 Processing page from internal map", {
444
- oldId,
445
- url: page.url(),
446
- });
447
- // Try to get the current CDP target ID directly from the page
448
- let currentId = null;
449
- try {
450
- const cdpSession = await page.context().newCDPSession(page);
451
- const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
452
- await cdpSession.detach();
453
- if (targetInfo && targetInfo.targetId) {
454
- currentId = targetInfo.targetId;
455
- this.log("✅ Got current target ID from CDP session", {
456
- oldId,
457
- currentId,
458
- url: page.url(),
459
- });
460
- }
461
- }
462
- catch (cdpError) {
463
- this.log("⚠️ Failed to get target ID from CDP session", {
464
- oldId,
465
- error: cdpError instanceof Error ? cdpError.message : String(cdpError),
466
- });
467
- }
468
- // If we got the current ID from CDP session, use it
469
- let debugInfo = currentId ? debugData.find((d) => d.id === currentId) : null;
470
- // Fallback 1: Try to find by old ID
471
- if (!debugInfo) {
472
- debugInfo = debugData.find((d) => d.id === oldId && !matchedDebugIds.has(d.id));
473
- }
474
- // Fallback 2: Try to find by URL (only if not already matched)
403
+ // Re-verify CDP Target IDs for all pages, as they might have changed
404
+ for (const [stableTabId, pageInfo] of this.pages.entries()) {
405
+ let currentCdpId = pageInfo.cdpTargetId;
406
+ // Find the CDP info for our KNOWN cdpTargetId
407
+ let debugInfo = debugData.find((d) => d.id === currentCdpId);
408
+ // If not found (e.g., ID changed), try to find it again
475
409
  if (!debugInfo) {
476
- this.log("⚠️ Page ID not found in CDP, attempting to match by URL", {
477
- oldId,
478
- pageUrl: page.url(),
479
- });
480
- debugInfo = debugData.find((d) => d.type === "page" && d.url === page.url() && !matchedDebugIds.has(d.id));
481
- if (debugInfo) {
482
- this.log("✅ Found page by URL match, updating ID", {
483
- oldId,
484
- newId: debugInfo.id,
485
- url: page.url(),
486
- });
410
+ this.log("⚠️ Re-finding CDP ID for", { stableTabId });
411
+ const newCdpId = await this.findCdpTargetId(pageInfo.page);
412
+ if (newCdpId) {
413
+ pageInfo.cdpTargetId = newCdpId;
414
+ debugInfo = debugData.find((d) => d.id === newCdpId);
415
+ this.log("✅ Re-found CDP ID", { stableTabId, newCdpId });
487
416
  }
488
417
  }
489
- else {
490
- this.log("✅ Found matching debug info by ID", {
491
- id: debugInfo.id,
492
- debugUrl: debugInfo.url,
493
- pageUrl: page.url(),
418
+ try {
419
+ pagesData.push({
420
+ id: stableTabId, // Use the STABLE ID
421
+ title: await pageInfo.page.title(),
422
+ url: pageInfo.page.url(),
423
+ wsDebuggerUrl: debugInfo?.webSocketDebuggerUrl || "", // Get the *current* ws url
494
424
  });
495
425
  }
496
- if (debugInfo) {
497
- // Mark this CDP ID as matched to avoid duplicate matches
498
- matchedDebugIds.add(debugInfo.id);
499
- // Update selected page ID if this was the selected page and ID changed
500
- if (oldId === this._selectedPageId && oldId !== debugInfo.id) {
501
- updatedSelectedPageId = debugInfo.id;
502
- this.log("🔄 Updated selected page ID", {
503
- oldId,
504
- newId: debugInfo.id,
505
- });
506
- }
507
- try {
508
- const pageData = {
509
- id: debugInfo.id,
510
- title: await page.title(),
511
- url: page.url(),
512
- wsDebuggerUrl: debugInfo.webSocketDebuggerUrl || "",
513
- };
514
- pagesData.push(pageData);
515
- updatedPages.set(debugInfo.id, page);
516
- this.log("✅ Page added to state", pageData);
517
- }
518
- catch (error) {
519
- this.log("❌ Error getting page data", {
520
- id: oldId,
521
- error: error instanceof Error ? error.message : String(error),
522
- });
523
- }
524
- }
525
- else {
526
- this.log("⚠️ No matching debug info found by ID or URL", {
527
- oldId,
528
- pageUrl: page.url(),
529
- availableDebugIds: debugData.map((d) => d.id),
530
- availableDebugUrls: debugData.map((d) => d.url),
531
- });
426
+ catch (error) {
427
+ this.log("❌ Error getting page data (page might be closed)", { stableTabId });
532
428
  }
533
429
  }
534
- // Update the internal pages map with current CDP IDs
535
- if (updatedPages.size !== this.pages.size || updatedSelectedPageId !== this._selectedPageId) {
536
- this.log("🔄 Updating internal state with new CDP IDs", {
537
- oldPagesCount: this.pages.size,
538
- newPagesCount: updatedPages.size,
539
- oldSelectedId: this._selectedPageId,
540
- newSelectedId: updatedSelectedPageId,
541
- });
542
- this.pages = updatedPages;
543
- this._selectedPageId = updatedSelectedPageId;
430
+ // Ensure selectedPageId is valid
431
+ if (this._selectedPageId && !this.pages.has(this._selectedPageId)) {
432
+ this._selectedPageId = this.pages.size > 0 ? Array.from(this.pages.keys())[0] : null;
433
+ this.log("🔄 Corrected selectedPageId", { new: this._selectedPageId });
544
434
  }
545
435
  const state = {
546
436
  pages: pagesData,
@@ -552,108 +442,502 @@ export class RemoteBrowserService extends EventEmitter {
552
442
  async createTab(url = "about:blank") {
553
443
  try {
554
444
  this.log("🆕 Creating new tab", { url });
555
- const page = await this.context.newPage();
556
- this.log("✅ New page created by Playwright", { url: page.url() });
557
- if (typeof url === "string" && url !== "about:blank") {
558
- this.log("🌐 Navigating to URL", { url });
445
+ const page = await this.context.newPage(); // This will trigger the 'page' event
446
+ if (url !== "about:blank") {
559
447
  await page.goto(url, { waitUntil: "domcontentloaded" });
560
- this.log("✅ Navigation complete", { finalUrl: page.url() });
561
- }
562
- // Wait for CDP to register the page
563
- this.log("⏳ Waiting for CDP registration...");
564
- await new Promise((resolve) => setTimeout(resolve, 500));
565
- const id = await this.getPageId(page);
566
- this.log("🔍 Retrieved page ID after wait", { id, url: page.url() });
567
- if (id) {
568
- this.pages.set(id, page);
569
- this._selectedPageId = id;
570
- this.log("✅ Tab created successfully", {
571
- id,
572
- url: page.url(),
573
- totalPages: this.pages.size,
574
- allPageIds: Array.from(this.pages.keys()),
575
- selectedPageId: this._selectedPageId,
576
- });
577
448
  }
578
- else {
579
- this.log("❌ Failed to get page ID for new tab", { url: page.url() });
580
- // Try to get debug data to see what's available
581
- const debugData = await this.getDebugURLs();
582
- this.log("🔍 Current CDP state after failed ID retrieval", {
583
- debugPages: debugData.map((p) => ({ id: p.id, url: p.url, type: p.type })),
584
- });
449
+ // The 'page' event handler now manages setting the selected ID
450
+ // We just need to find the new page and select it
451
+ for (const [stableTabId, pageInfo] of this.pages.entries()) {
452
+ if (pageInfo.page === page) {
453
+ this._selectedPageId = stableTabId;
454
+ this.log("✅ New tab created and selected", { stableTabId, url });
455
+ break;
456
+ }
585
457
  }
586
458
  await this.syncState();
587
459
  }
588
460
  catch (error) {
589
- this.log("❌ Error creating tab", {
590
- url,
591
- error: error instanceof Error ? error.message : String(error),
592
- stack: error instanceof Error ? error.stack : undefined,
593
- });
594
- throw error;
461
+ this.log("❌ Error creating tab", { error });
595
462
  }
596
463
  }
597
- async closeTab(pageId) {
464
+ async closeTab(stableTabId) {
598
465
  try {
599
- this.log("🗑️ Closing tab", { pageId });
600
- const page = this.pages.get(pageId);
601
- if (page) {
602
- this.log("✅ Found page to close", { pageId, url: page.url() });
603
- await page.close();
604
- this.log("✅ Page closed successfully", { pageId });
466
+ this.log("🗑️ Closing tab", { stableTabId });
467
+ const pageInfo = this.pages.get(stableTabId);
468
+ if (pageInfo) {
469
+ await pageInfo.page.close(); // This will trigger the 'close' event
605
470
  }
606
471
  else {
607
- this.log("⚠️ Page not found", {
608
- pageId,
609
- availablePageIds: Array.from(this.pages.keys()),
610
- });
472
+ this.log("⚠️ Page not found for closing", { stableTabId });
611
473
  }
474
+ // 'close' event handler will manage state update
612
475
  }
613
476
  catch (error) {
614
- this.log("❌ Error closing tab", {
615
- pageId,
616
- error: error instanceof Error ? error.message : String(error),
617
- });
618
- throw error;
477
+ this.log("❌ Error closing tab", { error });
619
478
  }
620
479
  }
621
- async selectTab(pageId) {
480
+ async selectTab(stableTabId) {
622
481
  try {
623
- this.log("🎯 Selecting tab", { pageId });
624
- const page = this.pages.get(pageId);
625
- if (page) {
626
- this._selectedPageId = pageId;
627
- await page.bringToFront();
628
- this.log("✅ Tab selected successfully", {
629
- pageId,
630
- url: page.url(),
631
- selectedPageId: this._selectedPageId,
632
- });
482
+ this.log("🎯 Selecting tab", { stableTabId });
483
+ const pageInfo = this.pages.get(stableTabId);
484
+ if (pageInfo) {
485
+ this._selectedPageId = stableTabId;
486
+ await pageInfo.page.bringToFront();
487
+ this.log("✅ Tab selected successfully", { stableTabId });
633
488
  await this.syncState();
634
489
  }
635
490
  else {
636
- this.log("⚠️ Page not found for selection", {
637
- pageId,
638
- availablePageIds: Array.from(this.pages.keys()),
639
- });
491
+ this.log("⚠️ Page not found for selection", { stableTabId });
640
492
  }
641
493
  }
642
494
  catch (error) {
643
- this.log("❌ Error selecting tab", {
644
- pageId,
645
- error: error instanceof Error ? error.message : String(error),
646
- });
647
- throw error;
495
+ this.log("❌ Error selecting tab", { error });
648
496
  }
649
497
  }
498
+ // ... (getSelectedPage remains the same, but operates on new state)
650
499
  getSelectedPage() {
651
- const page = this._selectedPageId ? this.pages.get(this._selectedPageId) || null : null;
500
+ const pageInfo = this._selectedPageId ? this.pages.get(this._selectedPageId) : null;
652
501
  this.log("🔍 Getting selected page", {
653
502
  selectedPageId: this._selectedPageId,
654
- found: !!page,
655
- url: page?.url(),
503
+ found: !!pageInfo,
504
+ url: pageInfo?.page.url(),
656
505
  });
657
- return page;
506
+ return pageInfo?.page || null;
658
507
  }
659
508
  }
509
+ // export class RemoteBrowserService extends EventEmitter {
510
+ // private CDP_CONNECT_URL: string;
511
+ // private context: BrowserContext;
512
+ // private pages: Map<string, Page> = new Map();
513
+ // private _selectedPageId: string | null = null;
514
+ // constructor({ CDP_CONNECT_URL, context }: { CDP_CONNECT_URL: string; context: BrowserContext }) {
515
+ // super();
516
+ // this.CDP_CONNECT_URL = CDP_CONNECT_URL;
517
+ // this.context = context;
518
+ // this.log("🚀 RemoteBrowserService initialized", { CDP_CONNECT_URL });
519
+ // this.initializeListeners();
520
+ // }
521
+ // private log(message: string, data?: any) {
522
+ // const timestamp = new Date().toISOString();
523
+ // console.log(`[${timestamp}] [RemoteBrowserService] ${message}`, data ? JSON.stringify(data, null, 2) : "");
524
+ // }
525
+ // private async initializeListeners() {
526
+ // this.log("📡 Initializing listeners");
527
+ // // Listen for new pages
528
+ // this.context.on("page", async (page) => {
529
+ // this.log("🆕 New page event triggered", { url: page.url() });
530
+ // const id = await this.getPageId(page);
531
+ // this.log("🔍 Got page ID from CDP", { id, url: page.url() });
532
+ // if (id) {
533
+ // this.pages.set(id, page);
534
+ // this.log("✅ Page added to internal map", {
535
+ // id,
536
+ // url: page.url(),
537
+ // totalPages: this.pages.size,
538
+ // allPageIds: Array.from(this.pages.keys()),
539
+ // });
540
+ // await this.syncState();
541
+ // } else {
542
+ // this.log("❌ Failed to get page ID, page not added to map", { url: page.url() });
543
+ // }
544
+ // // Listen for page updates
545
+ // page.on("load", async () => {
546
+ // this.log("🔄 Page load event", { id, url: page.url() });
547
+ // await this.syncState();
548
+ // });
549
+ // page.on("close", async () => {
550
+ // this.log("🗑️ Page close event", { id, url: page.url() });
551
+ // if (id) {
552
+ // this.pages.delete(id);
553
+ // this.log("✅ Page removed from internal map", {
554
+ // id,
555
+ // remainingPages: this.pages.size,
556
+ // allPageIds: Array.from(this.pages.keys()),
557
+ // });
558
+ // if (this._selectedPageId === id) {
559
+ // const firstPage = Array.from(this.pages.keys())[0];
560
+ // this._selectedPageId = firstPage || null;
561
+ // this.log("🔄 Selected page changed after close", {
562
+ // oldSelectedId: id,
563
+ // newSelectedId: this._selectedPageId,
564
+ // });
565
+ // }
566
+ // await this.syncState();
567
+ // }
568
+ // });
569
+ // });
570
+ // // Initialize with existing pages
571
+ // const existingPages = this.context.pages();
572
+ // this.log("📄 Found existing pages", { count: existingPages.length });
573
+ // for (const page of existingPages) {
574
+ // const id = await this.getPageId(page);
575
+ // this.log("🔍 Processing existing page", { id, url: page.url() });
576
+ // if (id) {
577
+ // this.pages.set(id, page);
578
+ // this.log("✅ Existing page added to map", {
579
+ // id,
580
+ // url: page.url(),
581
+ // totalPages: this.pages.size,
582
+ // });
583
+ // }
584
+ // }
585
+ // // Set initial selected page
586
+ // if (this.pages.size > 0 && !this._selectedPageId) {
587
+ // this._selectedPageId = Array.from(this.pages.keys())[0];
588
+ // this.log("🎯 Initial selected page set", { selectedPageId: this._selectedPageId });
589
+ // }
590
+ // this.log("✅ Initialization complete", {
591
+ // totalPages: this.pages.size,
592
+ // selectedPageId: this._selectedPageId,
593
+ // allPageIds: Array.from(this.pages.keys()),
594
+ // });
595
+ // await this.syncState();
596
+ // }
597
+ // private async getPageId(page: Page): Promise<string | null> {
598
+ // try {
599
+ // const pageUrl = page.url();
600
+ // this.log("🔍 Getting page ID", { pageUrl });
601
+ // // Fetch debug data from /json endpoint (more reliable for matching)
602
+ // const debugData = await this.getDebugURLs();
603
+ // this.log("📊 CDP debug data received", {
604
+ // totalPages: debugData.length,
605
+ // pages: debugData.map((p) => ({ id: p.id, type: p.type, url: p.url })),
606
+ // });
607
+ // // Exact URL match
608
+ // for (const pageData of debugData) {
609
+ // if (pageData.type === "page" && pageData.url === pageUrl) {
610
+ // this.log("✅ Found exact URL match in /json", {
611
+ // id: pageData.id,
612
+ // url: pageUrl,
613
+ // });
614
+ // return pageData.id;
615
+ // }
616
+ // }
617
+ // this.log("⚠️ No exact match found, trying normalized URLs", { pageUrl });
618
+ // // Normalized URL match
619
+ // const normalizeUrl = (url: string) => {
620
+ // try {
621
+ // const u = new URL(url);
622
+ // const normalized = u.hostname.replace(/^www\./, "") + u.pathname + u.search;
623
+ // return normalized;
624
+ // } catch {
625
+ // return url;
626
+ // }
627
+ // };
628
+ // const normalizedPageUrl = normalizeUrl(pageUrl);
629
+ // for (const pageData of debugData) {
630
+ // if (pageData.type === "page") {
631
+ // const normalizedDebugUrl = normalizeUrl(pageData.url);
632
+ // if (normalizedDebugUrl === normalizedPageUrl) {
633
+ // this.log("✅ Found normalized URL match in /json", {
634
+ // id: pageData.id,
635
+ // pageUrl: normalizedPageUrl,
636
+ // debugUrl: normalizedDebugUrl,
637
+ // });
638
+ // return pageData.id;
639
+ // }
640
+ // }
641
+ // }
642
+ // // If still not found, try CDP session as fallback
643
+ // this.log("⚠️ Not found in /json, trying CDP Target.getTargetInfo", { pageUrl });
644
+ // try {
645
+ // const cdpSession = await page.context().newCDPSession(page);
646
+ // const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
647
+ // await cdpSession.detach();
648
+ // if (targetInfo && targetInfo.targetId) {
649
+ // // Verify this target ID exists in debug data
650
+ // const targetExists = debugData.some((d) => d.id === targetInfo.targetId);
651
+ // if (targetExists) {
652
+ // this.log("✅ Got target ID from CDP session and verified in /json", {
653
+ // targetId: targetInfo.targetId,
654
+ // url: pageUrl,
655
+ // });
656
+ // return targetInfo.targetId;
657
+ // } else {
658
+ // this.log("⚠️ Target ID from CDP session not found in /json (ID mismatch)", {
659
+ // cdpTargetId: targetInfo.targetId,
660
+ // availableIds: debugData.map((d) => d.id),
661
+ // });
662
+ // }
663
+ // }
664
+ // } catch (cdpError) {
665
+ // this.log("⚠️ Failed to get target ID from CDP session", {
666
+ // error: cdpError instanceof Error ? cdpError.message : String(cdpError),
667
+ // });
668
+ // }
669
+ // this.log("❌ No match found for page", {
670
+ // pageUrl,
671
+ // normalizedPageUrl,
672
+ // availablePages: debugData.filter((p) => p.type === "page").map((p) => p.url),
673
+ // });
674
+ // return null;
675
+ // } catch (error) {
676
+ // this.log("❌ Error getting page ID", {
677
+ // error: error instanceof Error ? error.message : String(error),
678
+ // stack: error instanceof Error ? error.stack : undefined,
679
+ // });
680
+ // return null;
681
+ // }
682
+ // }
683
+ // private async getDebugURLs(): Promise<DebugPageInfo[]> {
684
+ // const url = `${this.CDP_CONNECT_URL}/json`;
685
+ // this.log("📡 Fetching debug URLs", { url });
686
+ // try {
687
+ // const response = await fetch(url);
688
+ // if (!response.ok) {
689
+ // this.log("❌ Failed to fetch debug URLs", {
690
+ // status: response.status,
691
+ // statusText: response.statusText,
692
+ // });
693
+ // throw new Error("Error while fetching debug URL");
694
+ // }
695
+ // const data = await response.json();
696
+ // this.log("✅ Debug URLs fetched successfully", {
697
+ // count: data.length,
698
+ // pages: data.map((p: any) => ({ id: p.id, type: p.type, url: p.url })),
699
+ // });
700
+ // return data;
701
+ // } catch (error) {
702
+ // this.log("❌ Exception while fetching debug URLs", {
703
+ // error: error instanceof Error ? error.message : String(error),
704
+ // });
705
+ // throw error;
706
+ // }
707
+ // }
708
+ // private async syncState() {
709
+ // try {
710
+ // this.log("🔄 Starting state sync");
711
+ // const state = await this.getState();
712
+ // this.log("✅ State sync complete", {
713
+ // pagesCount: state.pages.length,
714
+ // selectedPageId: state.selectedPageId,
715
+ // pages: state.pages.map((p) => ({ id: p.id, title: p.title, url: p.url })),
716
+ // });
717
+ // this.emit("BrowserService.stateSync", state);
718
+ // } catch (error) {
719
+ // this.log("❌ Error syncing state", {
720
+ // error: error instanceof Error ? error.message : String(error),
721
+ // stack: error instanceof Error ? error.stack : undefined,
722
+ // });
723
+ // }
724
+ // }
725
+ // async getState(): Promise<BrowserState> {
726
+ // this.log("📊 Getting current state", {
727
+ // internalPagesCount: this.pages.size,
728
+ // internalPageIds: Array.from(this.pages.keys()),
729
+ // selectedPageId: this._selectedPageId,
730
+ // });
731
+ // const debugData = await this.getDebugURLs();
732
+ // this.log("📊 Debug data for state", {
733
+ // debugPagesCount: debugData.length,
734
+ // debugPages: debugData.map((p) => ({ id: p.id, type: p.type, url: p.url })),
735
+ // });
736
+ // const pagesData: PageData[] = [];
737
+ // const updatedPages = new Map<string, Page>();
738
+ // const matchedDebugIds = new Set<string>(); // Track which CDP IDs we've already matched
739
+ // let updatedSelectedPageId = this._selectedPageId;
740
+ // for (const [oldId, page] of this.pages.entries()) {
741
+ // this.log("🔍 Processing page from internal map", {
742
+ // oldId,
743
+ // url: page.url(),
744
+ // });
745
+ // // Try to find by old ID first (most common case - ID hasn't changed)
746
+ // let debugInfo = debugData.find((d) => d.id === oldId && !matchedDebugIds.has(d.id));
747
+ // // Fallback: Try to find by URL (page ID may have changed)
748
+ // if (!debugInfo) {
749
+ // this.log("⚠️ Page ID not found in CDP, attempting to match by URL", {
750
+ // oldId,
751
+ // pageUrl: page.url(),
752
+ // });
753
+ // debugInfo = debugData.find((d) => d.type === "page" && d.url === page.url() && !matchedDebugIds.has(d.id));
754
+ // if (debugInfo) {
755
+ // this.log("✅ Found page by URL match, updating ID", {
756
+ // oldId,
757
+ // newId: debugInfo.id,
758
+ // url: page.url(),
759
+ // });
760
+ // // Update selected page ID if this was the selected page and ID changed
761
+ // if (oldId === this._selectedPageId) {
762
+ // updatedSelectedPageId = debugInfo.id;
763
+ // this.log("🔄 Updated selected page ID", {
764
+ // oldId,
765
+ // newId: debugInfo.id,
766
+ // });
767
+ // }
768
+ // }
769
+ // } else {
770
+ // this.log("✅ Found matching debug info by ID", {
771
+ // id: debugInfo.id,
772
+ // debugUrl: debugInfo.url,
773
+ // pageUrl: page.url(),
774
+ // });
775
+ // }
776
+ // if (debugInfo) {
777
+ // // Mark this CDP ID as matched to avoid duplicate matches
778
+ // matchedDebugIds.add(debugInfo.id);
779
+ // try {
780
+ // const pageData = {
781
+ // id: debugInfo.id,
782
+ // title: await page.title(),
783
+ // url: page.url(),
784
+ // wsDebuggerUrl: debugInfo.webSocketDebuggerUrl || "",
785
+ // };
786
+ // pagesData.push(pageData);
787
+ // updatedPages.set(debugInfo.id, page);
788
+ // this.log("✅ Page added to state", pageData);
789
+ // } catch (error) {
790
+ // this.log("❌ Error getting page data", {
791
+ // id: oldId,
792
+ // error: error instanceof Error ? error.message : String(error),
793
+ // });
794
+ // }
795
+ // } else {
796
+ // this.log("⚠️ No matching debug info found by ID or URL", {
797
+ // oldId,
798
+ // pageUrl: page.url(),
799
+ // availableDebugIds: debugData.map((d) => d.id),
800
+ // availableDebugUrls: debugData.map((d) => d.url),
801
+ // alreadyMatched: Array.from(matchedDebugIds),
802
+ // });
803
+ // }
804
+ // }
805
+ // // Update the internal pages map with current CDP IDs
806
+ // if (updatedPages.size !== this.pages.size || updatedSelectedPageId !== this._selectedPageId) {
807
+ // this.log("🔄 Updating internal state with new CDP IDs", {
808
+ // oldPagesCount: this.pages.size,
809
+ // newPagesCount: updatedPages.size,
810
+ // oldSelectedId: this._selectedPageId,
811
+ // newSelectedId: updatedSelectedPageId,
812
+ // });
813
+ // this.pages = updatedPages;
814
+ // this._selectedPageId = updatedSelectedPageId;
815
+ // }
816
+ // const state = {
817
+ // pages: pagesData,
818
+ // selectedPageId: this._selectedPageId,
819
+ // };
820
+ // this.log("📦 Final state", state);
821
+ // return state;
822
+ // }
823
+ // async createTab(url: string = "about:blank"): Promise<void> {
824
+ // try {
825
+ // this.log("🆕 Creating new tab", { url });
826
+ // const page = await this.context.newPage();
827
+ // this.log("✅ New page created by Playwright", { url: page.url() });
828
+ // if (typeof url === "string" && url !== "about:blank") {
829
+ // this.log("🌐 Navigating to URL", { url });
830
+ // await page.goto(url, { waitUntil: "domcontentloaded" });
831
+ // this.log("✅ Navigation complete", { finalUrl: page.url() });
832
+ // }
833
+ // // Wait longer for CDP to register and stabilize the page
834
+ // this.log("⏳ Waiting for CDP registration and stabilization...");
835
+ // await new Promise((resolve) => setTimeout(resolve, 1000)); // Increased to 1 second
836
+ // // Try multiple times to get a stable page ID
837
+ // let id: string | null = null;
838
+ // let attempts = 0;
839
+ // const maxAttempts = 5;
840
+ // while (!id && attempts < maxAttempts) {
841
+ // attempts++;
842
+ // this.log(`🔍 Attempt ${attempts}/${maxAttempts} to get page ID`);
843
+ // id = await this.getPageId(page);
844
+ // if (!id) {
845
+ // this.log(`⏳ Page ID not found, waiting 500ms before retry...`);
846
+ // await new Promise((resolve) => setTimeout(resolve, 500));
847
+ // }
848
+ // }
849
+ // this.log("🔍 Retrieved page ID after retries", {
850
+ // id,
851
+ // url: page.url(),
852
+ // attempts,
853
+ // });
854
+ // if (id) {
855
+ // this.pages.set(id, page);
856
+ // this._selectedPageId = id;
857
+ // this.log("✅ Tab created successfully", {
858
+ // id,
859
+ // url: page.url(),
860
+ // totalPages: this.pages.size,
861
+ // allPageIds: Array.from(this.pages.keys()),
862
+ // selectedPageId: this._selectedPageId,
863
+ // });
864
+ // } else {
865
+ // this.log("❌ Failed to get page ID for new tab after all retries", {
866
+ // url: page.url(),
867
+ // attempts,
868
+ // });
869
+ // // Try to get debug data to see what's available
870
+ // const debugData = await this.getDebugURLs();
871
+ // this.log("🔍 Current CDP state after failed ID retrieval", {
872
+ // debugPages: debugData.map((p) => ({ id: p.id, url: p.url, type: p.type })),
873
+ // });
874
+ // }
875
+ // await this.syncState();
876
+ // } catch (error) {
877
+ // this.log("❌ Error creating tab", {
878
+ // url,
879
+ // error: error instanceof Error ? error.message : String(error),
880
+ // stack: error instanceof Error ? error.stack : undefined,
881
+ // });
882
+ // throw error;
883
+ // }
884
+ // }
885
+ // async closeTab(pageId: string): Promise<void> {
886
+ // try {
887
+ // this.log("🗑️ Closing tab", { pageId });
888
+ // const page = this.pages.get(pageId);
889
+ // if (page) {
890
+ // this.log("✅ Found page to close", { pageId, url: page.url() });
891
+ // await page.close();
892
+ // this.log("✅ Page closed successfully", { pageId });
893
+ // } else {
894
+ // this.log("⚠️ Page not found", {
895
+ // pageId,
896
+ // availablePageIds: Array.from(this.pages.keys()),
897
+ // });
898
+ // }
899
+ // } catch (error) {
900
+ // this.log("❌ Error closing tab", {
901
+ // pageId,
902
+ // error: error instanceof Error ? error.message : String(error),
903
+ // });
904
+ // throw error;
905
+ // }
906
+ // }
907
+ // async selectTab(pageId: string): Promise<void> {
908
+ // try {
909
+ // this.log("🎯 Selecting tab", { pageId });
910
+ // const page = this.pages.get(pageId);
911
+ // if (page) {
912
+ // this._selectedPageId = pageId;
913
+ // await page.bringToFront();
914
+ // this.log("✅ Tab selected successfully", {
915
+ // pageId,
916
+ // url: page.url(),
917
+ // selectedPageId: this._selectedPageId,
918
+ // });
919
+ // await this.syncState();
920
+ // } else {
921
+ // this.log("⚠️ Page not found for selection", {
922
+ // pageId,
923
+ // availablePageIds: Array.from(this.pages.keys()),
924
+ // });
925
+ // }
926
+ // } catch (error) {
927
+ // this.log("❌ Error selecting tab", {
928
+ // pageId,
929
+ // error: error instanceof Error ? error.message : String(error),
930
+ // });
931
+ // throw error;
932
+ // }
933
+ // }
934
+ // getSelectedPage(): Page | null {
935
+ // const page = this._selectedPageId ? this.pages.get(this._selectedPageId) || null : null;
936
+ // this.log("🔍 Getting selected page", {
937
+ // selectedPageId: this._selectedPageId,
938
+ // found: !!page,
939
+ // url: page?.url(),
940
+ // });
941
+ // return page;
942
+ // }
943
+ // }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-blinq/cucumber_client",
3
- "version": "1.0.1587-dev",
3
+ "version": "1.0.1589-dev",
4
4
  "description": " ",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",