diff --git a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index aa7e9cf3..a3001696 100644 --- a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -297,10 +297,37 @@ class Method extends Element { tsToJavaMethodName.put("goto", "navigate"); } - private static Map customSignature = new HashMap<>(); + private static Map customSignature = new HashMap<>(); static { - customSignature.put("Page.setViewportSize", "void setViewportSize(int width, int height);"); - customSignature.put("BrowserContext.setHTTPCredentials", "void setHTTPCredentials(String username, String password);"); + customSignature.put("Page.setViewportSize", new String[]{"void setViewportSize(int width, int height);"}); + customSignature.put("BrowserContext.setHTTPCredentials", new String[]{ + "void setHTTPCredentials(String username, String password);"}); + customSignature.put("BrowserContext.route", new String[]{ + "void route(String url, BiConsumer handler);", + "void route(Pattern url, BiConsumer handler);", + "void route(Predicate url, BiConsumer handler);", + }); + customSignature.put("Page.route", new String[]{ + "void route(String url, BiConsumer handler);", + "void route(Pattern url, BiConsumer handler);", + "void route(Predicate url, BiConsumer handler);", + }); + customSignature.put("BrowserContext.unroute", new String[]{ + "default void unroute(String url) { unroute(url, null); }", + "default void unroute(Pattern url) { unroute(url, null); }", + "default void unroute(Predicate url) { unroute(url, null); }", + "void unroute(String url, BiConsumer handler);", + "void unroute(Pattern url, BiConsumer handler);", + "void unroute(Predicate url, BiConsumer handler);", + }); + customSignature.put("Page.unroute", new String[]{ + "default void unroute(String url) { unroute(url, null); }", + "default void unroute(Pattern url) { unroute(url, null); }", + "default void unroute(Predicate url) { unroute(url, null); }", + "void unroute(String url, BiConsumer handler);", + "void unroute(Pattern url, BiConsumer handler);", + "void unroute(Predicate url, BiConsumer handler);", + }); } Method(TypeDefinition parent, JsonObject jsonElement) { @@ -327,7 +354,9 @@ class Method extends Element { void writeTo(List output, String offset) { if (customSignature.containsKey(jsonPath)) { - output.add(offset + customSignature.get(jsonPath)); + for (String signature : customSignature.get(jsonPath)) { + output.add(offset + signature); + } return; } for (int i = params.size() - 1; i >= 0; i--) { @@ -375,6 +404,9 @@ class Param extends Element { Param(Method method, JsonObject jsonElement) { super(method, jsonElement); type = new TypeRef(this, jsonElement.get("type").getAsJsonObject()); + if (jsonPath.contains(".route")) { + System.out.println(jsonPath + ": " + type.jsonName); + } } boolean isOptional() { @@ -490,12 +522,10 @@ class Interface extends TypeDefinition { void writeTo(List output, String offset) { output.add(header); output.add("import java.util.*;"); - if (jsonName.equals("Page")) { + if (Arrays.asList("Page", "BrowserContext").contains(jsonName)) { output.add("import java.util.function.BiConsumer;"); output.add("import java.util.function.Predicate;"); output.add("import java.util.regex.Pattern;"); - } else if (jsonName.equals("BrowserContext")) { - output.add("import java.util.function.BiConsumer;"); } output.add(""); diff --git a/lib/src/main/java/com/microsoft/playwright/BrowserContext.java b/lib/src/main/java/com/microsoft/playwright/BrowserContext.java index 4023fbc5..fff65399 100644 --- a/lib/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/lib/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -18,6 +18,8 @@ package com.microsoft.playwright; import java.util.*; import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; public interface BrowserContext { class HTTPCredentials { @@ -87,16 +89,20 @@ public interface BrowserContext { Page newPage(); List pages(); void route(String url, BiConsumer handler); + void route(Pattern url, BiConsumer handler); + void route(Predicate url, BiConsumer handler); void setDefaultNavigationTimeout(int timeout); void setDefaultTimeout(int timeout); void setExtraHTTPHeaders(Map headers); void setGeolocation(Geolocation geolocation); void setHTTPCredentials(String username, String password); void setOffline(boolean offline); - default void unroute(String url) { - unroute(url, null); - } + default void unroute(String url) { unroute(url, null); } + default void unroute(Pattern url) { unroute(url, null); } + default void unroute(Predicate url) { unroute(url, null); } void unroute(String url, BiConsumer handler); + void unroute(Pattern url, BiConsumer handler); + void unroute(Predicate url, BiConsumer handler); default Object waitForEvent(String event) { return waitForEvent(event, null); } diff --git a/lib/src/main/java/com/microsoft/playwright/Page.java b/lib/src/main/java/com/microsoft/playwright/Page.java index 0aaaa8c3..efb90f5d 100644 --- a/lib/src/main/java/com/microsoft/playwright/Page.java +++ b/lib/src/main/java/com/microsoft/playwright/Page.java @@ -889,6 +889,8 @@ public interface Page { } Response reload(ReloadOptions options); void route(String url, BiConsumer handler); + void route(Pattern url, BiConsumer handler); + void route(Predicate url, BiConsumer handler); default byte[] screenshot() { return screenshot(null); } @@ -922,10 +924,12 @@ public interface Page { uncheck(selector, null); } void uncheck(String selector, UncheckOptions options); - default void unroute(String url) { - unroute(url, null); - } + default void unroute(String url) { unroute(url, null); } + default void unroute(Pattern url) { unroute(url, null); } + default void unroute(Predicate url) { unroute(url, null); } void unroute(String url, BiConsumer handler); + void unroute(Pattern url, BiConsumer handler); + void unroute(Predicate url, BiConsumer handler); String url(); Viewport viewportSize(); default Object waitForEvent(String event) { diff --git a/lib/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/lib/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index cbeccd12..f75b6341 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -26,33 +26,19 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; -import java.util.function.Supplier; +import java.util.function.Predicate; import java.util.regex.Pattern; -import java.util.stream.Collectors; -import static com.microsoft.playwright.impl.Utils.globToRegex; import static com.microsoft.playwright.impl.Utils.isFunctionBody; class BrowserContextImpl extends ChannelOwner implements BrowserContext { private final BrowserImpl browser; final List pages = new ArrayList<>(); - private List routes = new ArrayList<>(); + final Router routes = new Router(); private boolean isClosedOrClosing; final Map bindings = new HashMap(); PageImpl ownerPage; - private class RouteInfo { - private String url; - private BiConsumer handler; - private final Pattern pattern; - - public RouteInfo(String url, BiConsumer handler) { - this.url = url; - this.handler = handler; - pattern = Pattern.compile(globToRegex(url)); - } - } - protected BrowserContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); browser = (BrowserImpl) parent; @@ -148,7 +134,21 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { @Override public void route(String url, BiConsumer handler) { - routes.add(new RouteInfo(url, handler)); + route(new UrlMatcher(url), handler); + } + + @Override + public void route(Pattern url, BiConsumer handler) { + route(new UrlMatcher(url), handler); + } + + @Override + public void route(Predicate url, BiConsumer handler) { + route(new UrlMatcher(url), handler); + } + + private void route(UrlMatcher matcher, BiConsumer handler) { + routes.add(matcher, handler); if (routes.size() == 1) { JsonObject params = new JsonObject(); params.addProperty("enabled", true); @@ -190,10 +190,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { @Override public void unroute(String url, BiConsumer handler) { - routes = routes.stream() - .filter(info -> !info.url.equals(url) || (handler != null && info.handler != handler)) - .collect(Collectors.toList()); - if (routes.isEmpty()) { + unroute(new UrlMatcher(url), handler); + } + + @Override + public void unroute(Pattern url, BiConsumer handler) { + unroute(new UrlMatcher(url), handler); + } + + @Override + public void unroute(Predicate url, BiConsumer handler) { + unroute(new UrlMatcher(url), handler); + } + + private void unroute(UrlMatcher matcher, BiConsumer handler) { + routes.remove(matcher, handler); + if (routes.size() == 0) { JsonObject params = new JsonObject(); params.addProperty("enabled", false); sendMessage("setNetworkInterceptionEnabled", params); @@ -220,12 +232,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { if ("route".equals(event)) { Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString()); Request request = connection.getExistingObject(params.getAsJsonObject("request").get("guid").getAsString()); - for (RouteInfo info : routes) { - if (info.pattern.matcher(request.url()).find()) { - info.handler.accept(route, request); - } + boolean handled = routes.handle(route, request); + if (!handled) { + route.continue_(); } - route.continue_(); } else if ("page".equals(event)) { PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString()); pages.add(page); diff --git a/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java index 1bde5888..3c232744 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -24,6 +24,8 @@ import com.microsoft.playwright.*; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; import static com.microsoft.playwright.impl.Utils.convertViaJson; @@ -34,6 +36,7 @@ public class PageImpl extends ChannelOwner implements Page { private final KeyboardImpl keyboard; private final MouseImpl mouse; private Viewport viewport; + private final Router routes = new Router(); // TODO: do not rely on the frame order in the tests private final Set frames = new LinkedHashSet<>(); private final List> consoleListeners = new ArrayList<>(); @@ -127,6 +130,16 @@ public class PageImpl extends ChannelOwner implements Page { if (frame.parentFrame != null) { frame.parentFrame.childFrames.remove(frame); } + } else if ("route".equals(event)) { + Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString()); + Request request = connection.getExistingObject(params.getAsJsonObject("request").get("guid").getAsString()); + boolean handled = routes.handle(route, request); + if (!handled) { + handled = browserContext.routes.handle(route, request); + } + if (!handled) { + route.continue_(); + } } else if ("close".equals(event)) { isClosed = true; browserContext.pages.remove(this); @@ -274,7 +287,7 @@ public class PageImpl extends ChannelOwner implements Page { @Override public Frame frame(FrameOptions options) { if (options == null) { - throw new IllegalArgumentException("Frame criteria should be cpecified"); + throw new IllegalArgumentException("Frame criteria should be specified"); } for (Frame frame : frames) { if (options.name != null && options.name.equals(frame.name())) { @@ -379,7 +392,26 @@ public class PageImpl extends ChannelOwner implements Page { @Override public void route(String url, BiConsumer handler) { + route(new UrlMatcher(url), handler); + } + @Override + public void route(Pattern url, BiConsumer handler) { + route(new UrlMatcher(url), handler); + } + + @Override + public void route(Predicate url, BiConsumer handler) { + route(new UrlMatcher(url), handler); + } + + private void route(UrlMatcher matcher, BiConsumer handler) { + routes.add(matcher, handler); + if (routes.size() == 1) { + JsonObject params = new JsonObject(); + params.addProperty("enabled", true); + sendMessage("setNetworkInterceptionEnabled", params); + } } @Override @@ -447,7 +479,26 @@ public class PageImpl extends ChannelOwner implements Page { @Override public void unroute(String url, BiConsumer handler) { + unroute(new UrlMatcher(url), handler); + } + @Override + public void unroute(Pattern url, BiConsumer handler) { + unroute(new UrlMatcher(url), handler); + } + + @Override + public void unroute(Predicate url, BiConsumer handler) { + unroute(new UrlMatcher(url), handler); + } + + private void unroute(UrlMatcher matcher, BiConsumer handler) { + routes.remove(matcher, handler); + if (routes.size() == 0) { + JsonObject params = new JsonObject(); + params.addProperty("enabled", false); + sendMessage("setNetworkInterceptionEnabled", params); + } } @Override diff --git a/lib/src/main/java/com/microsoft/playwright/impl/RequestImpl.java b/lib/src/main/java/com/microsoft/playwright/impl/RequestImpl.java index 58569230..edda4e04 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/RequestImpl.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/RequestImpl.java @@ -16,16 +16,20 @@ package com.microsoft.playwright.impl; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.Frame; import com.microsoft.playwright.Request; import com.microsoft.playwright.Response; +import java.util.HashMap; import java.util.Map; public class RequestImpl extends ChannelOwner implements Request { private RequestImpl redirectedFrom; private RequestImpl redirectedTo; + private final Map headers = new HashMap<>(); RequestImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); @@ -33,6 +37,10 @@ public class RequestImpl extends ChannelOwner implements Request { redirectedFrom = connection.getExistingObject(initializer.getAsJsonObject("redirectedFrom").get("guid").getAsString()); redirectedFrom.redirectedTo = this; } + for (JsonElement e : initializer.getAsJsonArray("headers")) { + JsonObject item = e.getAsJsonObject(); + headers.put(item.get("name").getAsString().toLowerCase(), item.get("value").getAsString()); + } } @Override @@ -47,7 +55,7 @@ public class RequestImpl extends ChannelOwner implements Request { @Override public Map headers() { - return null; + return headers; } @Override diff --git a/lib/src/main/java/com/microsoft/playwright/impl/Router.java b/lib/src/main/java/com/microsoft/playwright/impl/Router.java new file mode 100644 index 00000000..5a3c55ad --- /dev/null +++ b/lib/src/main/java/com/microsoft/playwright/impl/Router.java @@ -0,0 +1,63 @@ +/** + * 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.impl; + +import com.microsoft.playwright.Request; +import com.microsoft.playwright.Route; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +class Router { + private List routes = new ArrayList<>(); + + private static class RouteInfo { + final UrlMatcher matcher; + final BiConsumer handler; + + RouteInfo(UrlMatcher matcher, BiConsumer handler) { + this.matcher = matcher; + this.handler = handler; + } + } + + void add(UrlMatcher matcher, BiConsumer handler) { + routes.add(new RouteInfo(matcher, handler)); + } + + void remove(UrlMatcher matcher, BiConsumer handler) { + routes = routes.stream() + .filter(info -> !info.matcher.equals(matcher) || (handler != null && info.handler != handler)) + .collect(Collectors.toList()); + } + + int size() { + return routes.size(); + } + + boolean handle(Route route, Request request) { + for (RouteInfo info : routes) { + if (info.matcher.test(request.url())) { + info.handler.accept(route, request); + return true; + } + } + return false; + } +} diff --git a/lib/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java b/lib/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java new file mode 100644 index 00000000..d8a12ab3 --- /dev/null +++ b/lib/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java @@ -0,0 +1,65 @@ +/** + * 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.impl; + +import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import static com.microsoft.playwright.impl.Utils.globToRegex; + +class UrlMatcher { + private final Object rawSource; + private final Predicate predicate; + + private static Predicate toPridcate(Pattern pattern) { + return s -> pattern.matcher(s).find(); + } + + UrlMatcher(String url) { + this(url, toPridcate(Pattern.compile(globToRegex(url)))); + } + + UrlMatcher(Pattern pattern) { + this(pattern, toPridcate(pattern)); + } + UrlMatcher(Predicate predicate) { + this(predicate, predicate); + } + + private UrlMatcher(Object rawSource, Predicate predicate) { + this.rawSource = rawSource; + this.predicate = predicate; + } + + boolean test(String value) { + return predicate.test(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UrlMatcher that = (UrlMatcher) o; + return Objects.equals(rawSource, that.rawSource); + } + + @Override + public int hashCode() { + return Objects.hash(rawSource); + } +} diff --git a/lib/src/test/java/com/microsoft/playwright/TestPageRoute.java b/lib/src/test/java/com/microsoft/playwright/TestPageRoute.java new file mode 100644 index 00000000..71bb5d78 --- /dev/null +++ b/lib/src/test/java/com/microsoft/playwright/TestPageRoute.java @@ -0,0 +1,127 @@ +/** + * 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.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestPageRoute { + private static Server server; + private static Browser browser; + private static boolean isChromium; + private static boolean isWebKit; + private static boolean isFirefox; + private BrowserContext context; + private Page page; + + @BeforeAll + static void launchBrowser() { + Playwright playwright = Playwright.create(); + BrowserType.LaunchOptions options = new BrowserType.LaunchOptions(); + browser = playwright.chromium().launch(options); + isChromium = true; + + } + + @BeforeAll + static void startServer() throws IOException { + server = new Server(8907); + } + + @AfterAll + static void stopServer() throws IOException { + browser.close(); + server.stop(); + server = null; + } + + @BeforeEach + void setUp() { + context = browser.newContext(); + page = context.newPage(); + } + + @AfterEach + void tearDown() { + context.close(); + context = null; + page = null; + } + + @Test + void shouldIntercept() { + boolean[] intercepted = {false}; + page.route("**/empty.html", (route, request) -> { + assertEquals(request, route.request()); + assertTrue(request.url().contains("empty.html")); + assertNotNull(request.headers().get("user-agent")); + assertEquals("GET", request.method()); + assertNull(request.postData()); + assertTrue(request.isNavigationRequest()); + assertEquals("document", request.resourceType()); + assertTrue(request.frame() == page.mainFrame()); + assertEquals("about:blank", request.frame().url()); + route.continue_(); + intercepted[0] = true; + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertTrue(response.ok()); + assertTrue(intercepted[0]); + } + + + @Test + void shouldUnroute() { + List intercepted = new ArrayList<>(); + BiConsumer handler1 = (route, request) -> { + intercepted.add(1); + route.continue_(); + }; + page.route("**/empty.html", handler1); + page.route("**/empty.html", (route, request) -> { + intercepted.add(2); + route.continue_(); + }); + page.route("**/empty.html", (route, request) -> { + intercepted.add(3); + route.continue_(); + }); + page.route("**/*", (route, request) -> { + intercepted.add(4); + route.continue_(); + }); + page.navigate(server.EMPTY_PAGE); + assertEquals(Arrays.asList(1), intercepted); + + intercepted.clear(); + page.unroute("**/empty.html", handler1); + page.navigate(server.EMPTY_PAGE); + assertEquals(Arrays.asList(2), intercepted); + + intercepted.clear(); + page.unroute("**/empty.html"); + page.navigate(server.EMPTY_PAGE); + assertEquals(Arrays.asList(4), intercepted); + } +}