Skip to content

Commit 4b873ec

Browse files
authored
feat: Add support for Chrome DevTools Protocol (CDPSession) (#1329)
Add new methods BrowserContext.newCDPSession and Browser.newBrowserCDPSession to create a Chrome DevTools Protocol[1] session for the page and browser respectively. Fixes #823 [1] https://chromedevtools.github.io/devtools-protocol/
1 parent 463146a commit 4b873ec

File tree

10 files changed

+402
-1
lines changed

10 files changed

+402
-1
lines changed

playwright/src/main/java/com/microsoft/playwright/Browser.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,14 @@ public StartTracingOptions setScreenshots(boolean screenshots) {
11861186
* @since v1.8
11871187
*/
11881188
boolean isConnected();
1189+
/**
1190+
* <strong>NOTE:</strong> CDP Sessions are only supported on Chromium-based browsers.
1191+
*
1192+
* <p> Returns the newly created browser session.
1193+
*
1194+
* @since v1.11
1195+
*/
1196+
CDPSession newBrowserCDPSession();
11891197
/**
11901198
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
11911199
*

playwright/src/main/java/com/microsoft/playwright/BrowserContext.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,26 @@ default void grantPermissions(List<String> permissions) {
752752
* @since v1.8
753753
*/
754754
void grantPermissions(List<String> permissions, GrantPermissionsOptions options);
755+
/**
756+
* <strong>NOTE:</strong> CDP sessions are only supported on Chromium-based browsers.
757+
*
758+
* <p> Returns the newly created session.
759+
*
760+
* @param page Target to create new session for. For backwards-compatibility, this parameter is named {@code page}, but it can be a
761+
* {@code Page} or {@code Frame} type.
762+
* @since v1.11
763+
*/
764+
CDPSession newCDPSession(Page page);
765+
/**
766+
* <strong>NOTE:</strong> CDP sessions are only supported on Chromium-based browsers.
767+
*
768+
* <p> Returns the newly created session.
769+
*
770+
* @param page Target to create new session for. For backwards-compatibility, this parameter is named {@code page}, but it can be a
771+
* {@code Page} or {@code Frame} type.
772+
* @since v1.11
773+
*/
774+
CDPSession newCDPSession(Frame page);
755775
/**
756776
* Creates a new page in the browser context.
757777
*
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.playwright;
18+
19+
import java.util.function.Consumer;
20+
import com.google.gson.JsonObject;
21+
22+
/**
23+
* The {@code CDPSession} instances are used to talk raw Chrome Devtools Protocol:
24+
* <ul>
25+
* <li> protocol methods can be called with {@code session.send} method.</li>
26+
* <li> protocol events can be subscribed to with {@code session.on} method.</li>
27+
* </ul>
28+
*
29+
* <p> Useful links:
30+
* <ul>
31+
* <li> Documentation on DevTools Protocol can be found here: <a
32+
* href="https://chromedevtools.github.io/devtools-protocol/">DevTools Protocol Viewer</a>.</li>
33+
* <li> Getting Started with DevTools Protocol: https://github.com/aslushnikov/getting-started-with-cdp/blob/master/README.md</li>
34+
* <pre>{@code
35+
* CDPSession client = page.context().newCDPSession(page);
36+
* client.send("Runtime.enable");
37+
*
38+
* client.on("Animation.animationCreated", (event) -> System.out.println("Animation created!"));
39+
*
40+
* JsonObject response = client.send("Animation.getPlaybackRate");
41+
* double playbackRate = response.get("playbackRate").getAsDouble();
42+
* System.out.println("playback rate is " + playbackRate);
43+
*
44+
* JsonObject params = new JsonObject();
45+
* params.addProperty("playbackRate", playbackRate / 2);
46+
* client.send("Animation.setPlaybackRate", params);
47+
* }</pre>
48+
* </ul>
49+
*/
50+
public interface CDPSession {
51+
/**
52+
* Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used to
53+
* send messages.
54+
*
55+
* @since v1.8
56+
*/
57+
void detach();
58+
/**
59+
*
60+
*
61+
* @param method Protocol method name.
62+
* @since v1.8
63+
*/
64+
default JsonObject send(String method) {
65+
return send(method, null);
66+
}
67+
/**
68+
*
69+
*
70+
* @param method Protocol method name.
71+
* @param args Optional method parameters.
72+
* @since v1.8
73+
*/
74+
JsonObject send(String method, JsonObject args);
75+
/**
76+
* Register an event handler for events with the specified event name. The given handler will be called for every event
77+
* with the given name.
78+
*
79+
* @param eventName CDP event name.
80+
* @param handler Event handler.
81+
* @since v1.37
82+
*/
83+
void on(String eventName, Consumer<JsonObject> handler);
84+
/**
85+
* Unregister an event handler for events with the specified event name. The given handler will not be called anymore for
86+
* events with the given name.
87+
*
88+
* @param eventName CDP event name.
89+
* @param handler Event handler.
90+
* @since v1.37
91+
*/
92+
void off(String eventName, Consumer<JsonObject> handler);
93+
}
94+

playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,22 @@ private Page waitForPageImpl(WaitForPageOptions options, Runnable code) {
214214
return waitForEventWithTimeout(EventType.PAGE, code, options.predicate, options.timeout);
215215
}
216216

217+
@Override
218+
public CDPSession newCDPSession(Page page) {
219+
JsonObject params = new JsonObject();
220+
params.add("page", ((PageImpl) page).toProtocolRef());
221+
JsonObject result = sendMessage("newCDPSession", params).getAsJsonObject();
222+
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
223+
}
224+
225+
@Override
226+
public CDPSession newCDPSession(Frame frame) {
227+
JsonObject params = new JsonObject();
228+
params.add("frame", ((FrameImpl) frame).toProtocolRef());
229+
JsonObject result = sendMessage("newCDPSession", params).getAsJsonObject();
230+
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
231+
}
232+
217233
@Override
218234
public void close() {
219235
withLogging("BrowserContext.close", () -> closeImpl());

playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,13 @@ void handleEvent(String event, JsonObject parameters) {
276276
}
277277
}
278278

279+
@Override
280+
public CDPSession newBrowserCDPSession() {
281+
JsonObject params = new JsonObject();
282+
JsonObject result = sendMessage("newBrowserCDPSession", params).getAsJsonObject();
283+
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
284+
}
285+
279286
private void didClose() {
280287
isConnected = false;
281288
listeners.notify(EventType.DISCONNECTED, this);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.microsoft.playwright.impl;
2+
3+
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonObject;
5+
import com.microsoft.playwright.CDPSession;
6+
7+
import java.util.HashMap;
8+
import java.util.function.Consumer;
9+
10+
public class CDPSessionImpl extends ChannelOwner implements CDPSession {
11+
private final ListenerCollection<String> listeners = new ListenerCollection<>(new HashMap<>(), this);
12+
13+
protected CDPSessionImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
14+
super(parent, type, guid, initializer);
15+
}
16+
17+
@Override
18+
void handleEvent(String event, JsonObject parameters) {
19+
super.handleEvent(event, parameters);
20+
if ("event".equals(event)) {
21+
String method = parameters.get("method").getAsString();
22+
JsonObject params = parameters.get("params").getAsJsonObject();
23+
listeners.notify(method, params);
24+
}
25+
}
26+
27+
public JsonObject send(String method) {
28+
return send(method, null);
29+
}
30+
31+
public JsonObject send(String method, JsonObject params) {
32+
JsonObject args = new JsonObject();
33+
if (params != null) {
34+
args.add("params", params);
35+
}
36+
args.addProperty("method", method);
37+
JsonElement response = connection.sendMessage(guid, "send", args);
38+
if (response == null) return null;
39+
else return response.getAsJsonObject().get("result").getAsJsonObject();
40+
}
41+
42+
@Override
43+
public void on(String event, Consumer<JsonObject> handler) {
44+
listeners.add(event, handler);
45+
}
46+
47+
@Override
48+
public void off(String event, Consumer<JsonObject> handler) {
49+
listeners.remove(event, handler);
50+
}
51+
52+
@Override
53+
public void detach() {
54+
sendMessage("detach");
55+
}
56+
}

playwright/src/main/java/com/microsoft/playwright/impl/Connection.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ private ChannelOwner createRemoteObject(String parentGuid, JsonObject params) {
359359
case "WritableStream":
360360
result = new WritableStream(parent, type, guid, initializer);
361361
break;
362+
case "CDPSession":
363+
result = new CDPSessionImpl(parent, type, guid, initializer);
364+
break;
362365
default:
363366
throw new PlaywrightException("Unknown type " + type);
364367
}

playwright/src/test/java/com/microsoft/playwright/TestBrowser.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616

1717
package com.microsoft.playwright;
1818

19+
import com.google.gson.JsonElement;
20+
import com.google.gson.JsonObject;
1921
import com.microsoft.playwright.options.BrowserChannel;
2022
import org.junit.jupiter.api.Assumptions;
2123
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.condition.EnabledIf;
2225

26+
import java.util.concurrent.atomic.AtomicReference;
2327
import java.util.regex.Pattern;
2428

2529
import static org.junit.jupiter.api.Assertions.*;
@@ -98,4 +102,29 @@ void shouldSupportDeprecatedChannelEnum() {
98102
void shouldReturnBrowserType() {
99103
assertEquals(browserType, browser.browserType());
100104
}
105+
106+
@Test
107+
@EnabledIf(value = "com.microsoft.playwright.TestBase#isChromium", disabledReason = "Chrome Devtools Protocol supported by chromium only")
108+
void shouldWorkWithNewBrowserCDPSession() {
109+
CDPSession session = browser.newBrowserCDPSession();
110+
111+
JsonElement response = session.send("Browser.getVersion");
112+
assertNotNull(response.getAsJsonObject().get("userAgent").toString());
113+
114+
AtomicReference<Boolean> gotEvent = new AtomicReference<>(false);
115+
116+
session.on("Target.targetCreated", jsonElement -> {
117+
gotEvent.set(true);
118+
});
119+
120+
JsonObject params = new JsonObject();
121+
params.addProperty("discover", true);
122+
session.send("Target.setDiscoverTargets", params);
123+
124+
Page page = browser.newPage();
125+
assertTrue(gotEvent.get());
126+
page.close();
127+
128+
session.detach();
129+
}
101130
}

0 commit comments

Comments
 (0)