diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java b/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java index 5e9a6520b..4761e8bea 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; +import static com.microsoft.playwright.impl.Utils.toJsRegexSource; import static java.util.Arrays.asList; abstract class AssertionsBase { @@ -95,7 +96,7 @@ protected static ValueWrapper formatValue(Object value) { static ExpectedTextValue expectedRegex(Pattern pattern) { ExpectedTextValue expected = new ExpectedTextValue(); - expected.regexSource = pattern.pattern(); + expected.regexSource = toJsRegexSource(pattern); if (pattern.flags() != 0) { expected.regexFlags = toJsRegexFlags(pattern); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java index 8d9c025cd..bc2e1382a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java @@ -23,6 +23,7 @@ import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; +import static com.microsoft.playwright.impl.Utils.toJsRegexSource; public class LocatorUtils { static String getByTextSelector(Object text, Locator.GetByTextOptions options) { @@ -132,6 +133,6 @@ private static String escapeForAttributeSelector(Object value, boolean exact) { } private static String toJsRegExp(Pattern pattern) { - return "/" + pattern.pattern() + "/" + toJsRegexFlags(pattern); + return "/" + toJsRegexSource(pattern) + "/" + toJsRegexFlags(pattern); } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java index bf50fda02..37b6df2b3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java @@ -38,6 +38,7 @@ import java.util.regex.Pattern; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; +import static com.microsoft.playwright.impl.Utils.toJsRegexSource; import static com.microsoft.playwright.impl.Utils.fromJsRegexFlags; class Serialization { @@ -176,7 +177,7 @@ private SerializedValue serializeValue(Object value) { result.bi = ((BigInteger)value).toString(); } else if (value instanceof Pattern) { result.r = new SerializedValue.R(); - result.r.p = ((Pattern)value).pattern(); + result.r.p = toJsRegexSource((Pattern)value); result.r.f = toJsRegexFlags(((Pattern)value)); } else if (value instanceof Exception) { Exception exception = (Exception) value; @@ -382,7 +383,7 @@ static void addHarUrlFilter(JsonObject options, Object urlFilter) { options.addProperty("urlGlob", (String) urlFilter); } else if (urlFilter instanceof Pattern) { Pattern pattern = (Pattern) urlFilter; - options.addProperty("urlRegexSource", pattern.pattern()); + options.addProperty("urlRegexSource", toJsRegexSource(pattern)); options.addProperty("urlRegexFlags", toJsRegexFlags(pattern)); } } @@ -400,7 +401,7 @@ private static void setStringOrRegex(JsonObject params, String name, Object valu params.addProperty(name, (String) value); } else if (value instanceof Pattern) { Pattern pattern = (Pattern) value; - params.addProperty(name + "RegexSource", pattern.pattern()); + params.addProperty(name + "RegexSource", toJsRegexSource(pattern)); params.addProperty(name + "RegexFlags", toJsRegexFlags(pattern)); } } 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 c19b08a84..4653f799e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java @@ -25,6 +25,7 @@ import java.util.regex.Pattern; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; +import static com.microsoft.playwright.impl.Utils.toJsRegexSource; class UrlMatcher { public final String glob; @@ -128,7 +129,7 @@ public String toString() { if (glob != null) return String.format("", glob); if (pattern != null) - return String.format("", pattern.pattern(), toJsRegexFlags(pattern)); + return String.format("", toJsRegexSource(pattern), toJsRegexFlags(pattern)); if (this.predicate != null) return ""; return ""; 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 2a3f8d652..4860e08df 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -324,6 +324,41 @@ static String toJsRegexFlags(Pattern pattern) { return regexFlags; } + // JavaScript regular expressions do not support \Q...\E quoting, so convert quoted sections into escaped literals. + static String toJsRegexSource(Pattern pattern) { + String source = pattern.pattern(); + if (!source.contains("\\Q")) { + return source; + } + StringBuilder result = new StringBuilder(); + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + if (c != '\\' || i + 1 == source.length()) { + result.append(c); + continue; + } + char next = source.charAt(++i); + if (next != 'Q') { + result.append(c).append(next); + continue; + } + int end = source.indexOf("\\E", i + 1); + if (end == -1) { + end = source.length(); + } + for (int j = i + 1; j < end; j++) { + char ch = source.charAt(j); + if ("^$\\.*+?()[]{}|/".indexOf(ch) != -1) { + result.append('\\'); + } + result.append(ch); + } + // Skip past the closing \E. + i = end + 1; + } + return result.toString(); + } + static int fromJsRegexFlags(String regexFlags) { int flags = 0; if (regexFlags.contains("i")) { @@ -409,7 +444,7 @@ static JsonObject interceptionPatterns(List matchers) { if (matcher.glob != null) { jsonPattern.addProperty("glob", matcher.glob); } else if (matcher.pattern != null) { - jsonPattern.addProperty("regexSource", matcher.pattern.pattern()); + jsonPattern.addProperty("regexSource", toJsRegexSource(matcher.pattern)); jsonPattern.addProperty("regexFlags", toJsRegexFlags(matcher.pattern)); } else { // Match all requests. diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRegexQuote.java b/playwright/src/test/java/com/microsoft/playwright/TestRegexQuote.java new file mode 100644 index 000000000..79d146942 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestRegexQuote.java @@ -0,0 +1,86 @@ +/* + * 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.Test; + +import java.util.regex.Pattern; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; + +// Java's \Q...\E quoting (Pattern.quote) is not supported by JavaScript regular expressions. +public class TestRegexQuote extends TestBase { + @Test + void shouldFilterByQuotedPattern() { + page.setContent("
Hello Playwright
Hello world
"); + Locator locator = page.locator("div").filter(new Locator.FilterOptions().setHasText(Pattern.compile(Pattern.quote("Playwright")))); + assertThat(locator).hasCount(1); + assertThat(locator).hasText("Hello Playwright"); + } + + @Test + void shouldEscapeRegexSpecialCharactersInQuotedPattern() { + page.setContent("
a.b(c)
aXbc
"); + Locator locator = page.locator("div").filter(new Locator.FilterOptions().setHasText(Pattern.compile(Pattern.quote("a.b(c)")))); + assertThat(locator).hasCount(1); + assertThat(locator).hasText("a.b(c)"); + assertThat(page.getByText(Pattern.compile("^" + Pattern.quote("a.b(c)") + "$"))).hasCount(1); + } + + @Test + void shouldHandleQuotedPatternContainingQuoteEnd() { + page.setContent("
x\\Ey[1+1]/
xEy
"); + Pattern pattern = Pattern.compile(Pattern.quote("x\\Ey[1+1]/")); + assertThat(page.locator("div").filter(new Locator.FilterOptions().setHasText(pattern))).hasCount(1); + assertThat(page.locator("div").first()).hasText(pattern); + assertThat(page.locator("div").last()).not().hasText(pattern); + } + + @Test + void shouldAssertTextWithQuotedPattern() { + page.setContent("
Price: $9.99 (USD)
"); + assertThat(page.locator("div")).hasText(Pattern.compile(Pattern.quote("$9.99 (USD)"))); + assertThat(page.locator("div")).hasText(Pattern.compile("^Price: " + Pattern.quote("$9.99") + "\\s+\\(USD\\)$")); + assertThat(page.locator("div")).not().hasText(Pattern.compile(Pattern.quote("$9x99"))); + } + + @Test + void shouldNotTreatEscapedBackslashFollowedByQAsQuote() { + page.setContent("
a\\Qb
"); + assertThat(page.locator("div")).hasText(Pattern.compile("a\\\\Qb")); + } + + @Test + void shouldSerializeQuotedPatternAsEvaluateArgument() { + Object result = page.evaluate("r => [r.source, r.test('a.b(c)'), r.test('aXbc')]", Pattern.compile(Pattern.quote("a.b(c)"))); + assertEquals(asList("a\\.b\\(c\\)", true, false), result); + } + + @Test + void shouldRouteWithQuotedPattern() { + boolean[] intercepted = {false}; + page.route(Pattern.compile(Pattern.quote(server.EMPTY_PAGE)), route -> { + intercepted[0] = true; + route.fulfill(new Route.FulfillOptions().setBody("intercepted")); + }); + page.navigate(server.EMPTY_PAGE); + assertTrue(intercepted[0]); + assertThat(page.locator("body")).hasText("intercepted"); + } +}