@roxybrowser/playwright 2.0.2-beta.1 → 2.0.2-beta.10

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 (63) hide show
  1. package/README.md +30 -7
  2. package/dist/bin/roxybrowser-mcp.js +3 -0
  3. package/dist/bin/roxybrowser-mcp.js.map +1 -1
  4. package/dist/browser.d.ts +33 -5
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +134 -15
  7. package/dist/browser.js.map +1 -1
  8. package/dist/browserType.d.ts +23 -1
  9. package/dist/browserType.d.ts.map +1 -1
  10. package/dist/browserType.js +33 -5
  11. package/dist/browserType.js.map +1 -1
  12. package/dist/mcp/backend/connect.d.ts +1 -0
  13. package/dist/mcp/backend/connect.d.ts.map +1 -1
  14. package/dist/mcp/backend/connect.js +4 -2
  15. package/dist/mcp/backend/connect.js.map +1 -1
  16. package/dist/mcp/backend/context.d.ts +3 -0
  17. package/dist/mcp/backend/context.d.ts.map +1 -1
  18. package/dist/mcp/backend/context.js +9 -1
  19. package/dist/mcp/backend/context.js.map +1 -1
  20. package/dist/mcp/backend/network.d.ts.map +1 -1
  21. package/dist/mcp/backend/network.js +30 -4
  22. package/dist/mcp/backend/network.js.map +1 -1
  23. package/dist/mcp/backend/response.d.ts +2 -0
  24. package/dist/mcp/backend/response.d.ts.map +1 -1
  25. package/dist/mcp/backend/response.js +53 -7
  26. package/dist/mcp/backend/response.js.map +1 -1
  27. package/dist/mcp/connectedBrowser.d.ts.map +1 -1
  28. package/dist/mcp/connectedBrowser.js +447 -39
  29. package/dist/mcp/connectedBrowser.js.map +1 -1
  30. package/dist/mcp/output.d.ts +5 -0
  31. package/dist/mcp/output.d.ts.map +1 -1
  32. package/dist/mcp/output.js +17 -1
  33. package/dist/mcp/output.js.map +1 -1
  34. package/dist/mcp/runtime.d.ts +7 -0
  35. package/dist/mcp/runtime.d.ts.map +1 -1
  36. package/dist/mcp/runtime.js +76 -11
  37. package/dist/mcp/runtime.js.map +1 -1
  38. package/dist/mcp/server.d.ts.map +1 -1
  39. package/dist/mcp/server.js +3 -1
  40. package/dist/mcp/server.js.map +1 -1
  41. package/dist/mcp/transports/inMemory.d.ts.map +1 -1
  42. package/dist/mcp/transports/inMemory.js +1 -0
  43. package/dist/mcp/transports/inMemory.js.map +1 -1
  44. package/dist/mcp/types.d.ts +7 -0
  45. package/dist/mcp/types.d.ts.map +1 -1
  46. package/dist/page.d.ts +2 -0
  47. package/dist/page.d.ts.map +1 -1
  48. package/dist/page.js +12 -0
  49. package/dist/page.js.map +1 -1
  50. package/dist/protocol/adapter.d.ts +1 -0
  51. package/dist/protocol/adapter.d.ts.map +1 -1
  52. package/dist/protocol/bidi/backend.d.ts +1 -1
  53. package/dist/protocol/bidi/backend.d.ts.map +1 -1
  54. package/dist/protocol/bidi/backend.js +6 -2
  55. package/dist/protocol/bidi/backend.js.map +1 -1
  56. package/dist/protocol/cdp/backend.d.ts.map +1 -1
  57. package/dist/protocol/cdp/backend.js +122 -8
  58. package/dist/protocol/cdp/backend.js.map +1 -1
  59. package/dist/roxybrowser.bundle.js +560 -68
  60. package/dist/roxybrowser.bundle.js.map +1 -1
  61. package/dist/types/api.d.ts +23 -4
  62. package/dist/types/api.d.ts.map +1 -1
  63. package/package.json +2 -1
