From 4a39417fd2ef37aab02eb8fe22f57266a861952c Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 18 Dec 2024 10:57:24 +0100 Subject: [PATCH] chore: follow upstream url matcher --- CONTRIBUTING.md | 2 +- .../microsoft/playwright/impl/UrlMatcher.java | 126 +++++++++++++----- .../com/microsoft/playwright/impl/Utils.java | 12 +- .../playwright/TestRouteWebSocket.java | 42 ++++++ 4 files changed, 138 insertions(+), 44 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab3033cd..dfe0ac85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ scripts/download_driver.sh ### Building and running the tests with Maven ```bash -mvn compile +mvn -B install -D skipTest mvn test # Executing a single test BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java index 69153ce7..c922ffe5 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java @@ -16,34 +16,29 @@ package com.microsoft.playwright.impl; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import com.microsoft.playwright.PlaywrightException; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; -import java.util.Objects; import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.List; import static com.microsoft.playwright.impl.Utils.globToRegex; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; class UrlMatcher { - final Object rawSource; - private final Predicate predicate; - - private static Predicate toPredicate(Pattern pattern) { - return s -> pattern.matcher(s).find(); - } - - static UrlMatcher any() { - return new UrlMatcher((Object) null, null); - } + private final URL baseURL; + public final String glob; + public final Pattern pattern; + public final Predicate predicate; static UrlMatcher forOneOf(URL baseUrl, Object object) { if (object == null) { - return UrlMatcher.any(); + return new UrlMatcher(baseUrl, (String) null); } if (object instanceof String) { return new UrlMatcher(baseUrl, (String) object); @@ -58,6 +53,16 @@ static UrlMatcher forOneOf(URL baseUrl, Object object) { } static String resolveUrl(URL baseUrl, String spec) { + try { + URL specURL = new URL(spec); + // We want to follow HTTP spec, so we enforce a slash if there is no path. + if (specURL.getPath().isEmpty()) { + spec = specURL.toString() + "/"; + } + } catch (MalformedURLException e) { + // Ignore - we end up here if spec is e.g. a relative path. + } + if (baseUrl == null) { return spec; } @@ -68,24 +73,70 @@ static String resolveUrl(URL baseUrl, String spec) { } } - UrlMatcher(URL base, String url) { - this(url, toPredicate(Pattern.compile(globToRegex(resolveUrl(base, url)))).or(s -> url == null || url.equals(s))); + static private String normaliseUrl(String url) { + URI parsedUrl; + try { + parsedUrl = new URI(url); + } catch (URISyntaxException e) { + return url; + } + // Align with the Node.js URL parser which automatically adds a slash to the path if it is empty. + if (parsedUrl.getPath().isEmpty()) { + try { + return new URI(parsedUrl.getScheme(), parsedUrl.getAuthority(), "/", parsedUrl.getQuery(), parsedUrl.getFragment()).toString(); + } catch (URISyntaxException e) { + return url; + } + } + return url; + } + + UrlMatcher(URL baseURL, String glob) { + this(baseURL, null, null, glob); } UrlMatcher(Pattern pattern) { - this(pattern, toPredicate(pattern)); + this(null, pattern, null, null); } + UrlMatcher(Predicate predicate) { - this(predicate, predicate); + this(null, null, predicate, null); } - private UrlMatcher(Object rawSource, Predicate predicate) { - this.rawSource = rawSource; + private UrlMatcher(URL baseURL, Pattern pattern, Predicate predicate, String glob) { + this.baseURL = baseURL; + this.pattern = pattern; this.predicate = predicate; + this.glob = glob; } boolean test(String value) { - return predicate == null || predicate.test(value); + return testImpl(baseURL, pattern, predicate, glob, value); + } + + private static boolean testImpl(URL baseURL, Pattern pattern, Predicate predicate, String glob, String value) { + if (pattern != null) { + return pattern.matcher(value).find(); + } + if (predicate != null) { + return predicate.test(value); + } + if (glob != null) { + if (!glob.startsWith("*")) { + glob = normaliseUrl(glob); + // Allow http(s) baseURL to match ws(s) urls. + if (baseURL != null && Pattern.compile("^https?://").matcher(baseURL.getProtocol()).find() && Pattern.compile("^wss?://").matcher(value).find()) { + try { + baseURL = new URL(baseURL.toString().replaceFirst("^http", "ws")); + } catch (MalformedURLException e) { + // Handle exception + } + } + glob = resolveUrl(baseURL, glob); + } + return Pattern.compile(globToRegex(glob)).matcher(value).find(); + } + return true; } @Override @@ -93,25 +144,28 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UrlMatcher that = (UrlMatcher) o; - if (rawSource instanceof Pattern && that.rawSource instanceof Pattern) { - Pattern a = (Pattern) rawSource; - Pattern b = (Pattern) that.rawSource; - return a.pattern().equals(b.pattern()) && a.flags() == b.flags(); + List matches = new ArrayList<>(); + if (baseURL != null && that.baseURL != null) { + matches.add(baseURL.equals(that.baseURL)); } - return Objects.equals(rawSource, that.rawSource); - } - - @Override - public int hashCode() { - return Objects.hash(rawSource); + if (pattern != null && that.pattern != null) { + matches.add(pattern.pattern().equals(that.pattern.pattern()) && pattern.flags() == that.pattern.flags()); + } + if (predicate != null && that.predicate != null) { + matches.add(predicate.equals(that.predicate)); + } + if (glob != null && that.glob != null) { + matches.add(glob.equals(that.glob)); + } + return matches.stream().allMatch(m -> m); } @Override public String toString() { - if (rawSource == null) - return ""; - if (rawSource instanceof Predicate) - return "matching predicate"; - return rawSource.toString(); + if (pattern != null) + return String.format("", pattern.pattern(), toJsRegexFlags(pattern)); + if (predicate != null) + return ""; + return String.format("", glob); } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java index 61df059a..42ab44d3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -462,13 +462,11 @@ static JsonObject interceptionPatterns(List matchers) { JsonArray jsonPatterns = new JsonArray(); for (UrlMatcher matcher: matchers) { JsonObject jsonPattern = new JsonObject(); - Object urlFilter = matcher.rawSource; - if (urlFilter instanceof String) { - jsonPattern.addProperty("glob", (String) urlFilter); - } else if (urlFilter instanceof Pattern) { - Pattern pattern = (Pattern) urlFilter; - jsonPattern.addProperty("regexSource", pattern.pattern()); - jsonPattern.addProperty("regexFlags", toJsRegexFlags(pattern)); + if (matcher.glob != null) { + jsonPattern.addProperty("glob", matcher.glob); + } else if (matcher.pattern != null) { + jsonPattern.addProperty("regexSource", matcher.pattern.pattern()); + jsonPattern.addProperty("regexFlags", toJsRegexFlags(matcher.pattern)); } else { // Match all requests. jsonPattern.addProperty("glob", "**/*"); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java index c414ac78..6de5e073 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java @@ -9,8 +9,10 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.List; import java.util.regex.Pattern; import static com.microsoft.playwright.Utils.mapOf; @@ -317,4 +319,44 @@ public void shouldWorkWithoutServer(Page page) { "close code=3008 reason=oops"), page.evaluate("window.log")); } + + @Test + public void shouldWorkWithNoTrailingSlash(Page page) throws Exception { + List log = new ArrayList<>(); + + // No trailing slash in the route pattern + page.routeWebSocket("ws://localhost:" + webSocketServer.getPort(), ws -> { + ws.onMessage(message -> { + log.add(message.text()); + ws.send("response"); + }); + }); + + page.navigate("about:blank"); + page.evaluate("({ port }) => {\n" + + " window.log = [];\n" + + " // No trailing slash in WebSocket URL\n" + + " window.ws = new WebSocket('ws://localhost:' + port);\n" + + " window.ws.addEventListener('message', event => window.log.push(event.data));\n" + + "}", mapOf("port", webSocketServer.getPort())); + + // Wait for WebSocket to be ready (readyState === 1) + page.waitForCondition(() -> { + Integer result = (Integer) page.evaluate("() => window.ws.readyState"); + return result == 1; + }); + + page.evaluate("() => window.ws.send('query')"); + + // Wait and verify server received message + page.waitForCondition(() -> log.size() >= 1); + assertEquals(asList("query"), log); + + // Wait and verify client received response + page.waitForCondition(() -> { + Boolean result = (Boolean) page.evaluate("() => window.log.length >= 1"); + return result; + }); + assertEquals(asList("response"), page.evaluate("window.log")); + } }