@dev-blinq/cucumber_client 1.0.1588-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,191 +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 });
248
246
  }
247
+ // If this is the first page, select it
248
+ if (this.pages.size === 1) {
249
+ this._selectedPageId = stableTabId;
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
- // Fetch debug data from /json endpoint (more reliable for matching)
315
+ this.log("🔍 Finding CDP target ID for page", { pageUrl });
307
316
  const debugData = await this.getDebugURLs();
308
- this.log("📊 CDP debug data received", {
309
- totalPages: debugData.length,
310
- pages: debugData.map((p) => ({ id: p.id, type: p.type, url: p.url })),
311
- });
312
- // Exact URL match
313
- for (const pageData of debugData) {
314
- if (pageData.type === "page" && pageData.url === pageUrl) {
315
- this.log("✅ Found exact URL match in /json", {
316
- id: pageData.id,
317
- url: pageUrl,
318
- });
319
- 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;
320
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;
321
333
  }
322
- this.log("⚠️ No exact match found, trying normalized URLs", { pageUrl });
323
- // Normalized URL match
334
+ // 2. Normalized URL match
324
335
  const normalizeUrl = (url) => {
325
336
  try {
326
337
  const u = new URL(url);
327
- const normalized = u.hostname.replace(/^www\./, "") + u.pathname + u.search;
328
- return normalized;
338
+ return u.hostname.replace(/^www\./, "") + u.pathname + u.search;
329
339
  }
330
340
  catch {
331
341
  return url;
332
342
  }
333
343
  };
334
344
  const normalizedPageUrl = normalizeUrl(pageUrl);
335
- for (const pageData of debugData) {
336
- if (pageData.type === "page") {
337
- const normalizedDebugUrl = normalizeUrl(pageData.url);
338
- if (normalizedDebugUrl === normalizedPageUrl) {
339
- this.log("✅ Found normalized URL match in /json", {
340
- id: pageData.id,
341
- pageUrl: normalizedPageUrl,
342
- debugUrl: normalizedDebugUrl,
343
- });
344
- return pageData.id;
345
- }
346
- }
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;
347
349
  }
348
- // If still not found, try CDP session as fallback
349
- this.log("⚠️ Not found in /json, trying CDP Target.getTargetInfo", { pageUrl });
350
+ // 3. Fallback to CDP Session
350
351
  try {
351
352
  const cdpSession = await page.context().newCDPSession(page);
352
353
  const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
353
354
  await cdpSession.detach();
354
355
  if (targetInfo && targetInfo.targetId) {
355
- // Verify this target ID exists in debug data
356
- const targetExists = debugData.some((d) => d.id === targetInfo.targetId);
357
- if (targetExists) {
358
- this.log("✅ Got target ID from CDP session and verified in /json", {
359
- targetId: targetInfo.targetId,
360
- url: pageUrl,
361
- });
362
- return targetInfo.targetId;
363
- }
364
- else {
365
- this.log("⚠️ Target ID from CDP session not found in /json (ID mismatch)", {
366
- cdpTargetId: targetInfo.targetId,
367
- availableIds: debugData.map((d) => d.id),
368
- });
369
- }
356
+ this.log("✅ Found CDP ID by session", { id: targetInfo.targetId });
357
+ return targetInfo.targetId;
370
358
  }
371
359
  }
372
360
  catch (cdpError) {
373
- this.log("⚠️ Failed to get target ID from CDP session", {
374
- error: cdpError instanceof Error ? cdpError.message : String(cdpError),
375
- });
361
+ this.log("⚠️ Failed to get target ID from CDP session", { error: cdpError });
376
362
  }
377
- this.log("❌ No match found for page", {
378
- pageUrl,
379
- normalizedPageUrl,
380
- availablePages: debugData.filter((p) => p.type === "page").map((p) => p.url),
381
- });
363
+ this.log("❌ No match found for page", { pageUrl });
382
364
  return null;
383
365
  }
384
366
  catch (error) {
385
- this.log("❌ Error getting page ID", {
386
- error: error instanceof Error ? error.message : String(error),
387
- stack: error instanceof Error ? error.stack : undefined,
388
- });
367
+ this.log("❌ Error finding CDP target ID", { error });
389
368
  return null;
390
369
  }
391
370
  }