@@ -544,7 +544,8 @@ export class CdpBrowserAdapter {
544
544
  this.state = {
545
545
  browserClient,
546
546
  version,
547
- connection
547
+ connection,
548
+ isolatedBrowserContextIds: new Set()
548
549
  };
549
550
  }
550
551
  async browser() {
@@ -576,7 +577,13 @@ class CdpBrowserSession {
576
577
  return this.state.version.Browser;
577
578
  }
578
579
  async newContext(options = {}) {
580
+ if (options.reuseDefaultUserContext) {
581
+ return new CdpBrowserContextAdapter(this.state, undefined, options);
582
+ }
579
583
  const response = await this.state.browserClient.Target.createBrowserContext({});
584
+ // Track the new isolated context so the default context adapter (reuseDefaultUserContext)
585
+ // can distinguish default-context targets from explicitly isolated ones.
586
+ this.state.isolatedBrowserContextIds.add(response.browserContextId);
580
587
  return new CdpBrowserContextAdapter(this.state, response.browserContextId, options);
581
588
  }
582
589
  async close() { }
@@ -610,6 +617,12 @@ class CdpBrowserContextAdapter {
610
617
  this.options = options;
611
618
  this.targetDiscoveryReady = this.initializeTargetDiscovery();
612
619
  }
620
+ // Resolves once target discovery and the initial page-population batch have
621
+ // completed. RoxyBrowser.newContext() awaits this so context.pages() is
622
+ // non-empty by the time the caller receives the BrowserContext object.
623
+ async ready() {
624
+ await this.targetDiscoveryReady;
625
+ }
613
626
  async newPage() {
614
627
  await this.targetDiscoveryReady;
615
628
  this.pendingManualPageCreations += 1;
@@ -784,6 +797,9 @@ class CdpBrowserContextAdapter {
784
797
  throw error;
785
798
  }
786
799
  }
800
+ // Remove from the shared set so the default context adapter stops
801
+ // excluding targets that belonged to this now-disposed context.
802
+ this.state.isolatedBrowserContextIds.delete(this.browserContextId);
787
803
  }
788
804
  await Promise.allSettled(pendingPages);
789
805
  this.pendingPages.clear();
@@ -804,19 +820,44 @@ class CdpBrowserContextAdapter {
804
820
  this.state.browserClient.Target.targetDestroyed?.((event) => {
805
821
  void this.handleTargetDetached(event.targetId);
806
822
  });
823
+ // Bug fix: when connecting to an existing browser's default user context
824
+ // (reuseDefaultUserContext: true), use waitForDebuggerOnStart: false.
825
+ //
826
+ // With waitForDebuggerOnStart: true, Chrome pauses EVERY new renderer process
827
+ // — including the ones created by cross-origin navigations — and waits for
828
+ // Runtime.runIfWaitingForDebugger before allowing them to execute.
829
+ // resumeOnInitialized only unblocks the initial attachment; subsequent
830
+ // navigations to cross-origin pages spin up fresh renderer processes that
831
+ // never get resumed, so page.goto() hangs indefinitely. The navigation
832
+ // appears to succeed only after our process exits and Chrome auto-detaches.
833
+ //
834
+ // For newly isolated contexts (normal newContext()), we keep true so that
835
+ // init scripts are injected before any page JavaScript executes.
807
836
  await this.state.browserClient.Target.setAutoAttach?.({
808
837
  autoAttach: true,
809
- waitForDebuggerOnStart: true,
838
+ waitForDebuggerOnStart: !this.options.reuseDefaultUserContext,
810
839
  flatten: true
811
840
  }).catch(() => { });
841
+ // Await pages that were attached synchronously via setAutoAttach events.
842
+ // In practice Chrome does NOT fire attachedToTarget for pre-existing page
843
+ // targets from the browser session, so this batch is usually empty — but
844
+ // it's still needed for targets attached via other mechanisms (workers, etc.).
845
+ const initialPagePromises = Array.from(this.pendingPages.values());
846
+ await Promise.allSettled(initialPagePromises);
812
847
  await this.state.browserClient.Target.getTargetInfo?.().catch(() => { });
813
848
  await this.state.browserClient.Target.setDiscoverTargets?.({
814
849
  discover: true
815
850
  });
851
+ // Explicitly discover and attach to any pre-existing page targets that
852
+ // setAutoAttach did not fire attachedToTarget for (Chrome's browser-level
853
+ // setAutoAttach only triggers attachedToTarget for new targets, not for
854
+ // tabs that were already open before our session connected).
855
+ // This is the primary mechanism that populates context.pages() after
856
+ // connectOverCDP — without it, pages() is always empty on connect.
857
+ await this.discoverTargets();
816
858
  this.targetPollTimer = setInterval(() => {
817
859
  void this.discoverTargets().catch(() => { });
818
860
  }, 100);
819
- await this.discoverTargets();
820
861
  }
