Skip to content

Commit

Permalink
chore: update selector escaping, roll driver (#1385)
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s authored Sep 18, 2023
1 parent 30778a3 commit 85c5f90
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->117.0.5938.48<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->117.0.5938.62<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->17.0<!-- GEN:stop --> ||||
| Firefox <!-- GEN:firefox-version -->117.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import java.util.regex.Pattern;

import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;

public class LocatorUtils {
Expand All @@ -25,10 +26,7 @@ static String getByLabelSelector(Object text, Locator.GetByLabelOptions options)
}

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) {
Expand Down Expand Up @@ -71,14 +69,7 @@ static String getByRoleSelector(AriaRole role, Locator.GetByRoleOptions options)
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)
Expand All @@ -87,38 +78,33 @@ static String getByRoleSelector(AriaRole role, Locator.GetByRoleOptions options)
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);
}
if (!(param instanceof String)) {
throw new IllegalArgumentException("text parameter must be Pattern or String: " + param);
}
String text = (String) param;
if (exact) {
return '"' + text.replace("\"", "\\\"") + '"';
static String escapeForTextSelector(Object value, boolean exact) {
if (value instanceof Pattern) {
return escapeRegexForSelector((Pattern) value);
}

if (text.contains("\"") || text.contains(">>") || text.startsWith("/")) {
return "/" + escapeForRegex(text).replaceAll("\\s+", "\\\\s+") + "/" + (caseSensitive ? "" : "i");
if (value instanceof String) {
return gson().toJson(value) + (exact ? "s" : "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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,39 @@ void shouldFilterByRegexWithQuotes() {
assertEquals("Hello \"world\"", page.locator("div", new Page.LocatorOptions().setHasText(Pattern.compile("Hello \"world\""))).textContent());
}

@Test
void shouldFilterByRegexWithASingleQuote() {
page.setContent("<button>let's let's<span>hello</span></button>");
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("<button>let\\'s let\\'s<span>hello</span></button>");
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("<div>Hello \"world\"</div><div>Hello world</div>");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,25 @@ void getByEscaping() {
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("<label for=target>foo &gt;&gt; bar</label><input id=target>");
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
import static org.junit.jupiter.api.Assertions.assertEquals;

public class TestSelectorsText extends TestBase {

@Test
void shouldWorkSmoke() {
page.setContent("<div>Hi&gt;&gt;<span></span></div>");
assertEquals("<span></span>", page.evalOnSelector("text=\"Hi>>\">>span", "e => e.outerHTML"));
assertEquals("<span></span>", page.evalOnSelector("text=/Hi\\>\\>/ >> span", "e => e.outerHTML"));

page.setContent("<div>let's<span>hello</span></div>");
assertEquals("<span>hello</span>", page.evalOnSelector("text=/let's/i >> span", "e => e.outerHTML"));
assertEquals("<span>hello</span>", page.evalOnSelector("text=/let\'s/i >> span", "e => e.outerHTML"));
}

@Test
void hasTextAndInternalTextShouldMatchFullNodeTextInStrictMode() {
page.setContent("<div id=div1>hello<span>world</span></div>\n" +
Expand Down
2 changes: 1 addition & 1 deletion scripts/CLI_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.38.0-alpha-sep-10-2023
1.38.0

0 comments on commit 85c5f90

Please sign in to comment.