392
371
  async getDebugURLs() {
393
372
  const url = `${this.CDP_CONNECT_URL}/json`;
394
- this.log("📡 Fetching debug URLs", { url });
395
373
  try {
396
374
  const response = await fetch(url);
397
375
  if (!response.ok) {
398
- this.log("❌ Failed to fetch debug URLs", {
399
- status: response.status,
400
- statusText: response.statusText,
401
- });
402
376
  throw new Error("Error while fetching debug URL");
403
377
  }
404
- const data = await response.json();
405
- this.log("✅ Debug URLs fetched successfully", {
406
- count: data.length,
407
- pages: data.map((p) => ({ id: p.id, type: p.type, url: p.url })),
408
- });
409
- return data;
378
+ return await response.json();
410
379
  }
411
380
  catch (error) {
412
- this.log("❌ Exception while fetching debug URLs", {
413
- error: error instanceof Error ? error.message : String(error),
414
- });
415
- throw error;
381
+ this.log("❌ Exception while fetching debug URLs", { error });
382
+ return []; // Return empty array on failure
416
383
  }
417
384
  }
418
385
  async syncState() {
@@ -422,110 +389,48 @@ export class RemoteBrowserService extends EventEmitter {
422
389
  this.log("✅ State sync complete", {
423
390
  pagesCount: state.pages.length,
424
391
  selectedPageId: state.selectedPageId,
425
- pages: state.pages.map((p) => ({ id: p.id, title: p.title, url: p.url })),
426
392
  });
427
393
  this.emit("BrowserService.stateSync", state);
428
394
  }
429
395
  catch (error) {
430
- this.log("❌ Error syncing state", {
431
- error: error instanceof Error ? error.message : String(error),
432
- stack: error instanceof Error ? error.stack : undefined,
433
- });
396
+ this.log("❌ Error syncing state", { error });
434
397
  }
435
398
  }
436
399
  async getState() {
437
- this.log("📊 Getting current state", {
438
- internalPagesCount: this.pages.size,
439
- internalPageIds: Array.from(this.pages.keys()),
440
- selectedPageId: this._selectedPageId,
441
- });
400
+ this.log("📊 Getting current state");
442
401
  const debugData = await this.getDebugURLs();
443
- this.log("📊 Debug data for state", {
444
- debugPagesCount: debugData.length,
445
- debugPages: debugData.map((p) => ({ id: p.id, type: p.type, url: p.url })),
446
- });
447
402
  const pagesData = [];
448
- const updatedPages = new Map();
449
- const matchedDebugIds = new Set(); // Track which CDP IDs we've already matched
450
- let updatedSelectedPageId = this._selectedPageId;
451
- for (const [oldId, page] of this.pages.entries()) {
452
- this.log("🔍 Processing page from internal map", {
453
- oldId,
454
- url: page.url(),
455
- });
456
- // Try to find by old ID first (most common case - ID hasn't changed)
457
- let debugInfo = debugData.find((d) => d.id === oldId && !matchedDebugIds.has(d.id));
458
- // Fallback: Try to find by URL (page ID may have changed)
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
459
409
  if (!debugInfo) {
460
- this.log("⚠️ Page ID not found in CDP, attempting to match by URL", {
461
- oldId,
462
- pageUrl: page.url(),
463
- });
464
- debugInfo = debugData.find((d) => d.type === "page" && d.url === page.url() && !matchedDebugIds.has(d.id));
465
- if (debugInfo) {
466
- this.log("✅ Found page by URL match, updating ID", {
467
- oldId,
468
- newId: debugInfo.id,
469
- url: page.url(),
470
- });
471
- // Update selected page ID if this was the selected page and ID changed
472
- if (oldId === this._selectedPageId) {
473
- updatedSelectedPageId = debugInfo.id;
474
- this.log("🔄 Updated selected page ID", {
475
- oldId,
476
- newId: debugInfo.id,
477
- });
478
- }
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 });
479
416
  }
480
417
  }