821
862
  async handleTargetAttached(event) {
822
863
  const { targetInfo } = event;
@@ -854,7 +895,9 @@ class CdpBrowserContextAdapter {
854
895
  fallbackUrl: targetInfo.url ?? "about:blank",
855
896
  hasWindowOpener: targetInfo.canAccessOpener ?? true,
856
897
  openerTargetId: targetInfo.openerId ?? null,
857
- emitPage: !this.manuallyCreatedTargetIds.has(targetInfo.targetId),
898
+ // Suppress auto-emit when called from discoverTargets() — it will emit
899
+ // pages itself in getTargets() order once all initializations complete.
900
+ emitPage: event._suppressEmit ? false : !this.manuallyCreatedTargetIds.has(targetInfo.targetId),
858
901
  sessionId: event.sessionId
859
902
  });
860
903
  this.pageSessionIds.set(targetInfo.targetId, event.sessionId);
@@ -874,14 +917,70 @@ class CdpBrowserContextAdapter {
874
917
  return;
875
918
  }
876
919
  const result = await this.state.browserClient.Target.getTargets();
877
- for (const targetInfo of result.targetInfos) {
878
- await this.handleTargetCreated(targetInfo);
920
+ // Filter to page targets we own that haven't been attached yet.
921
+ // Chrome's browser-level setAutoAttach does not fire attachedToTarget for
922
+ // tabs that were already open before our CDP session connected, so we must
923
+ // discover and attach to them explicitly here.
924
+ const unattached = result.targetInfos.filter(targetInfo => this.matchesTargetInfo(targetInfo) &&
925
+ !this.pages.has(targetInfo.targetId) &&
926
+ !this.pendingPages.has(targetInfo.targetId));
927
+ if (!unattached.length) {
928
+ return;
929
+ }
930
+ // Attach to all unattached targets concurrently — mirrors Playwright's approach
931
+ // of starting all attachments in parallel for faster connection on multi-tab browsers.
932
+ const sessions = (await Promise.all(unattached.map(async (targetInfo) => {
933
+ try {
934
+ const attachResult = await this.state.browserClient.Target.attachToTarget({ targetId: targetInfo.targetId, flatten: true });
935
+ return { targetInfo, sessionId: attachResult.sessionId };
936
+ }
937
+ catch {
938
+ return null;
939
+ }
940
+ }))).filter((s) => s !== null);
941
+ // Kick off page initialization for all targets concurrently, but suppress the
942
+ // automatic emitPage call inside getOrCreatePage. We will emit pages below in
943
+ // getTargets() order — which Chrome guarantees is tab-creation order — so that
944
+ // context.pages() has a stable, predictable ordering regardless of which tab's
945
+ // CDP initialization happens to complete first.
946
+ //
947
+ // handleTargetAttached runs synchronously until its first internal await, so
948
+ // pendingPages entries are populated for all targets before this loop ends.
949
+ for (const { targetInfo, sessionId } of sessions) {
950
+ void this.handleTargetAttached({
951
+ sessionId,
952
+ targetInfo,
953
+ waitingForDebugger: false,
954
+ _suppressEmit: true
955
+ });
956
+ }
957
+ // Capture all pending page promises now (they were set synchronously above).
958
+ const pagePromises = sessions
959
+ .map(({ targetInfo }) => this.pendingPages.get(targetInfo.targetId))
960
+ .filter((p) => p !== undefined);
961
+ // Wait for all initializations concurrently — mirrors Playwright's
962
+ // Promise.all(crPages.map(crPage => crPage._page.waitForInitializedOrError()))
963
+ await Promise.allSettled(pagePromises);
964
+ // Emit pages in the order getTargets() returned them (Chrome creation order).
965
+ // This gives context.pages() a stable ordering that matches the visual tab bar.
966
+ for (const { targetInfo } of sessions) {
967
+ const page = this.pages.get(targetInfo.targetId);
968
+ if (!page) {
969
+ continue;
970
+ }
971
+ const opener = targetInfo.openerId
972
+ ? await this.resolveKnownPage(targetInfo.openerId)
973
+ : null;
974
+ await this.emitPage(page, opener, targetInfo.canAccessOpener ?? true);
879
975
  }
880
976
  }
