Skip to content

Commit

Permalink
chore(roll): roll to new upstream RegExp / str escape logic (#2080)
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt authored Sep 18, 2023
1 parent 7873afd commit e576033
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 12 deletions.
2 changes: 1 addition & 1 deletion playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ def _on_dialog(self, dialog: Dialog) -> None:
else:
asyncio.create_task(dialog.dismiss())

async def _on_page_error(self, error: Error, page: Optional[Page]) -> None:
def _on_page_error(self, error: Error, page: Optional[Page]) -> None:
self.emit(BrowserContext.Events.WebError, WebError(self._loop, page, error))
if page:
page.emit(Page.Events.PageError, error)
Expand Down
9 changes: 1 addition & 8 deletions playwright/_impl/_locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
from playwright._impl._str_utils import (
escape_for_attribute_selector,
escape_for_text_selector,
escape_regex_flags,
)

if sys.version_info >= (3, 8): # pragma: no cover
Expand Down Expand Up @@ -847,16 +846,12 @@ def set_test_id_attribute_name(attribute_name: str) -> None:
def get_by_test_id_selector(
test_id_attribute_name: str, test_id: Union[str, Pattern[str]]
) -> str:
if isinstance(test_id, Pattern):
return f"internal:testid=[{test_id_attribute_name}=/{test_id.pattern}/{escape_regex_flags(test_id)}]"
return f"internal:testid=[{test_id_attribute_name}={escape_for_attribute_selector(test_id, True)}]"


def get_by_attribute_text_selector(
attr_name: str, text: Union[str, Pattern[str]], exact: bool = None
) -> str:
if isinstance(text, Pattern):
return f"internal:attr=[{attr_name}=/{text.pattern}/{escape_regex_flags(text)}]"
return f"internal:attr=[{attr_name}={escape_for_attribute_selector(text, exact=exact)}]"


Expand Down Expand Up @@ -915,9 +910,7 @@ def get_by_role_selector(
props.append(
(
"name",
f"/{name.pattern}/{escape_regex_flags(name)}"
if isinstance(name, Pattern)
else escape_for_attribute_selector(name, exact),
escape_for_attribute_selector(name, exact=exact),
)
)
if pressed is not None:
Expand Down
20 changes: 18 additions & 2 deletions playwright/_impl/_str_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,31 @@ def escape_for_regex(text: str) -> str:
return re.sub(r"[.*+?^>${}()|[\]\\]", "\\$&", text)


def escape_regex_for_selector(text: Pattern) -> str:
# Even number of backslashes followed by the quote -> insert a backslash.
return (
"/"
+ re.sub(r'(^|[^\\])(\\\\)*(["\'`])', r"\1\2\\\3", text.pattern).replace(
">>", "\\>\\>"
)
+ "/"
+ escape_regex_flags(text)
)


def escape_for_text_selector(
text: Union[str, Pattern[str]], exact: bool = None, case_sensitive: bool = None
) -> str:
if isinstance(text, Pattern):
return f"/{text.pattern}/{escape_regex_flags(text)}"
return escape_regex_for_selector(text)
return json.dumps(text) + ("s" if exact else "i")


def escape_for_attribute_selector(value: str, exact: bool = None) -> str:
def escape_for_attribute_selector(
value: Union[str, Pattern], exact: bool = None
) -> str:
if isinstance(value, Pattern):
return escape_regex_for_selector(value)
# TODO: this should actually be
# cssEscape(value).replace(/\\ /g, ' ')
# However, our attribute selectors do not conform to CSS parsing spec,
Expand Down
38 changes: 38 additions & 0 deletions tests/async/test_selectors_get_by.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import re

from playwright.async_api import Page, expect


Expand Down Expand Up @@ -87,6 +89,42 @@ async def test_get_by_escaping(page: Page) -> None:
0, timeout=500
)

await page.set_content(
"""<label for=target>foo &gt;&gt; bar</label><input id=target>"""
)
await page.eval_on_selector(
"input",
"""input => {
input.setAttribute('placeholder', 'foo >> bar');
input.setAttribute('title', 'foo >> bar');
input.setAttribute('alt', 'foo >> bar');
}""",
)
assert await page.get_by_text("foo >> bar").text_content() == "foo >> bar"
await expect(page.locator("label")).to_have_text("foo >> bar")
await expect(page.get_by_text("foo >> bar")).to_have_text("foo >> bar")
assert (
await page.get_by_text(re.compile("foo >> bar")).text_content() == "foo >> bar"
)
await expect(page.get_by_label("foo >> bar")).to_have_attribute("id", "target")
await expect(page.get_by_label(re.compile("foo >> bar"))).to_have_attribute(
"id", "target"
)
await expect(page.get_by_placeholder("foo >> bar")).to_have_attribute(
"id", "target"
)
await expect(page.get_by_alt_text("foo >> bar")).to_have_attribute("id", "target")
await expect(page.get_by_title("foo >> bar")).to_have_attribute("id", "target")
await expect(page.get_by_placeholder(re.compile("foo >> bar"))).to_have_attribute(
"id", "target"
)
await expect(page.get_by_alt_text(re.compile("foo >> bar"))).to_have_attribute(
"id", "target"
)
await expect(page.get_by_title(re.compile("foo >> bar"))).to_have_attribute(
"id", "target"
)


async def test_get_by_role_escaping(
page: Page,
Expand Down
237 changes: 236 additions & 1 deletion tests/async/test_selectors_text.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re

from playwright.async_api import Page, expect
import pytest

from playwright.async_api import Error, Page, expect


async def test_has_text_and_internal_text_should_match_full_node_text_in_strict_mode(
Expand Down Expand Up @@ -33,3 +35,236 @@ async def test_has_text_and_internal_text_should_match_full_node_text_in_strict_
"div1"
)
await expect(page.locator("div", has_text=re.compile("^hello$"))).to_have_id("div2")


async def test_should_work(page: Page, server) -> None:
await page.set_content(
"""
<div>yo</div><div>ya</div><div>\nye </div>
"""
)
assert await page.eval_on_selector("text=ya", "e => e.outerHTML") == "<div>ya</div>"
assert (
await page.eval_on_selector('text="ya"', "e => e.outerHTML") == "<div>ya</div>"
)
assert (
await page.eval_on_selector("text=/^[ay]+$/", "e => e.outerHTML")
== "<div>ya</div>"
)
assert (
await page.eval_on_selector("text=/Ya/i", "e => e.outerHTML") == "<div>ya</div>"
)
assert (
await page.eval_on_selector("text=ye", "e => e.outerHTML")
== "<div>\nye </div>"
)
assert ">\nye </div>" in await page.get_by_text("ye").evaluate("e => e.outerHTML")

await page.set_content(
"""
<div> ye </div><div>ye</div>
"""
)
assert (
await page.eval_on_selector('text="ye"', "e => e.outerHTML")
== "<div> ye </div>"
)
assert "> ye </div>" in await page.get_by_text("ye", exact=True).first.evaluate(
"e => e.outerHTML"
)

await page.set_content(
"""
<div>yo</div><div>"ya</div><div> hello world! </div>
"""
)
assert (
await page.eval_on_selector('text="\\"ya"', "e => e.outerHTML")
== '<div>"ya</div>'
)
assert (
await page.eval_on_selector("text=/hello/", "e => e.outerHTML")
== "<div> hello world! </div>"
)
assert (
await page.eval_on_selector("text=/^\\s*heLLo/i", "e => e.outerHTML")
== "<div> hello world! </div>"
)

await page.set_content(
"""
<div>yo<div>ya</div>hey<div>hey</div></div>
"""
)
assert (
await page.eval_on_selector("text=hey", "e => e.outerHTML") == "<div>hey</div>"
)
assert (
await page.eval_on_selector('text=yo>>text="ya"', "e => e.outerHTML")
== "<div>ya</div>"
)
assert (
await page.eval_on_selector('text=yo>> text="ya"', "e => e.outerHTML")
== "<div>ya</div>"
)
assert (
await page.eval_on_selector("text=yo >>text='ya'", "e => e.outerHTML")
== "<div>ya</div>"
)
assert (
await page.eval_on_selector("text=yo >> text='ya'", "e => e.outerHTML")
== "<div>ya</div>"
)
assert (
await page.eval_on_selector("'yo'>>\"ya\"", "e => e.outerHTML")
== "<div>ya</div>"
)
assert (
await page.eval_on_selector("\"yo\" >> 'ya'", "e => e.outerHTML")
== "<div>ya</div>"
)

await page.set_content(
"""
<div>yo<span id="s1"></span></div><div>yo<span id="s2"></span><span id="s3"></span></div>
"""
)
assert (
await page.eval_on_selector_all(
"text=yo", "es => es.map(e => e.outerHTML).join('\\n')"
)
== '<div>yo<span id="s1"></span></div>\n<div>yo<span id="s2"></span><span id="s3"></span></div>'
)

await page.set_content("<div>'</div><div>\"</div><div>\\</div><div>x</div>")
assert (
await page.eval_on_selector("text='\\''", "e => e.outerHTML") == "<div>'</div>"
)
assert (
await page.eval_on_selector("text='\"'", "e => e.outerHTML") == '<div>"</div>'
)
assert (
await page.eval_on_selector('text="\\""', "e => e.outerHTML") == '<div>"</div>'
)
assert (
await page.eval_on_selector('text="\'"', "e => e.outerHTML") == "<div>'</div>"
)
assert (
await page.eval_on_selector('text="\\x"', "e => e.outerHTML") == "<div>x</div>"
)
assert (
await page.eval_on_selector("text='\\x'", "e => e.outerHTML") == "<div>x</div>"
)
assert (
await page.eval_on_selector("text='\\\\'", "e => e.outerHTML")
== "<div>\\</div>"
)
assert (
await page.eval_on_selector('text="\\\\"', "e => e.outerHTML")
== "<div>\\</div>"
)
assert await page.eval_on_selector('text="', "e => e.outerHTML") == '<div>"</div>'
assert await page.eval_on_selector("text='", "e => e.outerHTML") == "<div>'</div>"
assert await page.eval_on_selector('"x"', "e => e.outerHTML") == "<div>x</div>"
assert await page.eval_on_selector("'x'", "e => e.outerHTML") == "<div>x</div>"
with pytest.raises(Error):
await page.query_selector_all('"')
with pytest.raises(Error):
await page.query_selector_all("'")

await page.set_content("<div> ' </div><div> \" </div>")
assert await page.eval_on_selector('text="', "e => e.outerHTML") == '<div> " </div>'
assert await page.eval_on_selector("text='", "e => e.outerHTML") == "<div> ' </div>"

await page.set_content("<div>Hi''&gt;&gt;foo=bar</div>")
assert (
await page.eval_on_selector("text=\"Hi''>>foo=bar\"", "e => e.outerHTML")
== "<div>Hi''&gt;&gt;foo=bar</div>"
)
await page.set_content("<div>Hi'\"&gt;&gt;foo=bar</div>")
assert (
await page.eval_on_selector('text="Hi\'\\">>foo=bar"', "e => e.outerHTML")
== "<div>Hi'\"&gt;&gt;foo=bar</div>"
)

await page.set_content("<div>Hi&gt;&gt;<span></span></div>")
assert (
await page.eval_on_selector('text="Hi>>">>span', "e => e.outerHTML")
== "<span></span>"
)
assert (
await page.eval_on_selector("text=/Hi\\>\\>/ >> span", "e => e.outerHTML")
== "<span></span>"
)

await page.set_content("<div>a<br>b</div><div>a</div>")
assert (
await page.eval_on_selector("text=a", "e => e.outerHTML") == "<div>a<br>b</div>"
)
assert (
await page.eval_on_selector("text=b", "e => e.outerHTML") == "<div>a<br>b</div>"
)
assert (
await page.eval_on_selector("text=ab", "e => e.outerHTML")
== "<div>a<br>b</div>"
)
assert await page.query_selector("text=abc") is None
assert await page.eval_on_selector_all("text=a", "els => els.length") == 2
assert await page.eval_on_selector_all("text=b", "els => els.length") == 1
assert await page.eval_on_selector_all("text=ab", "els => els.length") == 1
assert await page.eval_on_selector_all("text=abc", "els => els.length") == 0

await page.set_content("<div></div><span></span>")
await page.eval_on_selector(
"div",
"""div => {
div.appendChild(document.createTextNode('hello'))
div.appendChild(document.createTextNode('world'))
}""",
)
await page.eval_on_selector(
"span",
"""span => {
span.appendChild(document.createTextNode('hello'))
span.appendChild(document.createTextNode('world'))
}""",
)
assert (
await page.eval_on_selector("text=lowo", "e => e.outerHTML")
== "<div>helloworld</div>"
)
assert (
await page.eval_on_selector_all(
"text=lowo", "els => els.map(e => e.outerHTML).join('')"
)
== "<div>helloworld</div><span>helloworld</span>"
)

await page.set_content("<span>Sign&nbsp;in</span><span>Hello\n \nworld</span>")
assert (
await page.eval_on_selector("text=Sign in", "e => e.outerHTML")
== "<span>Sign&nbsp;in</span>"
)
assert len((await page.query_selector_all("text=Sign \tin"))) == 1
assert len((await page.query_selector_all('text="Sign in"'))) == 1
assert (
await page.eval_on_selector("text=lo wo", "e => e.outerHTML")
== "<span>Hello\n \nworld</span>"
)
assert (
await page.eval_on_selector('text="Hello world"', "e => e.outerHTML")
== "<span>Hello\n \nworld</span>"
)
assert await page.query_selector('text="lo wo"') is None
assert len((await page.query_selector_all("text=lo \nwo"))) == 1
assert len(await page.query_selector_all('text="lo \nwo"')) == 0

await page.set_content("<div>let's<span>hello</span></div>")
assert (
await page.eval_on_selector("text=/let's/i >> span", "e => e.outerHTML")
== "<span>hello</span>"
)
assert (
await page.eval_on_selector("text=/let\\'s/i >> span", "e => e.outerHTML")
== "<span>hello</span>"
)

0 comments on commit e576033

Please sign in to comment.