481
- else {
482
- this.log("✅ Found matching debug info by ID", {
483
- id: debugInfo.id,
484
- debugUrl: debugInfo.url,
485
- 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
486
424
  });
487
425
  }
488
- if (debugInfo) {
489
- // Mark this CDP ID as matched to avoid duplicate matches
490
- matchedDebugIds.add(debugInfo.id);
491
- try {
492
- const pageData = {
493
- id: debugInfo.id,
494
- title: await page.title(),
495
- url: page.url(),
496
- wsDebuggerUrl: debugInfo.webSocketDebuggerUrl || "",
497
- };
498
- pagesData.push(pageData);
499
- updatedPages.set(debugInfo.id, page);
500
- this.log("✅ Page added to state", pageData);
501
- }
502
- catch (error) {
503
- this.log("❌ Error getting page data", {
504
- id: oldId,
505
- error: error instanceof Error ? error.message : String(error),
506
- });
507
- }
508
- }
509
- else {
510
- this.log("⚠️ No matching debug info found by ID or URL", {
511
- oldId,
512
- pageUrl: page.url(),
513
- availableDebugIds: debugData.map((d) => d.id),
514
- availableDebugUrls: debugData.map((d) => d.url),
515
- alreadyMatched: Array.from(matchedDebugIds),
516
- });
426
+ catch (error) {
427
+ this.log("❌ Error getting page data (page might be closed)", { stableTabId });
517
428
  }
518
429
  }
519
- // Update the internal pages map with current CDP IDs
520
- if (updatedPages.size !== this.pages.size || updatedSelectedPageId !== this._selectedPageId) {
521
- this.log("🔄 Updating internal state with new CDP IDs", {
522
- oldPagesCount: this.pages.size,
523
- newPagesCount: updatedPages.size,
524
- oldSelectedId: this._selectedPageId,
525
- newSelectedId: updatedSelectedPageId,
526
- });
527
- this.pages = updatedPages;
528
- 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 });
529
434
  }
