diff --git a/playwright/async_api.py b/playwright/async_api.py index b20e91001..1dba5976e 100644 --- a/playwright/async_api.py +++ b/playwright/async_api.py @@ -28,6 +28,10 @@ from playwright.browser_context import BrowserContext as BrowserContextImpl from playwright.browser_server import BrowserServer as BrowserServerImpl from playwright.browser_type import BrowserType as BrowserTypeImpl +from playwright.cdp_session import CDPSession as CDPSessionImpl +from playwright.chromium_browser_context import ( + ChromiumBrowserContext as ChromiumBrowserContextImpl, +) from playwright.console_message import ConsoleMessage as ConsoleMessageImpl from playwright.dialog import Dialog as DialogImpl from playwright.download import Download as DownloadImpl @@ -647,7 +651,7 @@ def __init__(self, obj: JSHandleImpl): super().__init__(obj) async def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """JSHandle.evaluate @@ -676,7 +680,7 @@ async def evaluate( ) async def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """JSHandle.evaluateHandle @@ -1034,7 +1038,7 @@ async def selectOption( typing.List[str], typing.List["ElementHandle"], typing.List[SelectOption], - ], + ] = None, timeout: int = None, noWaitAfter: bool = None, ) -> typing.List[str]: @@ -1336,7 +1340,7 @@ async def evalOnSelector( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """ElementHandle.evalOnSelector @@ -1374,7 +1378,7 @@ async def evalOnSelectorAll( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """ElementHandle.evalOnSelectorAll @@ -1699,7 +1703,7 @@ async def frameElement(self) -> "ElementHandle": return mapping.from_impl(await self._impl_obj.frameElement()) async def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """Frame.evaluate @@ -1729,7 +1733,7 @@ async def evaluate( ) async def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """Frame.evaluateHandle @@ -1878,7 +1882,7 @@ async def evalOnSelector( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Frame.evalOnSelector @@ -1916,7 +1920,7 @@ async def evalOnSelectorAll( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Frame.evalOnSelectorAll @@ -2334,7 +2338,7 @@ async def selectOption( typing.List[str], typing.List["ElementHandle"], typing.List[SelectOption], - ], + ] = None, timeout: int = None, noWaitAfter: bool = None, ) -> typing.List[str]: @@ -2556,7 +2560,7 @@ async def waitForFunction( self, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, timeout: int = None, polling: typing.Union[int, Literal["raf"]] = None, ) -> "JSHandle": @@ -2640,7 +2644,7 @@ def url(self) -> str: return mapping.from_maybe_impl(self._impl_obj.url) async def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """Worker.evaluate @@ -2668,7 +2672,7 @@ async def evaluate( ) async def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """Worker.evaluateHandle @@ -2704,7 +2708,11 @@ def __init__(self, obj: SelectorsImpl): super().__init__(obj) async def register( - self, name: str, source: str = "", path: str = None, contentScript: bool = False + self, + name: str, + source: str = None, + path: str = None, + contentScript: bool = None, ) -> NoneType: """Selectors.register @@ -2714,9 +2722,9 @@ async def register( ---------- name : str Name that is used in selectors as a prefix, e.g. `{name: 'foo'}` enables `foo=myselectorbody` selectors. May only contain `[a-zA-Z0-9_]` characters. - source : str + source : Optional[str] Script that evaluates to a selector engine instance. - contentScript : bool + contentScript : Optional[bool] Whether to run this selector engine in isolated JavaScript environment. This environment has access to the same DOM, but not any JavaScript objects from the frame's scripts. Defaults to `false`. Note that running as a content script is not guaranteed when this engine is used together with other registered engines. """ return mapping.from_maybe_impl( @@ -3198,7 +3206,7 @@ async def dispatchEvent( ) async def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """Page.evaluate @@ -3230,7 +3238,7 @@ async def evaluate( ) async def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """Page.evaluateHandle @@ -3264,7 +3272,7 @@ async def evalOnSelector( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Page.evalOnSelector @@ -3303,7 +3311,7 @@ async def evalOnSelectorAll( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Page.evalOnSelectorAll @@ -4281,7 +4289,7 @@ async def selectOption( typing.List[str], typing.List["ElementHandle"], typing.List[SelectOption], - ], + ] = None, timeout: int = None, noWaitAfter: bool = None, ) -> typing.List[str]: @@ -4505,7 +4513,7 @@ async def waitForFunction( self, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, timeout: int = None, polling: typing.Union[int, Literal["raf"]] = None, ) -> "JSHandle": @@ -4913,7 +4921,7 @@ async def clearPermissions(self) -> NoneType: """ return mapping.from_maybe_impl(await self._impl_obj.clearPermissions()) - async def setGeolocation(self, geolocation: typing.Dict) -> NoneType: + async def setGeolocation(self, geolocation: typing.Dict = None) -> NoneType: """BrowserContext.setGeolocation Sets the context's geolocation. Passing `null` or `undefined` emulates position unavailable. @@ -5133,6 +5141,85 @@ def expect_page( mapping.register(BrowserContextImpl, BrowserContext) +class CDPSession(AsyncBase): + def __init__(self, obj: CDPSessionImpl): + super().__init__(obj) + + async def send(self, method: str, params: typing.Dict = None) -> typing.Dict: + """CDPSession.send + + Parameters + ---------- + method : str + protocol method name + params : Optional[typing.Dict] + Optional method parameters + + Returns + ------- + typing.Dict + """ + return mapping.from_maybe_impl( + await self._impl_obj.send(method=method, params=params) + ) + + async def detach(self) -> NoneType: + """CDPSession.detach + + Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used + to send messages. + """ + return mapping.from_maybe_impl(await self._impl_obj.detach()) + + +mapping.register(CDPSessionImpl, CDPSession) + + +class ChromiumBrowserContext(BrowserContext): + def __init__(self, obj: ChromiumBrowserContextImpl): + super().__init__(obj) + + def backgroundPages(self) -> typing.List["Page"]: + """ChromiumBrowserContext.backgroundPages + + Returns + ------- + typing.List[Page] + All existing background pages in the context. + """ + return mapping.from_impl_list(self._impl_obj.backgroundPages()) + + def serviceWorkers(self) -> typing.List["Worker"]: + """ChromiumBrowserContext.serviceWorkers + + Returns + ------- + typing.List[Worker] + All existing service workers in the context. + """ + return mapping.from_impl_list(self._impl_obj.serviceWorkers()) + + async def newCDPSession(self, page: "Page") -> "CDPSession": + """ChromiumBrowserContext.newCDPSession + + Parameters + ---------- + page : Page + Page to create new session for. + + Returns + ------- + CDPSession + Promise that resolves to the newly created session. + """ + return mapping.from_impl( + await self._impl_obj.newCDPSession(page=page._impl_obj) + ) + + +mapping.register(ChromiumBrowserContextImpl, ChromiumBrowserContext) + + class Browser(AsyncBase): def __init__(self, obj: BrowserImpl): super().__init__(obj) diff --git a/playwright/browser.py b/playwright/browser.py index 60d8f4058..fed3fb0c6 100644 --- a/playwright/browser.py +++ b/playwright/browser.py @@ -14,7 +14,7 @@ import sys from types import SimpleNamespace -from typing import Dict, List, Union +from typing import TYPE_CHECKING, Dict, List, Union from playwright.browser_context import BrowserContext from playwright.connection import ChannelOwner, from_channel @@ -27,15 +27,19 @@ else: # pragma: no cover from typing_extensions import Literal +if TYPE_CHECKING: # pragma: no cover + from playwright.browser_type import BrowserType + class Browser(ChannelOwner): Events = SimpleNamespace(Disconnected="disconnected",) def __init__( - self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + self, parent: "BrowserType", type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + self._browser_type = parent self._is_connected = True self._is_closed_or_closing = False diff --git a/playwright/cdp_session.py b/playwright/cdp_session.py new file mode 100644 index 000000000..0aad85106 --- /dev/null +++ b/playwright/cdp_session.py @@ -0,0 +1,39 @@ +# 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 Any, Dict + +from playwright.connection import ChannelOwner +from playwright.js_handle import parse_result, serialize_argument + + +class CDPSession(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._channel.on("event", lambda params: self._on_event(params)) + + def _on_event(self, params: Any) -> None: + self.emit(params["method"], parse_result(params["params"])) + + async def send(self, method: str, params: Dict = None) -> Dict: + payload = {"method": method} + if params: + payload["params"] = serialize_argument(params)["value"] + result = await self._channel.send("send", payload) + return parse_result(result) + + async def detach(self) -> None: + await self._channel.send("detach") diff --git a/playwright/chromium_browser_context.py b/playwright/chromium_browser_context.py new file mode 100644 index 000000000..bc13fb54d --- /dev/null +++ b/playwright/chromium_browser_context.py @@ -0,0 +1,66 @@ +# 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 types import SimpleNamespace +from typing import Dict, List, Set + +from playwright.browser_context import BrowserContext +from playwright.cdp_session import CDPSession +from playwright.connection import ChannelOwner, from_channel +from playwright.page import Page, Worker + + +class ChromiumBrowserContext(BrowserContext): + + Events = SimpleNamespace( + BackgroundPage="backgroundpage", ServiceWorker="serviceworker", + ) + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + + self._background_pages: Set[Page] = set() + self._service_workers: Set[Worker] = set() + + self._channel.on( + "crBackgroundPage", + lambda params: self._on_background_page(from_channel(params["page"])), + ) + + self._channel.on( + "crServiceWorker", + lambda params: self._on_service_worker(from_channel(params["worker"])), + ) + + def _on_background_page(self, page: Page) -> None: + self._background_pages.add(page) + self.emit(ChromiumBrowserContext.Events.BackgroundPage, page) + + def _on_service_worker(self, worker: Worker) -> None: + worker._context = self + self._service_workers.add(worker) + self.emit(ChromiumBrowserContext.Events.ServiceWorker, worker) + + def backgroundPages(self) -> List[Page]: + return list(self._background_pages) + + def serviceWorkers(self) -> List[Worker]: + return list(self._service_workers) + + async def newCDPSession(self, page: Page) -> CDPSession: + return from_channel( + await self._channel.send("crNewCDPSession", {"page": page._channel}) + ) diff --git a/playwright/element_handle.py b/playwright/element_handle.py index c02e370bc..a53c44808 100644 --- a/playwright/element_handle.py +++ b/playwright/element_handle.py @@ -187,7 +187,7 @@ async def querySelectorAll(self, selector: str) -> List["ElementHandle"]: ) async def evalOnSelector( - self, selector: str, expression: str, arg: Any = None, force_expr: bool = False + self, selector: str, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: return parse_result( await self._channel.send( @@ -202,7 +202,7 @@ async def evalOnSelector( ) async def evalOnSelectorAll( - self, selector: str, expression: str, arg: Any = None, force_expr: bool = False + self, selector: str, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: return parse_result( await self._channel.send( diff --git a/playwright/frame.py b/playwright/frame.py index 15882dcc5..58a5e7f18 100644 --- a/playwright/frame.py +++ b/playwright/frame.py @@ -180,7 +180,7 @@ async def frameElement(self) -> ElementHandle: return from_channel(await self._channel.send("frameElement")) async def evaluate( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: if not is_function_body(expression): force_expr = True @@ -196,7 +196,7 @@ async def evaluate( ) async def evaluateHandle( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> JSHandle: if not is_function_body(expression): force_expr = True @@ -242,7 +242,7 @@ async def dispatchEvent( ) async def evalOnSelector( - self, selector: str, expression: str, arg: Any = None, force_expr: bool = False + self, selector: str, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: return parse_result( await self._channel.send( @@ -257,7 +257,7 @@ async def evalOnSelector( ) async def evalOnSelectorAll( - self, selector: str, expression: str, arg: Any = None, force_expr: bool = False + self, selector: str, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: return parse_result( await self._channel.send( @@ -447,7 +447,7 @@ async def waitForFunction( self, expression: str, arg: Any = None, - force_expr: bool = False, + force_expr: bool = None, timeout: int = None, polling: Union[int, Literal["raf"]] = None, ) -> JSHandle: diff --git a/playwright/js_handle.py b/playwright/js_handle.py index e57e319e2..21d27738d 100644 --- a/playwright/js_handle.py +++ b/playwright/js_handle.py @@ -40,7 +40,7 @@ def _on_preview_updated(self, preview: str) -> None: self._preview = preview async def evaluate( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: if not is_function_body(expression): force_expr = True @@ -56,7 +56,7 @@ async def evaluate( ) async def evaluateHandle( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> "JSHandle": if not is_function_body(expression): force_expr = True diff --git a/playwright/object_factory.py b/playwright/object_factory.py index 6cdcd22b0..d4cd118ce 100644 --- a/playwright/object_factory.py +++ b/playwright/object_factory.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Dict +from typing import Any, Dict, cast from playwright.browser import Browser from playwright.browser_context import BrowserContext from playwright.browser_server import BrowserServer from playwright.browser_type import BrowserType +from playwright.cdp_session import CDPSession +from playwright.chromium_browser_context import ChromiumBrowserContext from playwright.connection import ChannelOwner from playwright.console_message import ConsoleMessage from playwright.dialog import Dialog @@ -44,13 +46,22 @@ def create_remote_object( if type == "BindingCall": return BindingCall(parent, type, guid, initializer) if type == "Browser": - return Browser(parent, type, guid, initializer) + return Browser(cast(BrowserType, parent), type, guid, initializer) if type == "BrowserServer": return BrowserServer(parent, type, guid, initializer) if type == "BrowserType": return BrowserType(parent, type, guid, initializer) if type == "BrowserContext": + browser_name: str = "" + if isinstance(parent, Browser): + browser_name = parent._browser_type.name + if isinstance(parent, BrowserType): + browser_name = parent.name + if browser_name == "chromium": + return ChromiumBrowserContext(parent, type, guid, initializer) return BrowserContext(parent, type, guid, initializer) + if type == "CDPSession": + return CDPSession(parent, type, guid, initializer) if type == "ConsoleMessage": return ConsoleMessage(parent, type, guid, initializer) if type == "Dialog": diff --git a/playwright/page.py b/playwright/page.py index 4cfe5c7b3..38771fd8d 100644 --- a/playwright/page.py +++ b/playwright/page.py @@ -300,26 +300,26 @@ async def dispatchEvent( return await self._main_frame.dispatchEvent(**locals_to_params(locals())) async def evaluate( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: return await self._main_frame.evaluate(expression, arg, force_expr=force_expr) async def evaluateHandle( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> JSHandle: return await self._main_frame.evaluateHandle( expression, arg, force_expr=force_expr ) async def evalOnSelector( - self, selector: str, expression: str, arg: Any = None, force_expr: bool = False + self, selector: str, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: return await self._main_frame.evalOnSelector( selector, expression, arg, force_expr=force_expr ) async def evalOnSelectorAll( - self, selector: str, expression: str, arg: Any = None, force_expr: bool = False + self, selector: str, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: return await self._main_frame.evalOnSelectorAll( selector, expression, arg, force_expr=force_expr @@ -661,7 +661,7 @@ async def waitForFunction( self, expression: str, arg: Any = None, - force_expr: bool = False, + force_expr: bool = None, timeout: int = None, polling: Union[int, Literal["raf"]] = None, ) -> JSHandle: @@ -792,10 +792,13 @@ def __init__( super().__init__(parent, type, guid, initializer) self._channel.on("close", lambda _: self._on_close()) self._page: Optional[Page] = None + self._context: Optional["BrowserContext"] = None def _on_close(self) -> None: if self._page: self._page._workers.remove(self) + if self._context: + self._context._service_workers.remove(self) self.emit(Worker.Events.Close, self) @property @@ -803,7 +806,7 @@ def url(self) -> str: return self._initializer["url"] async def evaluate( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> Any: if not is_function_body(expression): force_expr = True @@ -819,7 +822,7 @@ async def evaluate( ) async def evaluateHandle( - self, expression: str, arg: Any = None, force_expr: bool = False + self, expression: str, arg: Any = None, force_expr: bool = None ) -> JSHandle: return from_channel( await self._channel.send( diff --git a/playwright/selectors.py b/playwright/selectors.py index 5306d474e..e3f3d8aa9 100644 --- a/playwright/selectors.py +++ b/playwright/selectors.py @@ -16,6 +16,7 @@ from playwright.connection import ChannelOwner from playwright.element_handle import ElementHandle +from playwright.helper import Error class Selectors(ChannelOwner): @@ -25,14 +26,21 @@ def __init__( super().__init__(parent, type, guid, initializer) async def register( - self, name: str, source: str = "", path: str = None, contentScript: bool = False + self, + name: str, + source: str = None, + path: str = None, + contentScript: bool = None, ) -> None: + if not source and not path: + raise Error("Either source or path should be specified") if path: with open(path, "r") as file: source = file.read() - await self._channel.send( - "register", dict(name=name, source=source, contentScript=contentScript), - ) + params: Dict = dict(name=name, source=source) + if contentScript: + params["contentScript"] = True + await self._channel.send("register", params) async def _createSelector(self, name: str, handle: ElementHandle) -> Optional[str]: return await self._channel.send( diff --git a/playwright/sync_api.py b/playwright/sync_api.py index 93bd205d8..3fcfb2982 100644 --- a/playwright/sync_api.py +++ b/playwright/sync_api.py @@ -27,6 +27,10 @@ from playwright.browser_context import BrowserContext as BrowserContextImpl from playwright.browser_server import BrowserServer as BrowserServerImpl from playwright.browser_type import BrowserType as BrowserTypeImpl +from playwright.cdp_session import CDPSession as CDPSessionImpl +from playwright.chromium_browser_context import ( + ChromiumBrowserContext as ChromiumBrowserContextImpl, +) from playwright.console_message import ConsoleMessage as ConsoleMessageImpl from playwright.dialog import Dialog as DialogImpl from playwright.download import Download as DownloadImpl @@ -659,7 +663,7 @@ def __init__(self, obj: JSHandleImpl): super().__init__(obj) def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """JSHandle.evaluate @@ -692,7 +696,7 @@ def evaluate( ) def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """JSHandle.evaluateHandle @@ -1062,7 +1066,7 @@ def selectOption( typing.List[str], typing.List["ElementHandle"], typing.List[SelectOption], - ], + ] = None, timeout: int = None, noWaitAfter: bool = None, ) -> typing.List[str]: @@ -1382,7 +1386,7 @@ def evalOnSelector( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """ElementHandle.evalOnSelector @@ -1422,7 +1426,7 @@ def evalOnSelectorAll( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """ElementHandle.evalOnSelectorAll @@ -1757,7 +1761,7 @@ def frameElement(self) -> "ElementHandle": return mapping.from_impl(self._sync(self._impl_obj.frameElement())) def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """Frame.evaluate @@ -1791,7 +1795,7 @@ def evaluate( ) def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """Frame.evaluateHandle @@ -1946,7 +1950,7 @@ def evalOnSelector( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Frame.evalOnSelector @@ -1986,7 +1990,7 @@ def evalOnSelectorAll( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Frame.evalOnSelectorAll @@ -2423,7 +2427,7 @@ def selectOption( typing.List[str], typing.List["ElementHandle"], typing.List[SelectOption], - ], + ] = None, timeout: int = None, noWaitAfter: bool = None, ) -> typing.List[str]: @@ -2666,7 +2670,7 @@ def waitForFunction( self, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, timeout: int = None, polling: typing.Union[int, Literal["raf"]] = None, ) -> "JSHandle": @@ -2754,7 +2758,7 @@ def url(self) -> str: return mapping.from_maybe_impl(self._impl_obj.url) def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """Worker.evaluate @@ -2786,7 +2790,7 @@ def evaluate( ) def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """Worker.evaluateHandle @@ -2826,7 +2830,11 @@ def __init__(self, obj: SelectorsImpl): super().__init__(obj) def register( - self, name: str, source: str = "", path: str = None, contentScript: bool = False + self, + name: str, + source: str = None, + path: str = None, + contentScript: bool = None, ) -> NoneType: """Selectors.register @@ -2836,9 +2844,9 @@ def register( ---------- name : str Name that is used in selectors as a prefix, e.g. `{name: 'foo'}` enables `foo=myselectorbody` selectors. May only contain `[a-zA-Z0-9_]` characters. - source : str + source : Optional[str] Script that evaluates to a selector engine instance. - contentScript : bool + contentScript : Optional[bool] Whether to run this selector engine in isolated JavaScript environment. This environment has access to the same DOM, but not any JavaScript objects from the frame's scripts. Defaults to `false`. Note that running as a content script is not guaranteed when this engine is used together with other registered engines. """ return mapping.from_maybe_impl( @@ -3324,7 +3332,7 @@ def dispatchEvent( ) def evaluate( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> typing.Any: """Page.evaluate @@ -3360,7 +3368,7 @@ def evaluate( ) def evaluateHandle( - self, expression: str, arg: typing.Any = None, force_expr: bool = False + self, expression: str, arg: typing.Any = None, force_expr: bool = None ) -> "JSHandle": """Page.evaluateHandle @@ -3398,7 +3406,7 @@ def evalOnSelector( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Page.evalOnSelector @@ -3439,7 +3447,7 @@ def evalOnSelectorAll( selector: str, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, ) -> typing.Any: """Page.evalOnSelectorAll @@ -4460,7 +4468,7 @@ def selectOption( typing.List[str], typing.List["ElementHandle"], typing.List[SelectOption], - ], + ] = None, timeout: int = None, noWaitAfter: bool = None, ) -> typing.List[str]: @@ -4705,7 +4713,7 @@ def waitForFunction( self, expression: str, arg: typing.Any = None, - force_expr: bool = False, + force_expr: bool = None, timeout: int = None, polling: typing.Union[int, Literal["raf"]] = None, ) -> "JSHandle": @@ -5121,7 +5129,7 @@ def clearPermissions(self) -> NoneType: """ return mapping.from_maybe_impl(self._sync(self._impl_obj.clearPermissions())) - def setGeolocation(self, geolocation: typing.Dict) -> NoneType: + def setGeolocation(self, geolocation: typing.Dict = None) -> NoneType: """BrowserContext.setGeolocation Sets the context's geolocation. Passing `null` or `undefined` emulates position unavailable. @@ -5355,6 +5363,85 @@ def expect_page( mapping.register(BrowserContextImpl, BrowserContext) +class CDPSession(SyncBase): + def __init__(self, obj: CDPSessionImpl): + super().__init__(obj) + + def send(self, method: str, params: typing.Dict = None) -> typing.Dict: + """CDPSession.send + + Parameters + ---------- + method : str + protocol method name + params : Optional[typing.Dict] + Optional method parameters + + Returns + ------- + typing.Dict + """ + return mapping.from_maybe_impl( + self._sync(self._impl_obj.send(method=method, params=params)) + ) + + def detach(self) -> NoneType: + """CDPSession.detach + + Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used + to send messages. + """ + return mapping.from_maybe_impl(self._sync(self._impl_obj.detach())) + + +mapping.register(CDPSessionImpl, CDPSession) + + +class ChromiumBrowserContext(BrowserContext): + def __init__(self, obj: ChromiumBrowserContextImpl): + super().__init__(obj) + + def backgroundPages(self) -> typing.List["Page"]: + """ChromiumBrowserContext.backgroundPages + + Returns + ------- + typing.List[Page] + All existing background pages in the context. + """ + return mapping.from_impl_list(self._impl_obj.backgroundPages()) + + def serviceWorkers(self) -> typing.List["Worker"]: + """ChromiumBrowserContext.serviceWorkers + + Returns + ------- + typing.List[Worker] + All existing service workers in the context. + """ + return mapping.from_impl_list(self._impl_obj.serviceWorkers()) + + def newCDPSession(self, page: "Page") -> "CDPSession": + """ChromiumBrowserContext.newCDPSession + + Parameters + ---------- + page : Page + Page to create new session for. + + Returns + ------- + CDPSession + Promise that resolves to the newly created session. + """ + return mapping.from_impl( + self._sync(self._impl_obj.newCDPSession(page=page._impl_obj)) + ) + + +mapping.register(ChromiumBrowserContextImpl, ChromiumBrowserContext) + + class Browser(SyncBase): def __init__(self, obj: BrowserImpl): super().__init__(obj) diff --git a/scripts/generate_api.py b/scripts/generate_api.py index ab3cc4c1d..e355c9890 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import inspect import re from types import FunctionType from typing import ( # type: ignore @@ -31,6 +30,8 @@ from playwright.browser_context import BrowserContext from playwright.browser_server import BrowserServer from playwright.browser_type import BrowserType +from playwright.cdp_session import CDPSession +from playwright.chromium_browser_context import ChromiumBrowserContext from playwright.console_message import ConsoleMessage from playwright.dialog import Dialog from playwright.download import Download @@ -53,13 +54,13 @@ def process_type(value: Any, param: bool = False) -> str: value = re.sub(r"playwright\.[\w]+\.([\w]+)", r'"\1"', value) value = re.sub(r"typing.Literal", "Literal", value) if param: - value = re.sub(r"typing.Union\[([^,]+), NoneType\]", r"\1", value) - if "Union[Literal" in value: - value = re.sub(r"typing.Union\[(.*), NoneType\]", r"\1", value) - else: - value = re.sub( - r"typing.Union\[(.*), NoneType\]", r"typing.Union[\1]", value - ) + value = re.sub(r"^typing.Union\[([^,]+), NoneType\]$", r"\1 = None", value) + value = re.sub( + r"typing.Union\[(Literal\[[^\]]+\]), NoneType\]", r"\1 = None", value + ) + value = re.sub( + r"^typing.Union\[(.+), NoneType\]$", r"typing.Union[\1] = None", value + ) return value @@ -68,22 +69,11 @@ def signature(func: FunctionType, indent: int) -> str: tokens = ["self"] split = ",\n" + " " * indent - func_signature = inspect.signature(func) for [name, value] in hints.items(): if name == "return": continue processed = process_type(value, True) - default_value = func_signature.parameters[name].default - if default_value is not func_signature.parameters[name].empty: - if isinstance(default_value, str): - default_value = '"' + default_value + '"' - elif isinstance(default_value, object): - default_value = str(default_value) - else: - raise ValueError(f"value {default_value} not recognized") - tokens.append(f"{name}: {processed} = {default_value}") - else: - tokens.append(f"{name}: {processed}") + tokens.append(f"{name}: {processed}") return split.join(tokens) @@ -99,6 +89,8 @@ def arguments(func: FunctionType, indent: int) -> str: tokens.append(f"{name}=self._wrap_handler({name})") elif "typing.Any" in value_str or "Handle" in value_str: tokens.append(f"{name}=mapping.to_impl({name})") + elif re.match(r" List[str]: from playwright.browser_context import BrowserContext as BrowserContextImpl from playwright.browser_server import BrowserServer as BrowserServerImpl from playwright.browser_type import BrowserType as BrowserTypeImpl +from playwright.cdp_session import CDPSession as CDPSessionImpl +from playwright.chromium_browser_context import ChromiumBrowserContext as ChromiumBrowserContextImpl from playwright.console_message import ConsoleMessage as ConsoleMessageImpl from playwright.dialog import Dialog as DialogImpl from playwright.download import Download as DownloadImpl @@ -194,6 +188,8 @@ def return_value(value: Any) -> List[str]: BindingCall, Page, BrowserContext, + CDPSession, + ChromiumBrowserContext, Browser, BrowserServer, BrowserType, diff --git a/tests/async/test_cdp_session.py b/tests/async/test_cdp_session.py new file mode 100644 index 000000000..a8c09ced6 --- /dev/null +++ b/tests/async/test_cdp_session.py @@ -0,0 +1,78 @@ +# 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 import Error + + +@pytest.mark.only_browser("chromium") +async def test_should_work(page): + client = await page.context.newCDPSession(page) + + await asyncio.gather( + client.send("Runtime.enable"), + client.send("Runtime.evaluate", {"expression": "window.foo = 'bar'"}), + ) + foo = await page.evaluate("() => window.foo") + assert foo == "bar" + + +@pytest.mark.only_browser("chromium") +async def test_should_send_events(page, server): + client = await page.context.newCDPSession(page) + await client.send("Network.enable") + events = [] + client.on("Network.requestWillBeSent", lambda event: events.append(event)) + await page.goto(server.EMPTY_PAGE) + assert len(events) == 1 + + +@pytest.mark.only_browser("chromium") +async def test_should_be_able_to_detach_session(page): + client = await page.context.newCDPSession(page) + await client.send("Runtime.enable") + eval_response = await client.send( + "Runtime.evaluate", {"expression": "1 + 2", "returnByValue": True} + ) + assert eval_response["result"]["value"] == 3 + await client.detach() + with pytest.raises(Error) as exc_info: + await client.send( + "Runtime.evaluate", {"expression": "3 + 1", "returnByValue": True} + ) + assert "Target browser or context has been closed" in exc_info.value.message + + +@pytest.mark.only_browser("chromium") +async def test_should_not_break_page_close(browser): + context = await browser.newContext() + page = await context.newPage() + session = await page.context.newCDPSession(page) + await session.detach() + await page.close() + await context.close() + + +@pytest.mark.only_browser("chromium") +async def test_should_detach_when_page_closes(browser): + context = await browser.newContext() + page = await context.newPage() + session = await context.newCDPSession(page) + await page.close() + with pytest.raises(Error): + await session.detach() + await context.close() diff --git a/tests/async/test_queryselector.py b/tests/async/test_queryselector.py index 923e7cfde..cd6714ba2 100644 --- a/tests/async/test_queryselector.py +++ b/tests/async/test_queryselector.py @@ -133,8 +133,8 @@ async def test_selectors_register_should_handle_errors(selectors, page: Page, ut with pytest.raises(Error) as exc: await selectors.register("$", dummy_selector_engine_script) assert ( - "Selector engine name may only contain [a-zA-Z0-9_] characters" - == exc.value.message + exc.value.message + == "Selector engine name may only contain [a-zA-Z0-9_] characters" ) # Selector names are case-sensitive.