Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: follow upstream url matcher #1717

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert this part, it can be refactored in a separate change.

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will only match baseURL while it was matching anything before.

}
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"));
}
}
Loading