881
977
  async handleTargetCreated(targetInfo) {
882
978
  if (this.closing || !this.matchesTargetInfo(targetInfo)) {
883
979
  return;
884
980
  }
981
+ // New targets created after our session connected are handled by the
982
+ // attachedToTarget events fired by setAutoAttach. Pre-existing targets are
983
+ // handled by discoverTargets() at initialization time and on each poll tick.
885
984
  }
886
985
  async handleTargetDetached(targetId, sessionId) {
887
986
  const attachedSessionId = this.pageSessionIds.get(targetId);
@@ -914,13 +1013,28 @@ class CdpBrowserContextAdapter {
914
1013
  if (targetInfo.openerId && this.pendingPages.has(targetInfo.openerId)) {
915
1014
  return true;
916
1015
  }
917
- return !this.browserContextId && !targetInfo.browserContextId;
1016
+ // When we represent the default browser context (no browserContextId), match
1017
+ // all page targets that do NOT belong to a known isolated context.
1018
+ //
1019
+ // Chrome 119+ assigns a non-empty UUID to ALL targets — including those in the
1020
+ // default context — so the old check `!targetInfo.browserContextId` incorrectly
1021
+ // excluded every tab that had been given a default-context UUID, making
1022
+ // context.pages() always empty after connectOverCDP.
1023
+ //
1024
+ // Isolated contexts are tracked in state.isolatedBrowserContextIds (populated
1025
+ // in CdpBrowserSession.newContext when Target.createBrowserContext is called).
1026
+ // Any target NOT in that set belongs to the default context.
1027
+ return !this.browserContextId &&
1028
+ !this.state.isolatedBrowserContextIds.has(targetInfo.browserContextId ?? "");
918
1029
  }
919
1030
  matchesBrowserContextTarget(targetInfo) {
920
1031
  if (targetInfo.browserContextId === this.browserContextId) {
921
1032
  return true;
922
1033
  }
923
- return !this.browserContextId && !targetInfo.browserContextId;
1034
+ // Same reasoning as matchesTargetInfo: default context adapter accepts all
1035
+ // non-isolated targets regardless of whether browserContextId is a UUID.
1036
+ return !this.browserContextId &&
1037
+ !this.state.isolatedBrowserContextIds.has(targetInfo.browserContextId ?? "");
924
1038
  }
925
1039
  async getOrCreatePage(targetId, options = {}) {
926
1040
  const existing = this.pages.get(targetId);