@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.
- package/README.md +30 -7
- package/dist/bin/roxybrowser-mcp.js +3 -0
- package/dist/bin/roxybrowser-mcp.js.map +1 -1
- package/dist/browser.d.ts +33 -5
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +134 -15
- package/dist/browser.js.map +1 -1
- package/dist/browserType.d.ts +23 -1
- package/dist/browserType.d.ts.map +1 -1
- package/dist/browserType.js +33 -5
- package/dist/browserType.js.map +1 -1
- package/dist/mcp/backend/connect.d.ts +1 -0
- package/dist/mcp/backend/connect.d.ts.map +1 -1
- package/dist/mcp/backend/connect.js +4 -2
- package/dist/mcp/backend/connect.js.map +1 -1
- package/dist/mcp/backend/context.d.ts +3 -0
- package/dist/mcp/backend/context.d.ts.map +1 -1
- package/dist/mcp/backend/context.js +9 -1
- package/dist/mcp/backend/context.js.map +1 -1
- package/dist/mcp/backend/network.d.ts.map +1 -1
- package/dist/mcp/backend/network.js +30 -4
- package/dist/mcp/backend/network.js.map +1 -1
- package/dist/mcp/backend/response.d.ts +2 -0
- package/dist/mcp/backend/response.d.ts.map +1 -1
- package/dist/mcp/backend/response.js +53 -7
- package/dist/mcp/backend/response.js.map +1 -1
- package/dist/mcp/connectedBrowser.d.ts.map +1 -1
- package/dist/mcp/connectedBrowser.js +447 -39
- package/dist/mcp/connectedBrowser.js.map +1 -1
- package/dist/mcp/output.d.ts +5 -0
- package/dist/mcp/output.d.ts.map +1 -1
- package/dist/mcp/output.js +17 -1
- package/dist/mcp/output.js.map +1 -1
- package/dist/mcp/runtime.d.ts +7 -0
- package/dist/mcp/runtime.d.ts.map +1 -1
- package/dist/mcp/runtime.js +76 -11
- package/dist/mcp/runtime.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +3 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/transports/inMemory.d.ts.map +1 -1
- package/dist/mcp/transports/inMemory.js +1 -0
- package/dist/mcp/transports/inMemory.js.map +1 -1
- package/dist/mcp/types.d.ts +7 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/page.d.ts +2 -0
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +12 -0
- package/dist/page.js.map +1 -1
- package/dist/protocol/adapter.d.ts +1 -0
- package/dist/protocol/adapter.d.ts.map +1 -1
- package/dist/protocol/bidi/backend.d.ts +1 -1
- package/dist/protocol/bidi/backend.d.ts.map +1 -1
- package/dist/protocol/bidi/backend.js +6 -2
- package/dist/protocol/bidi/backend.js.map +1 -1
- package/dist/protocol/cdp/backend.d.ts.map +1 -1
- package/dist/protocol/cdp/backend.js +122 -8
- package/dist/protocol/cdp/backend.js.map +1 -1
- package/dist/roxybrowser.bundle.js +560 -68
- package/dist/roxybrowser.bundle.js.map +1 -1
- package/dist/types/api.d.ts +23 -4
- package/dist/types/api.d.ts.map +1 -1
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
878
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|