From 40c663cccede4efd097aee26cabf4f6221e24d1c Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 6 Apr 2021 23:20:07 -0700 Subject: [PATCH] feat: support connectOverCDP (#387) --- .../com/microsoft/playwright/BrowserType.java | 43 +++++++++ .../playwright/impl/BrowserImpl.java | 5 +- .../playwright/impl/BrowserTypeImpl.java | 36 ++++++-- .../com/microsoft/playwright/TestBase.java | 2 +- .../playwright/TestBrowserTypeBasic.java | 53 +++++++++++ .../microsoft/playwright/TestChromium.java | 89 +++++++++++++++++++ scripts/CLI_VERSION | 2 +- 7 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestChromium.java diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index 7d96ff09..ea4111a9 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -62,6 +62,27 @@ public interface BrowserType { return this; } } + class ConnectOverCDPOptions { + /** + * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. + * Defaults to 0. + */ + public Double slowMo; + /** + * Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to + * disable timeout. + */ + public Double timeout; + + public ConnectOverCDPOptions setSlowMo(double slowMo) { + this.slowMo = slowMo; + return this; + } + public ConnectOverCDPOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } class LaunchOptions { /** * Additional arguments to pass to the browser instance. The list of Chromium flags can be found The default browser context is accessible via {@link Browser#contexts Browser.contexts()}. + * + *

NOTE: Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. + * + * @param wsEndpoint A CDP websocket endpoint to connect to. + */ + default Browser connectOverCDP(String wsEndpoint) { + return connectOverCDP(wsEndpoint, null); + } + /** + * This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol. + * + *

The default browser context is accessible via {@link Browser#contexts Browser.contexts()}. + * + *

NOTE: Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. + * + * @param wsEndpoint A CDP websocket endpoint to connect to. + */ + Browser connectOverCDP(String wsEndpoint, ConnectOverCDPOptions options); /** * A path where Playwright expects to find a bundled browser executable. */ 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 1644a416..9dbdbea1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java @@ -40,7 +40,8 @@ import static com.microsoft.playwright.impl.Utils.isSafeCloseError; class BrowserImpl extends ChannelOwner implements Browser { final Set contexts = new HashSet<>(); private final ListenerCollection listeners = new ListenerCollection<>(); - public boolean isRemote; + boolean isRemote; + boolean isConnectedOverWebSocket; private boolean isConnected = true; enum EventType { @@ -67,7 +68,7 @@ class BrowserImpl extends ChannelOwner implements Browser { } private void closeImpl() { - if (isRemote) { + if (isConnectedOverWebSocket) { try { connection.close(); } catch (IOException e) { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java index 3efc302c..0c88275c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java @@ -69,12 +69,8 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { playwright.sharedSelectors.addChannel(selectors); BrowserImpl browser = remoteBrowser.browser(); browser.isRemote = true; - Consumer connectionCloseListener = new Consumer() { - @Override - public void accept(WebSocketTransport t) { - browser.notifyRemoteClosed(); - } - }; + browser.isConnectedOverWebSocket = true; + Consumer connectionCloseListener = t -> browser.notifyRemoteClosed(); transport.onClose(connectionCloseListener); browser.onDisconnected(b -> { playwright.sharedSelectors.removeChannel(selectors); @@ -91,6 +87,34 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { } } + @Override + public Browser connectOverCDP(String wsEndpoint, ConnectOverCDPOptions options) { + if (!"chromium".equals(name())) { + throw new PlaywrightException("Connecting over CDP is only supported in Chromium."); + } + return withLogging("BrowserType.connectOverCDP", () -> connectOverCDPImpl(wsEndpoint, options)); + } + + private Browser connectOverCDPImpl(String wsEndpoint, ConnectOverCDPOptions options) { + if (options == null) { + options = new ConnectOverCDPOptions(); + } + + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("sdkLanguage", "java"); + params.addProperty("wsEndpoint", wsEndpoint); + JsonObject json = sendMessage("connectOverCDP", params).getAsJsonObject(); + + BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString()); + browser.isRemote = true; + if (json.has("defaultContext")) { + String contextId = json.getAsJsonObject("defaultContext").get("guid").getAsString(); + BrowserContextImpl defaultContext = connection.getExistingObject(contextId); + browser.contexts.add(defaultContext); + } + return browser; + } + public String executablePath() { return initializer.get("executablePath").getAsString(); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBase.java b/playwright/src/test/java/com/microsoft/playwright/TestBase.java index eed44e75..1c26cfde 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBase.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBase.java @@ -54,7 +54,7 @@ public class TestBase { return "firefox".equals(getBrowserNameFromEnv()); } - private static BrowserChannel getBrowserChannelFromEnv() { + static BrowserChannel getBrowserChannelFromEnv() { String channel = System.getenv("BROWSER_CHANNEL"); if (channel == null) { return null; diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java new file mode 100644 index 00000000..d54e5fb5 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java @@ -0,0 +1,53 @@ +/* + * 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 org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import static com.microsoft.playwright.Utils.getBrowserNameFromEnv; +import static org.junit.jupiter.api.Assertions.*; + +public class TestBrowserTypeBasic extends TestBase { + @Test + void browserTypeExecutablePathShouldWork() { + Assumptions.assumeTrue(getBrowserChannelFromEnv() == null); + Assumptions.assumeTrue(createLaunchOptions().executablePath == null, "Skip with custom executable path"); + String executablePath = browserType.executablePath(); + assertTrue(Files.exists(Paths.get(executablePath))); + } + + @Test + void browserTypeNameShouldWork() { + assertEquals(getBrowserNameFromEnv(), browserType.name()); + } + + @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Non-chromium behavior") + void shouldThrowWhenTryingToConnectWithNotChromium() { + try { + browserType.connectOverCDP("foo"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Connecting over CDP is only supported in Chromium.")); + } + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestChromium.java b/playwright/src/test/java/com/microsoft/playwright/TestChromium.java new file mode 100644 index 00000000..4161216d --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestChromium.java @@ -0,0 +1,89 @@ +/* + * 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.Gson; +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.net.URLConnection; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@EnabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Chromium-specific API") +public class TestChromium extends TestBase { + @Override + void createContextAndPage() { + // Do not create anything. + } + + private static String wsEndpointFromUrl(String urlString) throws IOException { + URL url = new URL(urlString); + URLConnection request = url.openConnection(); + request.connect(); + Reader reader = new InputStreamReader((InputStream) request.getContent()); + JsonObject json = new Gson().fromJson(reader, JsonObject.class); + return json.get("webSocketDebuggerUrl").getAsString(); + } + + @Test + void shouldConnectToAnExistingCdpSession() throws IOException { + int port = 9339; + try (Browser browserServer = browserType.launch(createLaunchOptions() + .setArgs(asList("--remote-debugging-port=" + port)))) { + String wsEndpoint = wsEndpointFromUrl("http://localhost:" + port + "/json/version/"); + Browser cdpBrowser = browserType.connectOverCDP(wsEndpoint); + List contexts = cdpBrowser.contexts(); + assertEquals(1, contexts.size()); + cdpBrowser.close(); + } + } + + @Test + void shouldConnectToAnExistingCdpSessionTwice() throws IOException { + int port = 9339; + try (Browser browserServer = browserType.launch(createLaunchOptions() + .setArgs(asList("--remote-debugging-port=" + port)))) { + String wsEndpoint = wsEndpointFromUrl("http://localhost:" + port + "/json/version/"); + Browser cdpBrowser1 = browserType.connectOverCDP(wsEndpoint); + Browser cdpBrowser2 = browserType.connectOverCDP(wsEndpoint); + List contexts1 = cdpBrowser1.contexts(); + assertEquals(1, contexts1.size()); + Page page1 = contexts1.get(0).newPage(); + page1.navigate(server.EMPTY_PAGE); + + List contexts2 = cdpBrowser2.contexts(); + assertEquals(1, contexts2.size()); + Page page2 = contexts2.get(0).newPage(); + page2.navigate(server.EMPTY_PAGE); + + assertEquals(2, contexts1.get(0).pages().size()); + assertEquals(2, contexts2.get(0).pages().size()); + + cdpBrowser1.close(); + cdpBrowser2.close(); + } + } +} diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 82da482c..6bf7cf4a 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.11.0-next-1617387566000 +1.11.0-next-1617755629000