530
435
  const state = {
531
436
  pages: pagesData,
@@ -537,127 +442,502 @@ export class RemoteBrowserService extends EventEmitter {
537
442
  async createTab(url = "about:blank") {
538
443
  try {
539
444
  this.log("🆕 Creating new tab", { url });
540
- const page = await this.context.newPage();
541
- this.log("✅ New page created by Playwright", { url: page.url() });
542
- if (typeof url === "string" && url !== "about:blank") {
543
- this.log("🌐 Navigating to URL", { url });
445
+ const page = await this.context.newPage(); // This will trigger the 'page' event
446
+ if (url !== "about:blank") {
544
447
  await page.goto(url, { waitUntil: "domcontentloaded" });
545
- this.log("✅ Navigation complete", { finalUrl: page.url() });
546
448
  }
547
- // Wait longer for CDP to register and stabilize the page
548
- this.log("⏳ Waiting for CDP registration and stabilization...");
549
- await new Promise((resolve) => setTimeout(resolve, 1000)); // Increased to 1 second
550
- // Try multiple times to get a stable page ID
551
- let id = null;
552
- let attempts = 0;
553
- const maxAttempts = 5;
554
- while (!id && attempts < maxAttempts) {
555
- attempts++;
556
- this.log(`🔍 Attempt ${attempts}/${maxAttempts} to get page ID`);
557
- id = await this.getPageId(page);
558
- if (!id) {
559
- this.log(`⏳ Page ID not found, waiting 500ms before retry...`);
560
- await new Promise((resolve) => setTimeout(resolve, 500));
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;
561
456
  }
562
457
  }
563
- this.log("🔍 Retrieved page ID after retries", {
564
- id,
565
- url: page.url(),
566
- attempts,
567
- });
568
- if (id) {
569
- this.pages.set(id, page);
570
- this._selectedPageId = id;
571
- this.log("✅ Tab created successfully", {
572
- id,
573
- url: page.url(),
574
- totalPages: this.pages.size,
575
- allPageIds: Array.from(this.pages.keys()),
576
- selectedPageId: this._selectedPageId,
577
- });
578
- }
579
- else {
580
- this.log("❌ Failed to get page ID for new tab after all retries", {
581
- url: page.url(),
582
- attempts,
583
- });
584
- // Try to get debug data to see what's available
585
- const debugData = await this.getDebugURLs();
586
- this.log("🔍 Current CDP state after failed ID retrieval", {
587
- debugPages: debugData.map((p) => ({ id: p.id, url: p.url, type: p.type })),
588
- });
589
- }
590
458
  await this.syncState();
591
459
  }
592
460
  catch (error) {
593
- this.log("❌ Error creating tab", {
594
- url,
595
- error: error instanceof Error ? error.message : String(error),
596
- stack: error instanceof Error ? error.stack : undefined,
597
- });
598
- throw error;
461
+ this.log("❌ Error creating tab", { error });
599
462
  }
600
463
  }
601
- async closeTab(pageId) {
464
+ async closeTab(stableTabId) {
602
465
  try {
603
- this.log("🗑️ Closing tab", { pageId });
604
- const page = this.pages.get(pageId);
605
- if (page) {
606
- this.log("✅ Found page to close", { pageId, url: page.url() });
607
- await page.close();
608
- 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
609
470
  }
610
471
  else {
611
- this.log("⚠️ Page not found", {
612
- pageId,
613
- availablePageIds: Array.from(this.pages.keys()),
614
- });
472
+ this.log("⚠️ Page not found for closing", { stableTabId });
615
473
  }
474
+ // 'close' event handler will manage state update
616
475
  }
617
476
  catch (error) {
618
- this.log("❌ Error closing tab", {
619
- pageId,
620
- error: error instanceof Error ? error.message : String(error),
621
- });
622
- throw error;
477
+ this.log("❌ Error closing tab", { error });
623
478
  }
624
479
  }
625
- async selectTab(pageId) {
480
+ async selectTab(stableTabId) {
626
481
  try {
627
- this.log("🎯 Selecting tab", { pageId });
628
- const page = this.pages.get(pageId);
629
- if (page) {
630
- this._selectedPageId = pageId;
631
- await page.bringToFront();
632
- this.log("✅ Tab selected successfully", {
633
- pageId,
634
- url: page.url(),
635
- selectedPageId: this._selectedPageId,
636
- });
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 });
637
488
  await this.syncState();
638
489
  }
639
490
  else {
640
- this.log("⚠️ Page not found for selection", {
641
- pageId,
642
- availablePageIds: Array.from(this.pages.keys()),
643
- });
491
+ this.log("⚠️ Page not found for selection", { stableTabId });
644
492
  }
645
493
  }
646
494
  catch (error) {
647
- this.log("❌ Error selecting tab", {
648
- pageId,
649
- error: error instanceof Error ? error.message : String(error),
650
- });
651
- throw error;
495
+ this.log("❌ Error selecting tab", { error });
652
496
  }
653
497
  }
498
+ // ... (getSelectedPage remains the same, but operates on new state)
654
499
  getSelectedPage() {
655
- const page = this._selectedPageId ? this.pages.get(this._selectedPageId) || null : null;
500
+ const pageInfo = this._selectedPageId ? this.pages.get(this._selectedPageId) : null;
656
501
  this.log("🔍 Getting selected page", {
657
502
  selectedPageId: this._selectedPageId,
658
- found: !!page,
659
- url: page?.url(),
503
+ found: !!pageInfo,
504
+ url: pageInfo?.page.url(),
660
505
  });
661
- return page;
506
+ return pageInfo?.page || null;
662
507
  }
663
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.1588-dev",
3
+ "version": "1.0.1589-dev",
4
4
  "description": " ",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",