diff --git a/README.md b/README.md index afb42a98..2fa79d45 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 117.0.5938.48 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 117.0.5938.62 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 17.0 | ✅ | ✅ | ✅ | | Firefox 117.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | 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 7985da9d..3129ba38 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java @@ -5,6 +5,7 @@ import com.microsoft.playwright.options.AriaRole; import java.util.regex.Pattern; +import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; public class LocatorUtils { @@ -25,10 +26,7 @@ public class LocatorUtils { } private static String getByAttributeTextSelector(String attrName, Object value, boolean exact) { - if (value instanceof Pattern) { - return "internal:attr=[" + attrName + "=" + toJsRegExp((Pattern) value) + "]"; - } - return "internal:attr=[" + attrName + "=" + escapeForAttributeSelector((String) value, exact) + "]"; + return "internal:attr=[" + attrName + "=" + escapeForAttributeSelector(value, exact) + "]"; } static String getByTestIdSelector(Object testId) { @@ -71,14 +69,7 @@ public class LocatorUtils { if (options.level != null) addAttr(result, "level", options.level.toString()); if (options.name != null) { - String name; - if (options.name instanceof String) { - name = escapeForAttributeSelector((String) options.name, options.exact != null && options.exact); - } else if (options.name instanceof Pattern) { - name = toJsRegExp((Pattern) options.name); - } else { - throw new IllegalArgumentException("options.name can be String or Pattern, found: " + options.name); - } + String name = escapeForAttributeSelector(options.name, options.exact != null && options.exact); addAttr(result, "name", name); } if (options.pressed != null) @@ -87,38 +78,33 @@ public class LocatorUtils { return result.toString(); } - static String escapeForTextSelector(Object text, boolean exact) { - return escapeForTextSelector(text, exact, false); + private static String escapeRegexForSelector(Pattern re) { + // Even number of backslashes followed by the quote -> insert a backslash. + return toJsRegExp(re).replaceAll("(^|[^\\\\])(\\\\\\\\)*([\"'`])", "$1$2\\\\$3").replaceAll(">>", "\\\\>\\\\>"); } - private static String escapeForTextSelector(Object param, boolean exact, boolean caseSensitive) { - if (param instanceof Pattern) { - return toJsRegExp((Pattern) param); + static String escapeForTextSelector(Object value, boolean exact) { + if (value instanceof Pattern) { + return escapeRegexForSelector((Pattern) value); } - if (!(param instanceof String)) { - throw new IllegalArgumentException("text parameter must be Pattern or String: " + param); + if (value instanceof String) { + return gson().toJson(value) + (exact ? "s" : "i"); } - String text = (String) param; - if (exact) { - return '"' + text.replace("\"", "\\\"") + '"'; - } - - if (text.contains("\"") || text.contains(">>") || text.startsWith("/")) { - return "/" + escapeForRegex(text).replaceAll("\\s+", "\\\\s+") + "/" + (caseSensitive ? "" : "i"); - } - return text; + throw new IllegalArgumentException("text parameter must be Pattern or String: " + value); } - private static String escapeForRegex(String text) { - return text.replaceAll("[.*+?^>${}()|\\[\\]\\\\]", "\\\\\\\\$0"); - } - - private static String escapeForAttributeSelector(String value, boolean exact) { - // TODO: this should actually be - // cssEscape(value).replace(/\\ /g, ' ') - // However, our attribute selectors do not conform to CSS parsing spec, - // so we escape them differently. - return '"' + value.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"") + '"' + (exact ? "" : "i"); + private static String escapeForAttributeSelector(Object value, boolean exact) { + if (value instanceof Pattern) { + return escapeRegexForSelector((Pattern) value); + } + if (value instanceof String) { + // TODO: this should actually be + // cssEscape(value).replace(/\\ /g, ' ') + // However, our attribute selectors do not conform to CSS parsing spec, + // so we escape them differently. + return '"' + ((String) value).replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"") + '"' + (exact ? "" : "i"); + } + throw new IllegalArgumentException("Attribute can be String or Pattern, found: " + value); } private static String toJsRegExp(Pattern pattern) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java b/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java index 30581c07..66e46d93 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java @@ -107,6 +107,39 @@ public class TestPageLocatorQuery extends TestBase { assertEquals("Hello \"world\"", page.locator("div", new Page.LocatorOptions().setHasText(Pattern.compile("Hello \"world\""))).textContent()); } + @Test + void shouldFilterByRegexWithASingleQuote() { + page.setContent(""); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let's", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let's", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let['abc]s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let['abc]s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\\\\'s", Pattern.CASE_INSENSITIVE)))).not().isVisible(); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\\\\'s", Pattern.CASE_INSENSITIVE)))).not().isVisible(); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let's let\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let's let\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\\'s let's", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\\'s let's", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + + page.setContent(""); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\\'s", Pattern.CASE_INSENSITIVE)))).not().isVisible(); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\\'s", Pattern.CASE_INSENSITIVE)))).not().isVisible(); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\\\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\\\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\\\\'s let\\\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\\\\'s let\\\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("let\\\\\\'s let\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + assertThat(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("let\\\\\\'s let\\\\'s", Pattern.CASE_INSENSITIVE))).locator("span")).hasText("hello"); + } + @Test void shouldFilterByRegexAndRegexpFlags() { page.setContent("
Hello \"world\"
Hello world
"); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java index c261a2f9..fb6a8245 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java @@ -153,6 +153,25 @@ public class TestSelectorsGetBy extends TestBase { assertThat(page.getByTitle("my title", new Page.GetByTitleOptions().setExact(true))).hasCount(1, new LocatorAssertions.HasCountOptions().setTimeout(500)); assertThat(page.getByTitle("my t\\itle", new Page.GetByTitleOptions().setExact(true))).hasCount(0, new LocatorAssertions.HasCountOptions().setTimeout(500)); assertThat(page.getByTitle("my t\\\\itle", new Page.GetByTitleOptions().setExact(true))).hasCount(0, new LocatorAssertions.HasCountOptions().setTimeout(500)); + + page.setContent(""); + page.evalOnSelector("input", "input => {\n" + + " input.setAttribute('placeholder', 'foo >> bar');\n" + + " input.setAttribute('title', 'foo >> bar');\n" + + " input.setAttribute('alt', 'foo >> bar');\n" + + " }"); + assertEquals("foo >> bar", page.getByText("foo >> bar").textContent()); + assertThat(page.locator("label")).hasText("foo >> bar"); + assertThat(page.getByText("foo >> bar")).hasText("foo >> bar"); + assertEquals("foo >> bar", page.getByText(Pattern.compile("foo >> bar")).textContent()); + assertThat(page.getByLabel("foo >> bar")).hasAttribute("id", "target"); + assertThat(page.getByLabel(Pattern.compile("foo >> bar"))).hasAttribute("id", "target"); + assertThat(page.getByPlaceholder("foo >> bar")).hasAttribute("id", "target"); + assertThat(page.getByAltText("foo >> bar")).hasAttribute("id", "target"); + assertThat(page.getByTitle("foo >> bar")).hasAttribute("id", "target"); + assertThat(page.getByPlaceholder(Pattern.compile("foo >> bar"))).hasAttribute("id", "target"); + assertThat(page.getByAltText(Pattern.compile("foo >> bar"))).hasAttribute("id", "target"); + assertThat(page.getByTitle(Pattern.compile("foo >> bar"))).hasAttribute("id", "target"); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsText.java b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsText.java index 328954d5..67dddcde 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsText.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsText.java @@ -9,6 +9,18 @@ import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSelectorsText extends TestBase { + + @Test + void shouldWorkSmoke() { + page.setContent("
Hi>>
"); + assertEquals("", page.evalOnSelector("text=\"Hi>>\">>span", "e => e.outerHTML")); + assertEquals("", page.evalOnSelector("text=/Hi\\>\\>/ >> span", "e => e.outerHTML")); + + page.setContent("
let'shello
"); + assertEquals("hello", page.evalOnSelector("text=/let's/i >> span", "e => e.outerHTML")); + assertEquals("hello", page.evalOnSelector("text=/let\'s/i >> span", "e => e.outerHTML")); + } + @Test void hasTextAndInternalTextShouldMatchFullNodeTextInStrictMode() { page.setContent("
helloworld
\n" + diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 4e71adb2..ebeef2f2 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.38.0-alpha-sep-10-2023 +1.38.0