diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 4293f1220..ddb8ac41a 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -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) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 8c9a18f03..8a0c8282f 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -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 @@ -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)}]" @@ -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: diff --git a/playwright/_impl/_str_utils.py b/playwright/_impl/_str_utils.py index 769f530de..8b3e65a39 100644 --- a/playwright/_impl/_str_utils.py +++ b/playwright/_impl/_str_utils.py @@ -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, diff --git a/tests/async/test_selectors_get_by.py b/tests/async/test_selectors_get_by.py index 1a07d1a9a..718264b62 100644 --- a/tests/async/test_selectors_get_by.py +++ b/tests/async/test_selectors_get_by.py @@ -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 @@ -87,6 +89,42 @@ async def test_get_by_escaping(page: Page) -> None: 0, timeout=500 ) + await page.set_content( + """""" + ) + 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, diff --git a/tests/async/test_selectors_text.py b/tests/async/test_selectors_text.py index 1f09bdebd..9de7f6b4d 100644 --- a/tests/async/test_selectors_text.py +++ b/tests/async/test_selectors_text.py @@ -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( @@ -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( + """ +
yo
ya
\nye
+ """ + ) + assert await page.eval_on_selector("text=ya", "e => e.outerHTML") == "
ya
" + assert ( + await page.eval_on_selector('text="ya"', "e => e.outerHTML") == "
ya
" + ) + assert ( + await page.eval_on_selector("text=/^[ay]+$/", "e => e.outerHTML") + == "
ya
" + ) + assert ( + await page.eval_on_selector("text=/Ya/i", "e => e.outerHTML") == "
ya
" + ) + assert ( + await page.eval_on_selector("text=ye", "e => e.outerHTML") + == "
\nye
" + ) + assert ">\nye " in await page.get_by_text("ye").evaluate("e => e.outerHTML") + + await page.set_content( + """ +
ye
ye
+ """ + ) + assert ( + await page.eval_on_selector('text="ye"', "e => e.outerHTML") + == "
ye
" + ) + assert "> ye " in await page.get_by_text("ye", exact=True).first.evaluate( + "e => e.outerHTML" + ) + + await page.set_content( + """ +
yo
"ya
hello world!
+ """ + ) + assert ( + await page.eval_on_selector('text="\\"ya"', "e => e.outerHTML") + == '
"ya
' + ) + assert ( + await page.eval_on_selector("text=/hello/", "e => e.outerHTML") + == "
hello world!
" + ) + assert ( + await page.eval_on_selector("text=/^\\s*heLLo/i", "e => e.outerHTML") + == "
hello world!
" + ) + + await page.set_content( + """ +
yo
ya
hey
hey
+ """ + ) + assert ( + await page.eval_on_selector("text=hey", "e => e.outerHTML") == "
hey
" + ) + assert ( + await page.eval_on_selector('text=yo>>text="ya"', "e => e.outerHTML") + == "
ya
" + ) + assert ( + await page.eval_on_selector('text=yo>> text="ya"', "e => e.outerHTML") + == "
ya
" + ) + assert ( + await page.eval_on_selector("text=yo >>text='ya'", "e => e.outerHTML") + == "
ya
" + ) + assert ( + await page.eval_on_selector("text=yo >> text='ya'", "e => e.outerHTML") + == "
ya
" + ) + assert ( + await page.eval_on_selector("'yo'>>\"ya\"", "e => e.outerHTML") + == "
ya
" + ) + assert ( + await page.eval_on_selector("\"yo\" >> 'ya'", "e => e.outerHTML") + == "
ya
" + ) + + await page.set_content( + """ +
yo
yo
+ """ + ) + assert ( + await page.eval_on_selector_all( + "text=yo", "es => es.map(e => e.outerHTML).join('\\n')" + ) + == '
yo
\n
yo
' + ) + + await page.set_content("
'
\"
\\
x
") + assert ( + await page.eval_on_selector("text='\\''", "e => e.outerHTML") == "
'
" + ) + assert ( + await page.eval_on_selector("text='\"'", "e => e.outerHTML") == '
"
' + ) + assert ( + await page.eval_on_selector('text="\\""', "e => e.outerHTML") == '
"
' + ) + assert ( + await page.eval_on_selector('text="\'"', "e => e.outerHTML") == "
'
" + ) + assert ( + await page.eval_on_selector('text="\\x"', "e => e.outerHTML") == "
x
" + ) + assert ( + await page.eval_on_selector("text='\\x'", "e => e.outerHTML") == "
x
" + ) + assert ( + await page.eval_on_selector("text='\\\\'", "e => e.outerHTML") + == "
\\
" + ) + assert ( + await page.eval_on_selector('text="\\\\"', "e => e.outerHTML") + == "
\\
" + ) + assert await page.eval_on_selector('text="', "e => e.outerHTML") == '
"
' + assert await page.eval_on_selector("text='", "e => e.outerHTML") == "
'
" + assert await page.eval_on_selector('"x"', "e => e.outerHTML") == "
x
" + assert await page.eval_on_selector("'x'", "e => e.outerHTML") == "
x
" + with pytest.raises(Error): + await page.query_selector_all('"') + with pytest.raises(Error): + await page.query_selector_all("'") + + await page.set_content("
'
\"
") + assert await page.eval_on_selector('text="', "e => e.outerHTML") == '
"
' + assert await page.eval_on_selector("text='", "e => e.outerHTML") == "
'
" + + await page.set_content("
Hi''>>foo=bar
") + assert ( + await page.eval_on_selector("text=\"Hi''>>foo=bar\"", "e => e.outerHTML") + == "
Hi''>>foo=bar
" + ) + await page.set_content("
Hi'\">>foo=bar
") + assert ( + await page.eval_on_selector('text="Hi\'\\">>foo=bar"', "e => e.outerHTML") + == "
Hi'\">>foo=bar
" + ) + + await page.set_content("
Hi>>
") + assert ( + await page.eval_on_selector('text="Hi>>">>span', "e => e.outerHTML") + == "" + ) + assert ( + await page.eval_on_selector("text=/Hi\\>\\>/ >> span", "e => e.outerHTML") + == "" + ) + + await page.set_content("
a
b
a
") + assert ( + await page.eval_on_selector("text=a", "e => e.outerHTML") == "
a
b
" + ) + assert ( + await page.eval_on_selector("text=b", "e => e.outerHTML") == "
a
b
" + ) + assert ( + await page.eval_on_selector("text=ab", "e => e.outerHTML") + == "
a
b
" + ) + 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("
") + 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") + == "
helloworld
" + ) + assert ( + await page.eval_on_selector_all( + "text=lowo", "els => els.map(e => e.outerHTML).join('')" + ) + == "
helloworld
helloworld" + ) + + await page.set_content("Sign inHello\n \nworld") + assert ( + await page.eval_on_selector("text=Sign in", "e => e.outerHTML") + == "Sign in" + ) + 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") + == "Hello\n \nworld" + ) + assert ( + await page.eval_on_selector('text="Hello world"', "e => e.outerHTML") + == "Hello\n \nworld" + ) + 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("
let'shello
") + assert ( + await page.eval_on_selector("text=/let's/i >> span", "e => e.outerHTML") + == "hello" + ) + assert ( + await page.eval_on_selector("text=/let\\'s/i >> span", "e => e.outerHTML") + == "hello" + )