diff --git a/README.md b/README.md index 224349861..081734543 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | :--- | :---: | :---: | :---: | | Chromium 113.0.5672.53 | ✅ | ✅ | ✅ | | WebKit 16.4 | ✅ | ✅ | ✅ | -| Firefox 112.0 | ✅ | ✅ | ✅ | +| Firefox 113.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 4266098cc..2c499d5b1 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -167,11 +167,15 @@ async def new_page( recordHarContent: HarContentPolicy = None, ) -> Page: params = locals_to_params(locals()) - context = await self.new_context(**params) - page = await context.new_page() - page._owned_context = context - context._owner_page = page - return page + + async def inner() -> Page: + context = await self.new_context(**params) + page = await context.new_page() + page._owned_context = context + context._owner_page = page + return page + + return await self._connection.wrap_api_call(inner) async def close(self) -> None: if self._is_closed_or_closing: diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index f2787f862..5678441b4 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -44,6 +44,8 @@ from_channel, from_nullable_channel, ) +from playwright._impl._console_message import ConsoleMessage +from playwright._impl._dialog import Dialog from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._fetch import APIRequestContext from playwright._impl._frame import Frame @@ -82,6 +84,8 @@ class BrowserContext(ChannelOwner): Events = SimpleNamespace( BackgroundPage="backgroundpage", Close="close", + Console="console", + Dialog="dialog", Page="page", ServiceWorker="serviceworker", Request="request", @@ -136,6 +140,14 @@ def __init__( "serviceWorker", lambda params: self._on_service_worker(from_channel(params["worker"])), ) + self._channel.on( + "console", + lambda params: self._on_console_message(from_channel(params["message"])), + ) + + self._channel.on( + "dialog", lambda params: self._on_dialog(from_channel(params["dialog"])) + ) self._channel.on( "request", lambda params: self._on_request( @@ -174,6 +186,8 @@ def __init__( ) self._set_event_to_subscription_mapping( { + BrowserContext.Events.Console: "console", + BrowserContext.Events.Dialog: "dialog", BrowserContext.Events.Request: "request", BrowserContext.Events.Response: "response", BrowserContext.Events.RequestFinished: "requestFinished", @@ -507,6 +521,27 @@ def _on_request_finished( if response: response._finished_future.set_result(True) + def _on_console_message(self, message: ConsoleMessage) -> None: + self.emit(BrowserContext.Events.Console, message) + page = message.page + if page: + page.emit(Page.Events.Console, message) + + def _on_dialog(self, dialog: Dialog) -> None: + has_listeners = self.emit(BrowserContext.Events.Dialog, dialog) + page = dialog.page + if page: + has_listeners = page.emit(Page.Events.Dialog, dialog) or has_listeners + if not has_listeners: + # Although we do similar handling on the server side, we still need this logic + # on the client side due to a possible race condition between two async calls: + # a) removing "dialog" listener subscription (client->server) + # b) actual "dialog" event (server->client) + if dialog.type == "beforeunload": + asyncio.create_task(dialog.accept()) + else: + asyncio.create_task(dialog.dismiss()) + def _on_request(self, request: Request, page: Optional[Page]) -> None: self.emit(BrowserContext.Events.Request, request) if page: diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 1b693ff62..5f906c47e 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -161,8 +161,11 @@ def _set_event_to_subscription_mapping(self, mapping: Dict[str, str]) -> None: def _update_subscription(self, event: str, enabled: bool) -> None: protocol_event = self._event_to_subscription_mapping.get(event) if protocol_event: - self._channel.send_no_reply( - "updateSubscription", {"event": protocol_event, "enabled": enabled} + self._connection.wrap_api_call_sync( + lambda: self._channel.send_no_reply( + "updateSubscription", {"event": protocol_event, "enabled": enabled} + ), + True, ) def _add_event_handler(self, event: str, k: Any, v: Any) -> None: diff --git a/playwright/_impl/_console_message.py b/playwright/_impl/_console_message.py index dd19b40ce..9bed32ac8 100644 --- a/playwright/_impl/_console_message.py +++ b/playwright/_impl/_console_message.py @@ -12,18 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List +from typing import TYPE_CHECKING, Dict, List, Optional from playwright._impl._api_structures import SourceLocation -from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._connection import ( + ChannelOwner, + from_channel, + from_nullable_channel, +) from playwright._impl._js_handle import JSHandle +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + class ConsoleMessage(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + # Note: currently, we only report console messages for pages and they always have a page. + # However, in the future we might report console messages for service workers or something else, + # where page() would be null. + self._page: Optional["Page"] = from_nullable_channel(initializer.get("page")) def __repr__(self) -> str: return f"" @@ -46,3 +57,7 @@ def args(self) -> List[JSHandle]: @property def location(self) -> SourceLocation: return self._initializer["location"] + + @property + def page(self) -> Optional["Page"]: + return self._page diff --git a/playwright/_impl/_dialog.py b/playwright/_impl/_dialog.py index 585cfde75..a0c6ca77f 100644 --- a/playwright/_impl/_dialog.py +++ b/playwright/_impl/_dialog.py @@ -12,17 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict +from typing import TYPE_CHECKING, Dict, Optional -from playwright._impl._connection import ChannelOwner +from playwright._impl._connection import ChannelOwner, from_nullable_channel from playwright._impl._helper import locals_to_params +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + class Dialog(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + self._page: Optional["Page"] = from_nullable_channel(initializer.get("page")) def __repr__(self) -> str: return f"" @@ -39,6 +43,10 @@ def message(self) -> str: def default_value(self) -> str: return self._initializer["defaultValue"] + @property + def page(self) -> Optional["Page"]: + return self._page + async def accept(self, promptText: str = None) -> None: await self._channel.send("accept", locals_to_params(locals())) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index f5df9ca6b..416b09214 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -356,6 +356,14 @@ def or_(self, locator: "Locator") -> "Locator": self._selector + " >> internal:or=" + json.dumps(locator._selector), ) + def and_(self, locator: "Locator") -> "Locator": + if locator._frame != self._frame: + raise Error("Locators must belong to the same frame.") + return Locator( + self._frame, + self._selector + " >> internal:and=" + json.dumps(locator._selector), + ) + async def focus(self, timeout: float = None) -> None: params = locals_to_params(locals()) return await self._frame.focus(self._selector, strict=True, **params) diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 2414df934..75dd3b2e0 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -48,7 +48,6 @@ from_nullable_channel, ) from playwright._impl._console_message import ConsoleMessage -from playwright._impl._dialog import Dialog from playwright._impl._download import Download from playwright._impl._element_handle import ElementHandle from playwright._impl._event_context_manager import EventContextManagerImpl @@ -159,14 +158,7 @@ def __init__( lambda params: self._on_binding(from_channel(params["binding"])), ) self._channel.on("close", lambda _: self._on_close()) - self._channel.on( - "console", - lambda params: self.emit( - Page.Events.Console, from_channel(params["message"]) - ), - ) self._channel.on("crash", lambda _: self._on_crash()) - self._channel.on("dialog", lambda params: self._on_dialog(params)) self._channel.on("download", lambda params: self._on_download(params)) self._channel.on( "fileChooser", @@ -223,6 +215,8 @@ def __init__( self._set_event_to_subscription_mapping( { + Page.Events.Console: "console", + Page.Events.Dialog: "dialog", Page.Events.Request: "request", Page.Events.Response: "response", Page.Events.RequestFinished: "requestFinished", @@ -286,16 +280,6 @@ def _on_close(self) -> None: def _on_crash(self) -> None: self.emit(Page.Events.Crash, self) - def _on_dialog(self, params: Any) -> None: - dialog = cast(Dialog, from_channel(params["dialog"])) - if self.listeners(Page.Events.Dialog): - self.emit(Page.Events.Dialog, dialog) - else: - if dialog.type == "beforeunload": - asyncio.create_task(dialog.accept()) - else: - asyncio.create_task(dialog.dismiss()) - def _on_download(self, params: Any) -> None: url = params["url"] suggested_filename = params["suggestedFilename"] diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index fed10692d..7ff612aec 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -848,7 +848,7 @@ async def fallback( ```py # Handle GET requests. - def handle_post(route): + def handle_get(route): if route.request.method != \"GET\": route.fallback() return @@ -869,7 +869,7 @@ def handle_post(route): ```py # Handle GET requests. - def handle_post(route): + def handle_get(route): if route.request.method != \"GET\": route.fallback() return @@ -7091,6 +7091,18 @@ def location(self) -> SourceLocation: """ return mapping.from_impl(self._impl_obj.location) + @property + def page(self) -> typing.Optional["Page"]: + """ConsoleMessage.page + + The page that produced this console message, if any. + + Returns + ------- + Union[Page, None] + """ + return mapping.from_impl_nullable(self._impl_obj.page) + mapping.register(ConsoleMessageImpl, ConsoleMessage) @@ -7132,6 +7144,18 @@ def default_value(self) -> str: """ return mapping.from_maybe_impl(self._impl_obj.default_value) + @property + def page(self) -> typing.Optional["Page"]: + """Dialog.page + + The page that initiated this dialog, if available. + + Returns + ------- + Union[Page, None] + """ + return mapping.from_impl_nullable(self._impl_obj.page) + async def accept(self, prompt_text: typing.Optional[str] = None) -> None: """Dialog.accept @@ -7324,9 +7348,9 @@ def on( Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. - The arguments passed into `console.log` appear as arguments on the event handler. + The arguments passed into `console.log` are available on the `ConsoleMessage` event handler argument. - An example of handling `console` event: + **Usage** ```py async def print_args(msg): @@ -7336,7 +7360,7 @@ async def print_args(msg): print(values) page.on(\"console\", print_args) - await page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ``` ```py @@ -7345,7 +7369,7 @@ def print_args(msg): print(arg.json_value()) page.on(\"console\", print_args) - page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ```""" @typing.overload @@ -7392,12 +7416,14 @@ def on( [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and actions like click will never finish. + **Usage** + ```python page.on(\"dialog\", lambda dialog: dialog.accept()) ``` - **NOTE** When no `page.on('dialog')` listeners are present, all dialogs are automatically dismissed. - """ + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" @typing.overload def on( @@ -7624,9 +7650,9 @@ def once( Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. - The arguments passed into `console.log` appear as arguments on the event handler. + The arguments passed into `console.log` are available on the `ConsoleMessage` event handler argument. - An example of handling `console` event: + **Usage** ```py async def print_args(msg): @@ -7636,7 +7662,7 @@ async def print_args(msg): print(values) page.on(\"console\", print_args) - await page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ``` ```py @@ -7645,7 +7671,7 @@ def print_args(msg): print(arg.json_value()) page.on(\"console\", print_args) - page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ```""" @typing.overload @@ -7692,12 +7718,14 @@ def once( [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and actions like click will never finish. + **Usage** + ```python page.on(\"dialog\", lambda dialog: dialog.accept()) ``` - **NOTE** When no `page.on('dialog')` listeners are present, all dialogs are automatically dismissed. - """ + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" @typing.overload def once( @@ -9897,7 +9925,7 @@ async def screenshot( When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to `false`. clip : Union[{x: float, y: float, width: float, height: float}, None] - An object which specifies clipping of the resulting image. Should have the following fields: + An object which specifies clipping of the resulting image. animations : Union["allow", "disabled", None] When set to `"disabled"`, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration: @@ -12485,6 +12513,63 @@ def on( - Browser application is closed or crashed. - The `browser.close()` method was called.""" + @typing.overload + def on( + self, + event: Literal["console"], + f: typing.Callable[ + ["ConsoleMessage"], "typing.Union[typing.Awaitable[None], None]" + ], + ) -> None: + """ + Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also + emitted if the page throws an error or a warning. + + The arguments passed into `console.log` and the page are available on the `ConsoleMessage` event handler argument. + + **Usage** + + ```py + async def print_args(msg): + values = [] + for arg in msg.args: + values.append(await arg.json_value()) + print(values) + + context.on(\"console\", print_args) + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ``` + + ```py + def print_args(msg): + for arg in msg.args: + print(arg.json_value()) + + context.on(\"console\", print_args) + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ```""" + + @typing.overload + def on( + self, + event: Literal["dialog"], + f: typing.Callable[["Dialog"], "typing.Union[typing.Awaitable[None], None]"], + ) -> None: + """ + Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + either `dialog.accept()` or `dialog.dismiss()` the dialog - otherwise the page will + [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, + and actions like click will never finish. + + **Usage** + + ```python + context.on(\"dialog\", lambda dialog: dialog.accept()) + ``` + + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" + @typing.overload def on( self, @@ -12617,6 +12702,63 @@ def once( - Browser application is closed or crashed. - The `browser.close()` method was called.""" + @typing.overload + def once( + self, + event: Literal["console"], + f: typing.Callable[ + ["ConsoleMessage"], "typing.Union[typing.Awaitable[None], None]" + ], + ) -> None: + """ + Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also + emitted if the page throws an error or a warning. + + The arguments passed into `console.log` and the page are available on the `ConsoleMessage` event handler argument. + + **Usage** + + ```py + async def print_args(msg): + values = [] + for arg in msg.args: + values.append(await arg.json_value()) + print(values) + + context.on(\"console\", print_args) + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ``` + + ```py + def print_args(msg): + for arg in msg.args: + print(arg.json_value()) + + context.on(\"console\", print_args) + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ```""" + + @typing.overload + def once( + self, + event: Literal["dialog"], + f: typing.Callable[["Dialog"], "typing.Union[typing.Awaitable[None], None]"], + ) -> None: + """ + Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + either `dialog.accept()` or `dialog.dismiss()` the dialog - otherwise the page will + [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, + and actions like click will never finish. + + **Usage** + + ```python + context.on(\"dialog\", lambda dialog: dialog.accept()) + ``` + + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" + @typing.overload def once( self, @@ -12886,6 +13028,9 @@ async def add_cookies(self, cookies: typing.List[SetCookieParam]) -> None: Parameters ---------- cookies : List[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None]}] + Adds cookies to the browser context. + + For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". """ return mapping.from_maybe_impl( @@ -13938,8 +14083,7 @@ async def new_context( Learn more about [storage state and auth](../auth.md). Populates context with given storage state. This option can be used to initialize context with logged-in - information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or - an object with the following fields: + information obtained via `browser_context.storage_state()`. base_url : Union[str, None] When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, or `page.expect_response()` it takes the base URL in consideration by @@ -14153,8 +14297,7 @@ async def new_page( Learn more about [storage state and auth](../auth.md). Populates context with given storage state. This option can be used to initialize context with logged-in - information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or - an object with the following fields: + information obtained via `browser_context.storage_state()`. base_url : Union[str, None] When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, or `page.expect_response()` it takes the base URL in consideration by @@ -16585,6 +16728,35 @@ def or_(self, locator: "Locator") -> "Locator": return mapping.from_impl(self._impl_obj.or_(locator=locator._impl_obj)) + def and_(self, locator: "Locator") -> "Locator": + """Locator.and_ + + Creates a locator that matches both this locator and the argument locator. + + **Usage** + + The following example finds a button with a specific title. + + ```py + button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + ``` + + ```py + button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + ``` + + Parameters + ---------- + locator : Locator + Additional locator to match. + + Returns + ------- + Locator + """ + + return mapping.from_impl(self._impl_obj.and_(locator=locator._impl_obj)) + async def focus(self, *, timeout: typing.Optional[float] = None) -> None: """Locator.focus diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 33318acd4..7be26802b 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -860,7 +860,7 @@ def fallback( ```py # Handle GET requests. - def handle_post(route): + def handle_get(route): if route.request.method != \"GET\": route.fallback() return @@ -881,7 +881,7 @@ def handle_post(route): ```py # Handle GET requests. - def handle_post(route): + def handle_get(route): if route.request.method != \"GET\": route.fallback() return @@ -7211,6 +7211,18 @@ def location(self) -> SourceLocation: """ return mapping.from_impl(self._impl_obj.location) + @property + def page(self) -> typing.Optional["Page"]: + """ConsoleMessage.page + + The page that produced this console message, if any. + + Returns + ------- + Union[Page, None] + """ + return mapping.from_impl_nullable(self._impl_obj.page) + mapping.register(ConsoleMessageImpl, ConsoleMessage) @@ -7252,6 +7264,18 @@ def default_value(self) -> str: """ return mapping.from_maybe_impl(self._impl_obj.default_value) + @property + def page(self) -> typing.Optional["Page"]: + """Dialog.page + + The page that initiated this dialog, if available. + + Returns + ------- + Union[Page, None] + """ + return mapping.from_impl_nullable(self._impl_obj.page) + def accept(self, prompt_text: typing.Optional[str] = None) -> None: """Dialog.accept @@ -7436,9 +7460,9 @@ def on( Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. - The arguments passed into `console.log` appear as arguments on the event handler. + The arguments passed into `console.log` are available on the `ConsoleMessage` event handler argument. - An example of handling `console` event: + **Usage** ```py async def print_args(msg): @@ -7448,7 +7472,7 @@ async def print_args(msg): print(values) page.on(\"console\", print_args) - await page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ``` ```py @@ -7457,7 +7481,7 @@ def print_args(msg): print(arg.json_value()) page.on(\"console\", print_args) - page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ```""" @typing.overload @@ -7498,12 +7522,14 @@ def on( [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and actions like click will never finish. + **Usage** + ```python page.on(\"dialog\", lambda dialog: dialog.accept()) ``` - **NOTE** When no `page.on('dialog')` listeners are present, all dialogs are automatically dismissed. - """ + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" @typing.overload def on( @@ -7684,9 +7710,9 @@ def once( Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. - The arguments passed into `console.log` appear as arguments on the event handler. + The arguments passed into `console.log` are available on the `ConsoleMessage` event handler argument. - An example of handling `console` event: + **Usage** ```py async def print_args(msg): @@ -7696,7 +7722,7 @@ async def print_args(msg): print(values) page.on(\"console\", print_args) - await page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ``` ```py @@ -7705,7 +7731,7 @@ def print_args(msg): print(arg.json_value()) page.on(\"console\", print_args) - page.evaluate(\"console.log('hello', 5, {foo: 'bar'})\") + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") ```""" @typing.overload @@ -7748,12 +7774,14 @@ def once( [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and actions like click will never finish. + **Usage** + ```python page.on(\"dialog\", lambda dialog: dialog.accept()) ``` - **NOTE** When no `page.on('dialog')` listeners are present, all dialogs are automatically dismissed. - """ + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" @typing.overload def once( @@ -9965,7 +9993,7 @@ def screenshot( When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to `false`. clip : Union[{x: float, y: float, width: float, height: float}, None] - An object which specifies clipping of the resulting image. Should have the following fields: + An object which specifies clipping of the resulting image. animations : Union["allow", "disabled", None] When set to `"disabled"`, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration: @@ -12589,6 +12617,57 @@ def on( - Browser application is closed or crashed. - The `browser.close()` method was called.""" + @typing.overload + def on( + self, event: Literal["console"], f: typing.Callable[["ConsoleMessage"], "None"] + ) -> None: + """ + Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also + emitted if the page throws an error or a warning. + + The arguments passed into `console.log` and the page are available on the `ConsoleMessage` event handler argument. + + **Usage** + + ```py + async def print_args(msg): + values = [] + for arg in msg.args: + values.append(await arg.json_value()) + print(values) + + context.on(\"console\", print_args) + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ``` + + ```py + def print_args(msg): + for arg in msg.args: + print(arg.json_value()) + + context.on(\"console\", print_args) + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ```""" + + @typing.overload + def on( + self, event: Literal["dialog"], f: typing.Callable[["Dialog"], "None"] + ) -> None: + """ + Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + either `dialog.accept()` or `dialog.dismiss()` the dialog - otherwise the page will + [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, + and actions like click will never finish. + + **Usage** + + ```python + context.on(\"dialog\", lambda dialog: dialog.accept()) + ``` + + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" + @typing.overload def on(self, event: Literal["page"], f: typing.Callable[["Page"], "None"]) -> None: """ @@ -12697,6 +12776,57 @@ def once( - Browser application is closed or crashed. - The `browser.close()` method was called.""" + @typing.overload + def once( + self, event: Literal["console"], f: typing.Callable[["ConsoleMessage"], "None"] + ) -> None: + """ + Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also + emitted if the page throws an error or a warning. + + The arguments passed into `console.log` and the page are available on the `ConsoleMessage` event handler argument. + + **Usage** + + ```py + async def print_args(msg): + values = [] + for arg in msg.args: + values.append(await arg.json_value()) + print(values) + + context.on(\"console\", print_args) + await page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ``` + + ```py + def print_args(msg): + for arg in msg.args: + print(arg.json_value()) + + context.on(\"console\", print_args) + page.evaluate(\"console.log('hello', 5, { foo: 'bar' })\") + ```""" + + @typing.overload + def once( + self, event: Literal["dialog"], f: typing.Callable[["Dialog"], "None"] + ) -> None: + """ + Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + either `dialog.accept()` or `dialog.dismiss()` the dialog - otherwise the page will + [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, + and actions like click will never finish. + + **Usage** + + ```python + context.on(\"dialog\", lambda dialog: dialog.accept()) + ``` + + **NOTE** When no `page.on('dialog')` or `browser_context.on('dialog')` listeners are present, all dialogs are + automatically dismissed.""" + @typing.overload def once( self, event: Literal["page"], f: typing.Callable[["Page"], "None"] @@ -12950,6 +13080,9 @@ def add_cookies(self, cookies: typing.List[SetCookieParam]) -> None: Parameters ---------- cookies : List[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None]}] + Adds cookies to the browser context. + + For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". """ return mapping.from_maybe_impl( @@ -14012,8 +14145,7 @@ def new_context( Learn more about [storage state and auth](../auth.md). Populates context with given storage state. This option can be used to initialize context with logged-in - information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or - an object with the following fields: + information obtained via `browser_context.storage_state()`. base_url : Union[str, None] When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, or `page.expect_response()` it takes the base URL in consideration by @@ -14229,8 +14361,7 @@ def new_page( Learn more about [storage state and auth](../auth.md). Populates context with given storage state. This option can be used to initialize context with logged-in - information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or - an object with the following fields: + information obtained via `browser_context.storage_state()`. base_url : Union[str, None] When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, or `page.expect_response()` it takes the base URL in consideration by @@ -16695,6 +16826,35 @@ def or_(self, locator: "Locator") -> "Locator": return mapping.from_impl(self._impl_obj.or_(locator=locator._impl_obj)) + def and_(self, locator: "Locator") -> "Locator": + """Locator.and_ + + Creates a locator that matches both this locator and the argument locator. + + **Usage** + + The following example finds a button with a specific title. + + ```py + button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + ``` + + ```py + button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + ``` + + Parameters + ---------- + locator : Locator + Additional locator to match. + + Returns + ------- + Locator + """ + + return mapping.from_impl(self._impl_obj.and_(locator=locator._impl_obj)) + def focus(self, *, timeout: typing.Optional[float] = None) -> None: """Locator.focus diff --git a/setup.py b/setup.py index 88d020a46..4ce9c812a 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.33.0" +driver_version = "1.34.0-alpha-may-17-2023" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_accessibility.py b/tests/async/test_accessibility.py index 623bd5908..8b2ff2e16 100644 --- a/tests/async/test_accessibility.py +++ b/tests/async/test_accessibility.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import sys + import pytest @@ -93,7 +96,12 @@ async def test_accessibility_should_work(page, is_firefox, is_chromium): {"role": "textbox", "name": "placeholder", "value": "and a value"}, { "role": "textbox", - "name": "This is a description!", + "name": "placeholder" + if ( + sys.platform == "darwin" + and int(os.uname().release.split(".")[0]) >= 21 + ) + else "This is a description!", "value": "and a value", }, # webkit uses the description over placeholder for the name ], diff --git a/tests/async/test_browsercontext_events.py b/tests/async/test_browsercontext_events.py new file mode 100644 index 000000000..da6ce191a --- /dev/null +++ b/tests/async/test_browsercontext_events.py @@ -0,0 +1,182 @@ +# 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. + +import asyncio + +import pytest + +from playwright.sync_api import Page + +from ..server import HttpRequestWithPostBody, Server + + +async def test_console_event_should_work(page: Page) -> None: + [message, _] = await asyncio.gather( + page.context.wait_for_event("console"), + page.evaluate("() => console.log('hello')"), + ) + assert message.text == "hello" + assert message.page == page + + +async def test_console_event_should_work_in_popup(page: Page) -> None: + [message, popup, _] = await asyncio.gather( + page.context.wait_for_event("console"), + page.wait_for_event("popup"), + page.evaluate( + """() => { + const win = window.open(''); + win.console.log('hello'); + }""" + ), + ) + assert message.text == "hello" + assert message.page == popup + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +async def test_console_event_should_work_in_popup_2( + page: Page, browser_name: str +) -> None: + [message, popup, _] = await asyncio.gather( + page.context.wait_for_event("console", lambda msg: msg.type == "log"), + page.context.wait_for_event("page"), + page.evaluate( + """async () => { + const win = window.open('javascript:console.log("hello")'); + await new Promise(f => setTimeout(f, 0)); + win.close(); + }""" + ), + ) + assert message.text == "hello" + assert message.page == popup + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +async def test_console_event_should_work_in_immediately_closed_popup( + page: Page, browser_name: str +) -> None: + [message, popup, _] = await asyncio.gather( + page.context.wait_for_event("console"), + page.wait_for_event("popup"), + page.evaluate( + """async () => { + const win = window.open(); + win.console.log('hello'); + win.close(); + }""" + ), + ) + assert message.text == "hello" + assert message.page == popup + + +async def test_dialog_event_should_work1(page: Page) -> None: + prompt_task = None + + async def open_dialog() -> None: + nonlocal prompt_task + prompt_task = asyncio.create_task(page.evaluate("() => prompt('hey?')")) + + [dialog1, dialog2, _] = await asyncio.gather( + page.context.wait_for_event("dialog"), + page.wait_for_event("dialog"), + open_dialog(), + ) + assert dialog1 == dialog2 + assert dialog1.message == "hey?" + assert dialog1.page == page + await dialog1.accept("hello") + assert await prompt_task == "hello" + + +async def test_dialog_event_should_work_in_popup(page: Page) -> None: + prompt_task = None + + async def open_dialog() -> None: + nonlocal prompt_task + prompt_task = asyncio.create_task( + page.evaluate("() => window.open('').prompt('hey?')") + ) + + [dialog, popup, _] = await asyncio.gather( + page.context.wait_for_event("dialog"), + page.wait_for_event("popup"), + open_dialog(), + ) + assert dialog.message == "hey?" + assert dialog.page == popup + await dialog.accept("hello") + assert await prompt_task == "hello" + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +async def test_dialog_event_should_work_in_popup_2( + page: Page, browser_name: str +) -> None: + promise = asyncio.create_task( + page.evaluate("() => window.open('javascript:prompt(\"hey?\")')") + ) + dialog = await page.context.wait_for_event("dialog") + assert dialog.message == "hey?" + assert dialog.page is None + await dialog.accept("hello") + await promise + + +# console message from javascript: url is not reported at all +@pytest.mark.skip_browser("firefox") +async def test_dialog_event_should_work_in_immdiately_closed_popup(page: Page) -> None: + [message, popup, _] = await asyncio.gather( + page.context.wait_for_event("console"), + page.wait_for_event("popup"), + page.evaluate( + """() => { + const win = window.open(); + win.console.log('hello'); + win.close(); + }""" + ), + ) + assert message.text == "hello" + assert message.page == popup + + +async 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) + await page.goto(server.EMPTY_PAGE) + await page.set_content("Click me") + + promise = asyncio.create_task(page.click("a")) + [dialog, popup] = await asyncio.gather( + page.context.wait_for_event("dialog"), + page.wait_for_event("popup"), + ) + + assert dialog.message == "hey?" + assert dialog.page == popup + await dialog.accept("hello") + await promise + await popup.evaluate("window.result") == "hello" diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py index 57be74c90..2de3a244c 100644 --- a/tests/async/test_locators.py +++ b/tests/async/test_locators.py @@ -949,6 +949,31 @@ async def test_should_support_locator_filter(page: Page) -> None: await expect(page.locator("div").filter(has_not_text="foo")).to_have_count(2) +async def test_locators_should_support_locator_and(page: Page, server: Server): + await page.set_content( + """ +
hello
world
+ hello2world2 + """ + ) + await expect(page.locator("div").and_(page.locator("div"))).to_have_count(2) + await expect(page.locator("div").and_(page.get_by_test_id("foo"))).to_have_text( + ["hello"] + ) + await expect(page.locator("div").and_(page.get_by_test_id("bar"))).to_have_text( + ["world"] + ) + await expect(page.get_by_test_id("foo").and_(page.locator("div"))).to_have_text( + ["hello"] + ) + await expect(page.get_by_test_id("bar").and_(page.locator("span"))).to_have_text( + ["world2"] + ) + await expect( + page.locator("span").and_(page.get_by_test_id(re.compile("bar|foo"))) + ).to_have_count(2) + + async def test_locators_has_does_not_encode_unicode(page: Page, server: Server): await page.goto(server.EMPTY_PAGE) locators = [ diff --git a/tests/async/test_popup.py b/tests/async/test_popup.py index d1dda1443..68ed1273d 100644 --- a/tests/async/test_popup.py +++ b/tests/async/test_popup.py @@ -316,20 +316,27 @@ async def test_should_emit_for_immediately_closed_popups(context, server): async def test_should_be_able_to_capture_alert(context): page = await context.new_page() - evaluate_promise = asyncio.create_task( - page.evaluate( - """() => { + evaluate_task = None + + async def evaluate() -> None: + nonlocal evaluate_task + evaluate_task = asyncio.create_task( + page.evaluate( + """() => { const win = window.open('') win.alert('hello') }""" + ) ) + + [popup, dialog, _] = await asyncio.gather( + page.wait_for_event("popup"), context.wait_for_event("dialog"), evaluate() ) - popup = await page.wait_for_event("popup") - dialog = await popup.wait_for_event("dialog") assert dialog.message == "hello" + assert dialog.page == popup await dialog.dismiss() - await evaluate_promise + await evaluate_task async def test_should_work_with_empty_url(context): diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py new file mode 100644 index 000000000..480adb7f7 --- /dev/null +++ b/tests/async/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.async_api import Page + + +async def test_should_work_with_internal_and(page: Page, server): + await page.set_content( + """ +
hello
world
+ hello2world2 + """ + ) + assert ( + await page.eval_on_selector_all( + 'div >> internal:and="span"', "els => els.map(e => e.textContent)" + ) + ) == [] + assert ( + await page.eval_on_selector_all( + 'div >> internal:and=".foo"', "els => els.map(e => e.textContent)" + ) + ) == ["hello"] + assert ( + await page.eval_on_selector_all( + 'div >> internal:and=".bar"', "els => els.map(e => e.textContent)" + ) + ) == ["world"] + assert ( + await page.eval_on_selector_all( + 'span >> internal:and="span"', "els => els.map(e => e.textContent)" + ) + ) == ["hello2", "world2"] + assert ( + await page.eval_on_selector_all( + '.foo >> internal:and="div"', "els => els.map(e => e.textContent)" + ) + ) == ["hello"] + assert ( + await page.eval_on_selector_all( + '.bar >> internal:and="span"', "els => els.map(e => e.textContent)" + ) + ) == ["world2"] diff --git a/tests/sync/test_accessibility.py b/tests/sync/test_accessibility.py index d4fdb9dfa..d71f27a4d 100644 --- a/tests/sync/test_accessibility.py +++ b/tests/sync/test_accessibility.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import sys + import pytest from playwright.sync_api import Page @@ -97,7 +100,12 @@ def test_accessibility_should_work( {"role": "textbox", "name": "placeholder", "value": "and a value"}, { "role": "textbox", - "name": "This is a description!", + "name": "placeholder" + if ( + sys.platform == "darwin" + and int(os.uname().release.split(".")[0]) >= 21 + ) + else "This is a description!", "value": "and a value", }, # webkit uses the description over placeholder for the name ],