diff --git a/README.md b/README.md index 081734543..ae6c59054 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 113.0.5672.53 | ✅ | ✅ | ✅ | +| Chromium 114.0.5735.35 | ✅ | ✅ | ✅ | | WebKit 16.4 | ✅ | ✅ | ✅ | | Firefox 113.0 | ✅ | ✅ | ✅ | diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 5678441b4..8b7df9722 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -478,6 +478,13 @@ async def wait_for_event( pass return await event_info + def expect_console_message( + self, + predicate: Callable[[ConsoleMessage], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl[ConsoleMessage]: + return self.expect_event(Page.Events.Console, predicate, timeout) + def expect_page( self, predicate: Callable[[Page], bool] = None, diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 7ff612aec..b3eabb16f 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -13733,6 +13733,38 @@ async def wait_for_event( ) ) + def expect_console_message( + self, + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None + ) -> AsyncEventContextManager["ConsoleMessage"]: + """BrowserContext.expect_console_message + + Performs action and waits for a `ConsoleMessage` to be logged by in the pages in the context. If predicate is + provided, it passes `ConsoleMessage` value into the `predicate` function and waits for `predicate(message)` to + return a truthy value. Will throw an error if the page is closed before the `browser_context.on('console')` event + is fired. + + Parameters + ---------- + predicate : Union[Callable[[ConsoleMessage], bool], None] + Receives the `ConsoleMessage` object and resolves to truthy value when the waiting should resolve. + timeout : Union[float, None] + Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The + default value can be changed by using the `browser_context.set_default_timeout()`. + + Returns + ------- + EventContextManager[ConsoleMessage] + """ + + return AsyncEventContextManager( + self._impl_obj.expect_console_message( + predicate=self._wrap_handler(predicate), timeout=timeout + ).future + ) + def expect_page( self, predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 7be26802b..ce94063be 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -13799,6 +13799,38 @@ def wait_for_event( ) ) + def expect_console_message( + self, + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None + ) -> EventContextManager["ConsoleMessage"]: + """BrowserContext.expect_console_message + + Performs action and waits for a `ConsoleMessage` to be logged by in the pages in the context. If predicate is + provided, it passes `ConsoleMessage` value into the `predicate` function and waits for `predicate(message)` to + return a truthy value. Will throw an error if the page is closed before the `browser_context.on('console')` event + is fired. + + Parameters + ---------- + predicate : Union[Callable[[ConsoleMessage], bool], None] + Receives the `ConsoleMessage` object and resolves to truthy value when the waiting should resolve. + timeout : Union[float, None] + Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The + default value can be changed by using the `browser_context.set_default_timeout()`. + + Returns + ------- + EventContextManager[ConsoleMessage] + """ + return EventContextManager( + self, + self._impl_obj.expect_console_message( + predicate=self._wrap_handler(predicate), timeout=timeout + ).future, + ) + def expect_page( self, predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, diff --git a/setup.py b/setup.py index 4ce9c812a..93a9caa17 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.34.0-alpha-may-17-2023" +driver_version = "1.34.3" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py index 5966e3e79..ec3b7e230 100644 --- a/tests/async/test_browsercontext.py +++ b/tests/async/test_browsercontext.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio +import re from urllib.parse import urlparse import pytest @@ -477,13 +478,13 @@ def handler(route, request, ordinal): def handler4(route, request): handler(route, request, 4) - await context.route("**/empty.html", handler4) + await context.route(re.compile("empty.html"), handler4) await page.goto(server.EMPTY_PAGE) assert intercepted == [4] intercepted = [] - await context.unroute("**/empty.html", handler4) + await context.unroute(re.compile("empty.html"), handler4) await page.goto(server.EMPTY_PAGE) assert intercepted == [3] diff --git a/tests/async/test_browsercontext_events.py b/tests/async/test_browsercontext_events.py index da6ce191a..ff37015dc 100644 --- a/tests/async/test_browsercontext_events.py +++ b/tests/async/test_browsercontext_events.py @@ -180,3 +180,11 @@ def handle_route(request: HttpRequestWithPostBody) -> None: await dialog.accept("hello") await promise await popup.evaluate("window.result") == "hello" + + +async def test_console_event_should_work_with_context_manager(page: Page) -> None: + async with page.context.expect_console_message() as cm_info: + await page.evaluate("() => console.log('hello')") + message = await cm_info.value + assert message.text == "hello" + assert message.page == page diff --git a/tests/async/test_interception.py b/tests/async/test_interception.py index 439f68125..08a24273a 100644 --- a/tests/async/test_interception.py +++ b/tests/async/test_interception.py @@ -14,6 +14,7 @@ import asyncio import json +import re import pytest @@ -75,13 +76,13 @@ def handler4(route): intercepted.append(4) asyncio.create_task(route.continue_()) - await page.route("**/empty.html", handler4) + await page.route(re.compile("empty.html"), handler4) await page.goto(server.EMPTY_PAGE) assert intercepted == [4] intercepted = [] - await page.unroute("**/empty.html", handler4) + await page.unroute(re.compile("empty.html"), handler4) await page.goto(server.EMPTY_PAGE) assert intercepted == [3] diff --git a/tests/sync/test_browsercontext_events.py b/tests/sync/test_browsercontext_events.py new file mode 100644 index 000000000..6d0840e6a --- /dev/null +++ b/tests/sync/test_browsercontext_events.py @@ -0,0 +1,200 @@ +# 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. + +from typing import Optional + +import pytest + +from playwright.sync_api import Dialog, Page + +from ..server import HttpRequestWithPostBody, Server + + +def test_console_event_should_work(page: Page) -> None: + with page.context.expect_console_message() as console_info: + page.evaluate("() => console.log('hello')") + message = console_info.value + assert message.text == "hello" + assert message.page == page + + +def test_console_event_should_work_in_popup(page: Page) -> None: + with page.context.expect_console_message() as console_info: + with page.expect_popup() as popup_info: + page.evaluate( + """() => { + const win = window.open(''); + win.console.log('hello'); + }""" + ) + message = console_info.value + popup = popup_info.value + assert message.text == "hello" + assert message.page == popup + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +def test_console_event_should_work_in_popup_2(page: Page, browser_name: str) -> None: + with page.context.expect_console_message( + lambda msg: msg.type == "log" + ) as console_info: + with page.context.expect_page() as page_info: + page.evaluate( + """async () => { + const win = window.open('javascript:console.log("hello")'); + await new Promise(f => setTimeout(f, 0)); + win.close(); + }""" + ) + message = console_info.value + popup = page_info.value + assert message.text == "hello" + assert message.page == popup + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +def test_console_event_should_work_in_immediately_closed_popup( + page: Page, browser_name: str +) -> None: + with page.context.expect_console_message( + lambda msg: msg.type == "log" + ) as console_info: + with page.context.expect_page() as page_info: + page.evaluate( + """() => { + const win = window.open(''); + win.console.log('hello'); + win.close(); + }""" + ) + message = console_info.value + popup = page_info.value + assert message.text == "hello" + assert message.page == popup + + +def test_dialog_event_should_work1(page: Page) -> None: + dialog1: Optional[Dialog] = None + + def handle_page_dialog(dialog: Dialog) -> None: + nonlocal dialog1 + dialog1 = dialog + dialog.accept("hello") + + page.on("dialog", handle_page_dialog) + + dialog2: Optional[Dialog] = None + + def handle_context_dialog(dialog: Dialog) -> None: + nonlocal dialog2 + dialog2 = dialog + + page.context.on("dialog", handle_context_dialog) + + assert page.evaluate("() => prompt('hey?')") == "hello" + assert dialog1 + assert dialog1 == dialog2 + assert dialog1.message == "hey?" + assert dialog1.page == page + + +def test_dialog_event_should_work_in_popup1(page: Page) -> None: + dialog: Optional[Dialog] = None + + def handle_dialog(d: Dialog) -> None: + nonlocal dialog + dialog = d + dialog.accept("hello") + + page.context.on("dialog", handle_dialog) + + with page.expect_popup() as popup_info: + assert page.evaluate("() => window.open('').prompt('hey?')") == "hello" + popup = popup_info.value + assert dialog + assert dialog.message == "hey?" + assert dialog.page == popup + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +def test_dialog_event_should_work_in_popup_2(page: Page, browser_name: str) -> None: + def handle_dialog(dialog: Dialog) -> None: + assert dialog.message == "hey?" + assert dialog.page is None + dialog.accept("hello") + + page.context.on("dialog", handle_dialog) + + assert page.evaluate("() => window.open('javascript:prompt(\"hey?\")')") + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +def test_dialog_event_should_work_in_immdiately_closed_popup(page: Page) -> None: + popup = None + + def handle_popup(p: Page) -> None: + nonlocal popup + popup = p + + page.on("popup", handle_popup) + + with page.context.expect_console_message() as console_info: + page.evaluate( + """() => { + const win = window.open(); + win.console.log('hello'); + win.close(); + }""" + ) + message = console_info.value + + assert message.text == "hello" + assert message.page == popup + + +def test_dialog_event_should_work_with_inline_script_tag( + page: Page, server: Server +) -> None: + def handle_route(request: HttpRequestWithPostBody) -> None: + request.setHeader("content-type", "text/html") + request.write(b"""""") + request.finish() + + server.set_route("/popup.html", handle_route) + page.goto(server.EMPTY_PAGE) + page.set_content("Click me") + + def handle_dialog(dialog: Dialog) -> None: + assert dialog.message == "hey?" + assert dialog.page == popup + dialog.accept("hello") + + page.context.on("dialog", handle_dialog) + + with page.expect_popup() as popup_info: + page.click("a") + popup = popup_info.value + assert popup.evaluate("window.result") == "hello" + + +def test_console_event_should_work_with_context_manager(page: Page) -> None: + with page.context.expect_console_message() as cm_info: + page.evaluate("() => console.log('hello')") + message = cm_info.value + assert message.text == "hello" + assert message.page == page diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py index 8c00368d9..2c0455d57 100644 --- a/tests/sync/test_locators.py +++ b/tests/sync/test_locators.py @@ -863,6 +863,25 @@ def test_should_support_locator_filter(page: Page) -> None: expect(page.locator("div").filter(has_not_text="foo")).to_have_count(2) +def test_locators_should_support_locator_and(page: Page) -> None: + page.set_content( + """ +
hello
world
+ hello2world2 + """ + ) + expect(page.locator("div").and_(page.locator("div"))).to_have_count(2) + expect(page.locator("div").and_(page.get_by_test_id("foo"))).to_have_text(["hello"]) + expect(page.locator("div").and_(page.get_by_test_id("bar"))).to_have_text(["world"]) + expect(page.get_by_test_id("foo").and_(page.locator("div"))).to_have_text(["hello"]) + expect(page.get_by_test_id("bar").and_(page.locator("span"))).to_have_text( + ["world2"] + ) + expect( + page.locator("span").and_(page.get_by_test_id(re.compile("bar|foo"))) + ).to_have_count(2) + + def test_locators_has_does_not_encode_unicode(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) locators = [ diff --git a/tests/sync/test_selectors_misc.py b/tests/sync/test_selectors_misc.py new file mode 100644 index 000000000..ad7ec16ea --- /dev/null +++ b/tests/sync/test_selectors_misc.py @@ -0,0 +1,54 @@ +# 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. + +from playwright.sync_api import Page + + +def test_should_work_with_internal_and(page: Page) -> None: + page.set_content( + """ +
hello
world
+ hello2world2 + """ + ) + assert ( + page.eval_on_selector_all( + 'div >> internal:and="span"', "els => els.map(e => e.textContent)" + ) + ) == [] + assert ( + page.eval_on_selector_all( + 'div >> internal:and=".foo"', "els => els.map(e => e.textContent)" + ) + ) == ["hello"] + assert ( + page.eval_on_selector_all( + 'div >> internal:and=".bar"', "els => els.map(e => e.textContent)" + ) + ) == ["world"] + assert ( + page.eval_on_selector_all( + 'span >> internal:and="span"', "els => els.map(e => e.textContent)" + ) + ) == ["hello2", "world2"] + assert ( + page.eval_on_selector_all( + '.foo >> internal:and="div"', "els => els.map(e => e.textContent)" + ) + ) == ["hello"] + assert ( + page.eval_on_selector_all( + '.bar >> internal:and="span"', "els => els.map(e => e.textContent)" + ) + ) == ["world2"]