diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java
index b418332e..b4a8a84b 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Browser.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java
@@ -1186,6 +1186,14 @@ public interface Browser extends AutoCloseable {
* @since v1.8
*/
boolean isConnected();
+ /**
+ * NOTE: CDP Sessions are only supported on Chromium-based browsers.
+ *
+ *
Returns the newly created browser session.
+ *
+ * @since v1.11
+ */
+ CDPSession newBrowserCDPSession();
/**
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
*
diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java
index 51886ae4..74378506 100644
--- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java
+++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java
@@ -752,6 +752,26 @@ public interface BrowserContext extends AutoCloseable {
* @since v1.8
*/
void grantPermissions(List permissions, GrantPermissionsOptions options);
+ /**
+ * NOTE: CDP sessions are only supported on Chromium-based browsers.
+ *
+ * Returns the newly created session.
+ *
+ * @param page Target to create new session for. For backwards-compatibility, this parameter is named {@code page}, but it can be a
+ * {@code Page} or {@code Frame} type.
+ * @since v1.11
+ */
+ CDPSession newCDPSession(Page page);
+ /**
+ * NOTE: CDP sessions are only supported on Chromium-based browsers.
+ *
+ *
Returns the newly created session.
+ *
+ * @param page Target to create new session for. For backwards-compatibility, this parameter is named {@code page}, but it can be a
+ * {@code Page} or {@code Frame} type.
+ * @since v1.11
+ */
+ CDPSession newCDPSession(Frame page);
/**
* Creates a new page in the browser context.
*
diff --git a/playwright/src/main/java/com/microsoft/playwright/CDPSession.java b/playwright/src/main/java/com/microsoft/playwright/CDPSession.java
new file mode 100644
index 00000000..a1c5b0d2
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/CDPSession.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright;
+
+import java.util.function.Consumer;
+import com.google.gson.JsonObject;
+
+/**
+ * The {@code CDPSession} instances are used to talk raw Chrome Devtools Protocol:
+ *
+ * - protocol methods can be called with {@code session.send} method.
+ * - protocol events can be subscribed to with {@code session.on} method.
+ *
+ *
+ * Useful links:
+ *
+ * - Documentation on DevTools Protocol can be found here: DevTools Protocol Viewer.
+ * - Getting Started with DevTools Protocol: https://github.com/aslushnikov/getting-started-with-cdp/blob/master/README.md
+ *
{@code
+ * CDPSession client = page.context().newCDPSession(page);
+ * client.send("Runtime.enable");
+ *
+ * client.on("Animation.animationCreated", (event) -> System.out.println("Animation created!"));
+ *
+ * JsonObject response = client.send("Animation.getPlaybackRate");
+ * double playbackRate = response.get("playbackRate").getAsDouble();
+ * System.out.println("playback rate is " + playbackRate);
+ *
+ * JsonObject params = new JsonObject();
+ * params.addProperty("playbackRate", playbackRate / 2);
+ * client.send("Animation.setPlaybackRate", params);
+ * }
+ *
+ */
+public interface CDPSession {
+ /**
+ * Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used to
+ * send messages.
+ *
+ * @since v1.8
+ */
+ void detach();
+ /**
+ *
+ *
+ * @param method Protocol method name.
+ * @since v1.8
+ */
+ default JsonObject send(String method) {
+ return send(method, null);
+ }
+ /**
+ *
+ *
+ * @param method Protocol method name.
+ * @param args Optional method parameters.
+ * @since v1.8
+ */
+ JsonObject send(String method, JsonObject args);
+ /**
+ * Register an event handler for events with the specified event name. The given handler will be called for every event
+ * with the given name.
+ *
+ * @param eventName CDP event name.
+ * @param handler Event handler.
+ * @since v1.37
+ */
+ void on(String eventName, Consumer handler);
+ /**
+ * Unregister an event handler for events with the specified event name. The given handler will not be called anymore for
+ * events with the given name.
+ *
+ * @param eventName CDP event name.
+ * @param handler Event handler.
+ * @since v1.37
+ */
+ void off(String eventName, Consumer handler);
+}
+
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java
index 1fbcac3b..efb0f394 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java
@@ -214,6 +214,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
return waitForEventWithTimeout(EventType.PAGE, code, options.predicate, options.timeout);
}
+ @Override
+ public CDPSession newCDPSession(Page page) {
+ JsonObject params = new JsonObject();
+ params.add("page", ((PageImpl) page).toProtocolRef());
+ JsonObject result = sendMessage("newCDPSession", params).getAsJsonObject();
+ return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
+ }
+
+ @Override
+ public CDPSession newCDPSession(Frame frame) {
+ JsonObject params = new JsonObject();
+ params.add("frame", ((FrameImpl) frame).toProtocolRef());
+ JsonObject result = sendMessage("newCDPSession", params).getAsJsonObject();
+ return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
+ }
+
@Override
public void close() {
withLogging("BrowserContext.close", () -> closeImpl());
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java
index 17549fab..e630f9ba 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java
@@ -276,6 +276,13 @@ class BrowserImpl extends ChannelOwner implements Browser {
}
}
+ @Override
+ public CDPSession newBrowserCDPSession() {
+ JsonObject params = new JsonObject();
+ JsonObject result = sendMessage("newBrowserCDPSession", params).getAsJsonObject();
+ return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
+ }
+
private void didClose() {
isConnected = false;
listeners.notify(EventType.DISCONNECTED, this);
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java
new file mode 100644
index 00000000..929b9053
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java
@@ -0,0 +1,56 @@
+package com.microsoft.playwright.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.microsoft.playwright.CDPSession;
+
+import java.util.HashMap;
+import java.util.function.Consumer;
+
+public class CDPSessionImpl extends ChannelOwner implements CDPSession {
+ private final ListenerCollection listeners = new ListenerCollection<>(new HashMap<>(), this);
+
+ protected CDPSessionImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
+ super(parent, type, guid, initializer);
+ }
+
+ @Override
+ void handleEvent(String event, JsonObject parameters) {
+ super.handleEvent(event, parameters);
+ if ("event".equals(event)) {
+ String method = parameters.get("method").getAsString();
+ JsonObject params = parameters.get("params").getAsJsonObject();
+ listeners.notify(method, params);
+ }
+ }
+
+ public JsonObject send(String method) {
+ return send(method, null);
+ }
+
+ public JsonObject send(String method, JsonObject params) {
+ JsonObject args = new JsonObject();
+ if (params != null) {
+ args.add("params", params);
+ }
+ args.addProperty("method", method);
+ JsonElement response = connection.sendMessage(guid, "send", args);
+ if (response == null) return null;
+ else return response.getAsJsonObject().get("result").getAsJsonObject();
+ }
+
+ @Override
+ public void on(String event, Consumer handler) {
+ listeners.add(event, handler);
+ }
+
+ @Override
+ public void off(String event, Consumer handler) {
+ listeners.remove(event, handler);
+ }
+
+ @Override
+ public void detach() {
+ sendMessage("detach");
+ }
+}
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
index d996f0f1..e268aef7 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
@@ -359,6 +359,9 @@ public class Connection {
case "WritableStream":
result = new WritableStream(parent, type, guid, initializer);
break;
+ case "CDPSession":
+ result = new CDPSessionImpl(parent, type, guid, initializer);
+ break;
default:
throw new PlaywrightException("Unknown type " + type);
}
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java
index faa8425d..9abfed89 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java
@@ -16,10 +16,14 @@
package com.microsoft.playwright;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import com.microsoft.playwright.options.BrowserChannel;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.*;
@@ -98,4 +102,29 @@ public class TestBrowser extends TestBase {
void shouldReturnBrowserType() {
assertEquals(browserType, browser.browserType());
}
+
+ @Test
+ @EnabledIf(value = "com.microsoft.playwright.TestBase#isChromium", disabledReason = "Chrome Devtools Protocol supported by chromium only")
+ void shouldWorkWithNewBrowserCDPSession() {
+ CDPSession session = browser.newBrowserCDPSession();
+
+ JsonElement response = session.send("Browser.getVersion");
+ assertNotNull(response.getAsJsonObject().get("userAgent").toString());
+
+ AtomicReference gotEvent = new AtomicReference<>(false);
+
+ session.on("Target.targetCreated", jsonElement -> {
+ gotEvent.set(true);
+ });
+
+ JsonObject params = new JsonObject();
+ params.addProperty("discover", true);
+ session.send("Target.setDiscoverTargets", params);
+
+ Page page = browser.newPage();
+ assertTrue(gotEvent.get());
+ page.close();
+
+ session.detach();
+ }
}
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java
new file mode 100644
index 00000000..eeba7b6d
--- /dev/null
+++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@EnabledIf(value = "com.microsoft.playwright.TestBase#isChromium", disabledReason = "Chrome Devtools Protocol supported by chromium only")
+public class TestBrowserContextCDPSession extends TestBase {
+
+ @Test
+ void shouldWork() {
+ CDPSession cdpSession = page.context().newCDPSession(page);
+ cdpSession.send("Runtime.enable");
+
+ JsonObject params = new JsonObject();
+ params.addProperty("expression", "window.foo = 'bar'");
+ cdpSession.send("Runtime.evaluate", params);
+
+ Object foo = page.evaluate("window['foo']");
+ assertEquals("bar", foo);
+ }
+
+ @Test
+ void shouldSendEvents() {
+ CDPSession cdpSession = page.context().newCDPSession(page);
+ cdpSession.send("Network.enable");
+
+ List events = new ArrayList<>();
+ cdpSession.on("Network.requestWillBeSent", events::add);
+ page.navigate(server.EMPTY_PAGE);
+
+ assertEquals(1, events.size());
+ }
+
+ @Test
+ void shouldDetachSession() {
+ CDPSession cdpSession = page.context().newCDPSession(page);
+ cdpSession.send("Runtime.enable");
+
+ JsonObject params = new JsonObject();
+ params.addProperty("expression", "1 + 2");
+ params.addProperty("returnByValue", true);
+
+ JsonElement evaluateResult = cdpSession.send("Runtime.evaluate", params);
+ assertEquals(3, evaluateResult.getAsJsonObject().getAsJsonObject("result").get("value").getAsInt());
+
+ cdpSession.detach();
+
+ PlaywrightException exception = assertThrows(PlaywrightException.class, () -> {
+ cdpSession.send("Runtime.evaluate", params);
+ });
+ assertTrue(exception.getMessage().contains("Target page, context or browser has been closed"));
+ }
+
+ @Test
+ void shouldThrowNiceErrors() {
+ CDPSession cdpSession = page.context().newCDPSession(page);
+
+ PlaywrightException exception = assertThrows(PlaywrightException.class, () -> {
+ cdpSession.send("ThisCommand.DoesNotExist");
+ });
+ assertTrue(exception.getMessage().contains("'ThisCommand.DoesNotExist' wasn't found"));
+ }
+
+ @Test
+ void shouldWorkWithMainFrame() {
+ CDPSession cdpSession = page.context().newCDPSession(page.mainFrame());
+ JsonObject params = new JsonObject();
+ params.addProperty("expression", "window.foo = 'bar'");
+ cdpSession.send("Runtime.evaluate", params);
+
+ Object foo = page.evaluate("window['foo']");
+ assertEquals("bar", foo);
+ }
+
+ @Test
+ void shouldThrowIfTargetIsPartOfMain() {
+ page.navigate(server.PREFIX + "/frames/one-frame.html");
+ assertEquals(server.PREFIX + "/frames/one-frame.html", page.frames().get(0).url());
+ assertEquals(server.PREFIX + "/frames/frame.html", page.frames().get(1).url());
+
+ PlaywrightException exception = assertThrows(PlaywrightException.class, () -> {
+ page.context().newCDPSession(page.frames().get(1));
+ });
+ assertTrue(exception.getMessage().contains("This frame does not have a separate CDP session, it is a part of the parent frame's session"));
+ }
+
+ @Test
+ void shouldNotBreakPageClose() {
+ BrowserContext context = browser.newContext();
+ Page page = context.newPage();
+ CDPSession session = page.context().newCDPSession(page);
+ session.detach();
+ page.close();
+ context.close();
+ }
+
+ @Test
+ void shouldDetachWhenPageCloses() {
+ BrowserContext context = browser.newContext();
+ Page page = context.newPage();
+ CDPSession session = page.context().newCDPSession(page);
+ page.close();
+
+ PlaywrightException exception = assertThrows(PlaywrightException.class, session::detach);
+ assertTrue(exception.getMessage().contains("Target page, context or browser has been closed"));
+ context.close();
+ }
+
+ @Test
+ void shouldAddMultipleEventListeners() {
+ CDPSession cdpSession = page.context().newCDPSession(page);
+ cdpSession.send("Network.enable");
+
+ List events = new ArrayList<>();
+ cdpSession.on("Network.requestWillBeSent", events::add);
+ cdpSession.on("Network.requestWillBeSent", events::add);
+
+ page.navigate(server.EMPTY_PAGE);
+ assertEquals(2, events.size());
+ }
+
+ @Test
+ void shouldRemoveEventListeners() {
+ CDPSession cdpSession = page.context().newCDPSession(page);
+ cdpSession.send("Network.enable");
+
+ List events = new ArrayList<>();
+ Consumer listener1 = events::add;
+ cdpSession.on("Network.requestWillBeSent", listener1);
+ cdpSession.on("Network.requestWillBeSent", events::add);
+
+ page.navigate(server.EMPTY_PAGE);
+ assertEquals(2, events.size());
+
+ cdpSession.off("Network.requestWillBeSent", listener1);
+ events.clear();
+
+ page.navigate(server.EMPTY_PAGE);
+ assertEquals(1, events.size());
+ }
+}
diff --git a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java
index 9774eb30..caf601f5 100644
--- a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java
+++ b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java
@@ -964,7 +964,7 @@ class Interface extends TypeDefinition {
if (asList("Page", "Frame", "ElementHandle", "Locator", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").contains(jsonName)) {
output.add("import java.util.*;");
}
- if (asList("Page", "Browser", "BrowserContext", "WebSocket", "Worker").contains(jsonName)) {
+ if (asList("Page", "Browser", "BrowserContext", "WebSocket", "Worker", "CDPSession").contains(jsonName)) {
output.add("import java.util.function.Consumer;");
}
if (asList("Page", "BrowserContext").contains(jsonName)) {
@@ -977,6 +977,9 @@ class Interface extends TypeDefinition {
if (asList("Page", "Frame", "FrameLocator", "Locator", "Browser", "BrowserType", "BrowserContext", "PageAssertions", "LocatorAssertions").contains(jsonName)) {
output.add("import java.util.regex.Pattern;");
}
+ if ("CDPSession".equals(jsonName)) {
+ output.add("import com.google.gson.JsonObject;");
+ }
if ("PlaywrightAssertions".equals(jsonName)) {
output.add("import com.microsoft.playwright.APIResponse;");
output.add("import com.microsoft.playwright.Locator;");