Skip to content

Commit

Permalink
chore: follow upstream url matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt committed Dec 18, 2024
1 parent eb08046 commit f27b75b
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 40 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 100 additions & 32 deletions playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> predicate;

private static Predicate<String> 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<String> 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);
Expand All @@ -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;
}
Expand All @@ -68,50 +73,113 @@ 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.getScheme() != null && (
parsedUrl.getScheme().equals("http") || parsedUrl.getScheme().equals("https") ||
parsedUrl.getScheme().equals("ws") || parsedUrl.getScheme().equals("wss")
) && 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<String> predicate) {
this(predicate, predicate);
this(null, null, predicate, null);
}

private UrlMatcher(Object rawSource, Predicate<String> predicate) {
this.rawSource = rawSource;
private UrlMatcher(URL baseURL, Pattern pattern, Predicate<String> 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<String> 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
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<Boolean> matches = new ArrayList<>();
if (baseURL != null && that.baseURL != null) {
matches.add(baseURL.equals(that.baseURL));
}
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));
}
return Objects.equals(rawSource, that.rawSource);
if (glob != null && that.glob != null) {
matches.add(glob.equals(that.glob));
}
return matches.stream().allMatch(m -> m);
}

@Override
public int hashCode() {
return Objects.hash(rawSource);
if (pattern != null) {
return pattern.hashCode();
}
if (predicate != null) {
return predicate.hashCode();
}
return glob.hashCode();
}

@Override
public String toString() {
if (rawSource == null)
return "<any>";
if (rawSource instanceof Predicate)
return "matching predicate";
return rawSource.toString();
if (pattern != null)
return String.format("<regex pattern=\"%s\" flags=\"%s\">", pattern.pattern(), toJsRegexFlags(pattern));
if (predicate != null)
return "<predicate>";
return String.format("<glob pattern=\"%s\">", glob);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,11 @@ static JsonObject interceptionPatterns(List<UrlMatcher> 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", "**/*");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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"));
}
}

0 comments on commit f27b75b

Please sign in to comment.