This commit is contained in:
2026-10-27 17:51:34 -03:00
parent 752bb94d48
commit 197a54e41d
422 changed files with 314452 additions and 68 deletions
@@ -0,0 +1,19 @@
# 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.
"""
Python package `playwright` is a Python library to automate Chromium,
Firefox and WebKit with a single API. Playwright is built to enable cross-browser
web automation that is ever-green, capable, reliable and fast.
"""
@@ -0,0 +1,33 @@
# 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 subprocess
import sys
from playwright._impl._driver import compute_driver_executable, get_driver_env
def main() -> None:
try:
driver_executable, driver_cli = compute_driver_executable()
completed_process = subprocess.run(
[driver_executable, driver_cli, *sys.argv[1:]], env=get_driver_env()
)
sys.exit(completed_process.returncode)
except KeyboardInterrupt:
sys.exit(130)
if __name__ == "__main__":
main()
@@ -0,0 +1,20 @@
# 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 os
from typing import List
def get_hook_dirs() -> List[str]:
return [os.path.dirname(__file__)]
@@ -0,0 +1,17 @@
# 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 PyInstaller.utils.hooks import collect_data_files # type: ignore
datas = collect_data_files("playwright")
@@ -0,0 +1,17 @@
# 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 PyInstaller.utils.hooks import collect_data_files # type: ignore
datas = collect_data_files("playwright")
@@ -0,0 +1,249 @@
# 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.
# Generated from the docs by scripts/codegen/generate_api.js. Do not edit.
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict, Union
class BrowserBindResult(TypedDict):
endpoint: str
class ClientCertificate(TypedDict, total=False):
origin: str
certPath: Optional[Union[str, Path]]
cert: Optional[bytes]
keyPath: Optional[Union[str, Path]]
key: Optional[bytes]
pfxPath: Optional[Union[str, Path]]
pfx: Optional[bytes]
passphrase: Optional[str]
class Cookie(TypedDict, total=False):
name: str
value: str
domain: str
path: str
expires: float
httpOnly: bool
secure: bool
sameSite: Literal["Lax", "None", "Strict"]
partitionKey: Optional[str]
class DebuggerLocation(TypedDict):
file: str
line: Optional[int]
column: Optional[int]
class DebuggerPausedDetails(TypedDict):
location: DebuggerLocation
title: str
class FilePayload(TypedDict):
name: str
mimeType: str
buffer: bytes
class DropPayload(TypedDict, total=False):
files: Optional[
Union[str, Path, Sequence[Union[str, Path]], FilePayload, Sequence[FilePayload]]
]
data: Optional[Dict[str, str]]
class FloatRect(TypedDict):
x: float
y: float
width: float
height: float
class Geolocation(TypedDict, total=False):
latitude: float
longitude: float
accuracy: Optional[float]
class HttpCredentials(TypedDict, total=False):
username: str
password: str
origin: Optional[str]
send: Optional[Literal["always", "unauthorized"]]
class LocalStorageEntry(TypedDict):
name: str
value: str
class NameValue(TypedDict):
name: str
value: str
class OriginState(TypedDict):
origin: str
localStorage: List[LocalStorageEntry]
class PdfMargins(TypedDict, total=False):
top: Optional[Union[str, float]]
right: Optional[Union[str, float]]
bottom: Optional[Union[str, float]]
left: Optional[Union[str, float]]
class Position(TypedDict):
x: float
y: float
class ProxySettings(TypedDict, total=False):
server: str
bypass: Optional[str]
username: Optional[str]
password: Optional[str]
class RemoteAddr(TypedDict):
ipAddress: str
port: int
class RequestSizes(TypedDict):
requestBodySize: int
requestHeadersSize: int
responseBodySize: int
responseHeadersSize: int
class ResourceTiming(TypedDict):
startTime: float
domainLookupStart: float
domainLookupEnd: float
connectStart: float
secureConnectionStart: float
connectEnd: float
requestStart: float
responseStart: float
responseEnd: float
class ScreencastActionStyle(TypedDict, total=False):
point: Optional[str]
highlight: Optional[str]
title: Optional[str]
class ScreencastFrame(TypedDict):
data: bytes
timestamp: float
viewportWidth: int
viewportHeight: int
class ScreencastSize(TypedDict):
width: int
height: int
class SecurityDetails(TypedDict):
issuer: Optional[str]
protocol: Optional[str]
subjectName: Optional[str]
validFrom: Optional[float]
validTo: Optional[float]
class SetCookieParam(TypedDict, total=False):
name: str
value: str
url: Optional[str]
domain: Optional[str]
path: Optional[str]
expires: Optional[float]
httpOnly: Optional[bool]
secure: Optional[bool]
sameSite: Optional[Literal["Lax", "None", "Strict"]]
partitionKey: Optional[str]
class SourceLocation(TypedDict):
url: str
line: int
column: int
lineNumber: int
columnNumber: int
class StorageStateCookie(TypedDict, total=False):
name: str
value: str
domain: str
path: str
expires: float
httpOnly: bool
secure: bool
sameSite: Literal["Lax", "None", "Strict"]
class StorageState(TypedDict, total=False):
cookies: List[StorageStateCookie]
origins: List[OriginState]
class TracingGroupLocation(TypedDict):
file: str
line: Optional[int]
column: Optional[int]
class ViewportSize(TypedDict):
width: int
height: int
class VirtualCredential(TypedDict):
id: str
rpId: str
userHandle: str
privateKey: str
publicKey: str
signCount: int
class WebErrorLocation(TypedDict):
url: str
line: int
column: int
class WebMCPToolAnnotations(TypedDict, total=False):
readOnly: Optional[bool]
untrustedContent: Optional[bool]
consequential: Optional[bool]
class WebMCPTool(TypedDict, total=False):
name: str
description: str
inputSchema: Optional[Any]
annotations: Optional[WebMCPToolAnnotations]
@@ -0,0 +1,87 @@
# 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 pathlib
from pathlib import Path
from typing import Dict, Optional, Union, cast
from playwright._impl._connection import ChannelOwner, from_channel
from playwright._impl._helper import Error, make_dirs_for_file, patch_error_message
from playwright._impl._stream import Stream
class Artifact(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self.absolute_path = initializer["absolutePath"]
async def path_after_finished(self) -> pathlib.Path:
if self._connection.is_remote:
raise Error(
"Path is not available when using browser_type.connect(). Use save_as() to save a local copy."
)
path = await self._channel.send(
"pathAfterFinished",
None,
)
return pathlib.Path(path)
async def save_as(self, path: Union[str, Path]) -> None:
stream = cast(
Stream,
from_channel(
await self._channel.send(
"saveAsStream",
None,
)
),
)
make_dirs_for_file(path)
await stream.save_as(path)
async def failure(self) -> Optional[str]:
reason = await self._channel.send(
"failure",
None,
)
if reason is None:
return None
return patch_error_message(reason)
async def delete(self) -> None:
await self._channel.send(
"delete",
None,
)
async def read_info_buffer(self) -> bytes:
stream = cast(
Stream,
from_channel(
await self._channel.send(
"stream",
None,
)
),
)
buffer = await stream.read_all()
return buffer
async def cancel(self) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
await self._channel.send(
"cancel",
None,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,103 @@
# 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
from contextlib import AbstractAsyncContextManager
from types import TracebackType
from typing import Any, Callable, Generic, Optional, Type, TypeVar, Union
from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper
mapping = ImplToApiMapping()
T = TypeVar("T")
Self = TypeVar("Self", bound="AsyncContextManager")
class AsyncEventInfo(Generic[T]):
def __init__(self, future: "asyncio.Future[T]") -> None:
self._future = future
@property
async def value(self) -> T:
return mapping.from_maybe_impl(await self._future)
def _cancel(self) -> None:
self._future.cancel()
def is_done(self) -> bool:
return self._future.done()
class AsyncEventContextManager(Generic[T], AbstractAsyncContextManager):
def __init__(self, future: "asyncio.Future[T]") -> None:
self._event = AsyncEventInfo[T](future)
async def __aenter__(self) -> AsyncEventInfo[T]:
return self._event
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
if exc_val:
self._event._cancel()
else:
await self._event.value
class AsyncBase(ImplWrapper):
def __init__(self, impl_obj: Any) -> None:
super().__init__(impl_obj)
self._loop = impl_obj._loop
def __str__(self) -> str:
return self._impl_obj.__str__()
def _wrap_handler(self, handler: Union[Callable[..., Any], Any]) -> Any:
if callable(handler):
return mapping.wrap_handler(handler)
return handler
def on(self, event: Any, f: Any) -> None:
"""Registers the function ``f`` to the event name ``event``."""
self._impl_obj.on(event, self._wrap_handler(f))
def once(self, event: Any, f: Any) -> None:
"""The same as ``self.on``, except that the listener is automatically
removed after being called.
"""
self._impl_obj.once(event, self._wrap_handler(f))
def remove_listener(self, event: Any, f: Any) -> None:
"""Removes the function ``f`` from ``event``."""
self._impl_obj.remove_listener(event, self._wrap_handler(f))
class AsyncContextManager(AsyncBase):
async def __aenter__(self: Self) -> Self:
return self
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
await self.close()
async def close(self) -> None: ...
@@ -0,0 +1,296 @@
# 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 pathlib import Path
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Dict,
List,
Optional,
Pattern,
Sequence,
Set,
Union,
cast,
)
from playwright._impl._api_structures import (
BrowserBindResult,
ClientCertificate,
Geolocation,
HttpCredentials,
ProxySettings,
StorageState,
ViewportSize,
)
from playwright._impl._artifact import Artifact
from playwright._impl._browser_context import BrowserContext
from playwright._impl._cdp_session import CDPSession
from playwright._impl._connection import ChannelOwner, from_channel
from playwright._impl._errors import is_target_closed_error
from playwright._impl._helper import (
ColorScheme,
Contrast,
ForcedColors,
HarContentPolicy,
HarMode,
ReducedMotion,
ServiceWorkersPolicy,
locals_to_params,
make_dirs_for_file,
)
from playwright._impl._page import Page
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._browser_type import BrowserType
class Browser(ChannelOwner):
Events = SimpleNamespace(
Context="context",
Disconnected="disconnected",
)
def __init__(
self, parent: "BrowserType", type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._browser_type: Optional["BrowserType"] = None
self._is_connected = True
self._should_close_connection_on_close = False
self._cr_tracing_path: Optional[str] = None
self._contexts: Set[BrowserContext] = set()
self._traces_dir: Optional[str] = None
self._channel.on(
"context",
lambda params: self._did_create_context(
cast(BrowserContext, from_channel(params["context"]))
),
)
self._channel.on("close", lambda _: self._on_close())
self._close_reason: Optional[str] = None
def __repr__(self) -> str:
return f"<Browser type={self._browser_type} version={self.version}>"
def _connect_to_browser_type(
self,
browser_type: "BrowserType",
traces_dir: Optional[str] = None,
) -> None:
# Note: when using connect(), `browserType` is different from `this.parent`.
# This is why browser type is not wired up in the constructor, and instead this separate method is called later on.
self._browser_type = browser_type
self._traces_dir = traces_dir
for context in self._contexts:
self._setup_browser_context(context)
def _did_create_context(self, context: BrowserContext) -> None:
context._browser = self
self._contexts.add(context)
# Note: when connecting to a browser, initial contexts arrive before `_browserType` is set,
# and will be configured later in `ConnectToBrowserType`.
if self._browser_type:
self._setup_browser_context(context)
self.emit(Browser.Events.Context, context)
def _setup_browser_context(self, context: BrowserContext) -> None:
context._tracing._traces_dir = self._traces_dir
context._request._tracing._traces_dir = self._traces_dir
assert self._browser_type is not None
self._browser_type._playwright.selectors._contexts_for_selectors.add(context)
def _on_close(self) -> None:
self._is_connected = False
self.emit(Browser.Events.Disconnected, self)
@property
def contexts(self) -> List[BrowserContext]:
return list(self._contexts)
@property
def browser_type(self) -> "BrowserType":
assert self._browser_type is not None
return self._browser_type
def is_connected(self) -> bool:
return self._is_connected
async def new_context(
self,
viewport: ViewportSize = None,
screen: ViewportSize = None,
noViewport: bool = None,
ignoreHTTPSErrors: bool = None,
javaScriptEnabled: bool = None,
bypassCSP: bool = None,
userAgent: str = None,
locale: str = None,
timezoneId: str = None,
geolocation: Geolocation = None,
permissions: Sequence[str] = None,
extraHTTPHeaders: Dict[str, str] = None,
offline: bool = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
deviceScaleFactor: float = None,
isMobile: bool = None,
hasTouch: bool = None,
colorScheme: ColorScheme = None,
reducedMotion: ReducedMotion = None,
forcedColors: ForcedColors = None,
contrast: Contrast = None,
acceptDownloads: bool = None,
defaultBrowserType: str = None,
proxy: ProxySettings = None,
recordHarPath: Union[Path, str] = None,
recordHarOmitContent: bool = None,
recordVideoDir: Union[Path, str] = None,
recordVideoSize: ViewportSize = None,
storageState: Union[StorageState, str, Path] = None,
baseURL: str = None,
strictSelectors: bool = None,
serviceWorkers: ServiceWorkersPolicy = None,
recordHarUrlFilter: Union[Pattern[str], str] = None,
recordHarMode: HarMode = None,
recordHarContent: HarContentPolicy = None,
clientCertificates: List[ClientCertificate] = None,
recordVideoFps: int = None,
) -> BrowserContext:
params = locals_to_params(locals())
assert self._browser_type is not None
await self._browser_type._prepare_browser_context_params(params)
channel = await self._channel.send("newContext", None, params)
context = cast(BrowserContext, from_channel(channel))
await context._initialize_har_from_options(
record_har_content=recordHarContent,
record_har_mode=recordHarMode,
record_har_omit_content=recordHarOmitContent,
record_har_path=recordHarPath,
record_har_url_filter=recordHarUrlFilter,
)
return context
async def new_page(
self,
viewport: ViewportSize = None,
screen: ViewportSize = None,
noViewport: bool = None,
ignoreHTTPSErrors: bool = None,
javaScriptEnabled: bool = None,
bypassCSP: bool = None,
userAgent: str = None,
locale: str = None,
timezoneId: str = None,
geolocation: Geolocation = None,
permissions: Sequence[str] = None,
extraHTTPHeaders: Dict[str, str] = None,
offline: bool = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
deviceScaleFactor: float = None,
isMobile: bool = None,
hasTouch: bool = None,
colorScheme: ColorScheme = None,
forcedColors: ForcedColors = None,
contrast: Contrast = None,
reducedMotion: ReducedMotion = None,
acceptDownloads: bool = None,
defaultBrowserType: str = None,
proxy: ProxySettings = None,
recordHarPath: Union[Path, str] = None,
recordHarOmitContent: bool = None,
recordVideoDir: Union[Path, str] = None,
recordVideoSize: ViewportSize = None,
storageState: Union[StorageState, str, Path] = None,
baseURL: str = None,
strictSelectors: bool = None,
serviceWorkers: ServiceWorkersPolicy = None,
recordHarUrlFilter: Union[Pattern[str], str] = None,
recordHarMode: HarMode = None,
recordHarContent: HarContentPolicy = None,
clientCertificates: List[ClientCertificate] = None,
recordVideoFps: int = None,
) -> Page:
params = locals_to_params(locals())
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, title="Create page")
async def close(self, reason: str = None) -> None:
self._close_reason = reason
try:
if self._should_close_connection_on_close:
await self._connection.stop_async()
else:
await self._channel.send("close", None, {"reason": reason})
except Exception as e:
if not is_target_closed_error(e):
raise e
@property
def version(self) -> str:
return self._initializer["version"]
async def new_browser_cdp_session(self) -> CDPSession:
return from_channel(await self._channel.send("newBrowserCDPSession", None))
async def bind(
self,
title: str,
workspaceDir: str = None,
host: str = None,
port: int = None,
) -> BrowserBindResult:
return await self._channel.send_return_as_dict(
"startServer", None, locals_to_params(locals())
)
async def unbind(self) -> None:
await self._channel.send("stopServer", None)
async def start_tracing(
self,
page: Page = None,
path: Union[str, Path] = None,
screenshots: bool = None,
categories: Sequence[str] = None,
) -> None:
params = locals_to_params(locals())
if page:
params["page"] = page._channel
if path:
self._cr_tracing_path = str(path)
params["path"] = str(path)
await self._channel.send("startTracing", None, params)
async def stop_tracing(self) -> bytes:
artifact = cast(
Artifact, from_channel(await self._channel.send("stopTracing", None))
)
buffer = await artifact.read_info_buffer()
await artifact.delete()
if self._cr_tracing_path:
make_dirs_for_file(self._cr_tracing_path)
with open(self._cr_tracing_path, "wb") as f:
f.write(buffer)
self._cr_tracing_path = None
return buffer
@@ -0,0 +1,750 @@
# 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 json
from pathlib import Path
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Pattern,
Sequence,
Set,
Union,
cast,
)
from playwright._impl._api_structures import (
Cookie,
Geolocation,
SetCookieParam,
StorageState,
WebErrorLocation,
)
from playwright._impl._cdp_session import CDPSession
from playwright._impl._clock import Clock
from playwright._impl._connection import (
ChannelOwner,
from_channel,
from_nullable_channel,
)
from playwright._impl._console_message import ConsoleMessage
from playwright._impl._credentials import Credentials
from playwright._impl._debugger import Debugger
from playwright._impl._dialog import Dialog
from playwright._impl._disposable import Disposable, DisposableStub
from playwright._impl._errors import Error, TargetClosedError
from playwright._impl._event_context_manager import EventContextManagerImpl
from playwright._impl._fetch import APIRequestContext
from playwright._impl._frame import Frame
from playwright._impl._har_router import HarRouter
from playwright._impl._helper import (
HarContentPolicy,
HarMode,
RouteFromHarNotFoundPolicy,
RouteHandler,
RouteHandlerCallback,
TimeoutSettings,
URLMatch,
WebSocketRouteHandlerCallback,
async_readfile,
async_writefile,
create_task_and_ignore_exception,
locals_to_params,
parse_error,
to_impl,
)
from playwright._impl._network import (
Request,
Response,
Route,
WebSocketRoute,
WebSocketRouteHandler,
serialize_headers,
to_clear_cookies_params,
)
from playwright._impl._page import BindingCall, Page, Worker
from playwright._impl._tracing import Tracing
from playwright._impl._waiter import Waiter
from playwright._impl._web_error import WebError
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._browser import Browser
class BrowserContext(ChannelOwner):
Events = SimpleNamespace(
# Deprecated in v1.56, never emitted anymore.
BackgroundPage="backgroundpage",
Close="close",
Console="console",
Dialog="dialog",
DialogClosed="dialogclosed",
Download="download",
FrameAttached="frameattached",
FrameDetached="framedetached",
FrameNavigated="framenavigated",
Page="page",
PageClose="pageclose",
PageLoad="pageload",
WebError="weberror",
ServiceWorker="serviceworker",
Request="request",
Response="response",
RequestFailed="requestfailed",
RequestFinished="requestfinished",
)
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
# Browser is null for browser contexts created outside of normal browser, e.g. android or electron.
# circular import workaround:
self._browser: Optional["Browser"] = None
if parent.__class__.__name__ == "Browser":
self._browser = cast("Browser", parent)
self._pages: List[Page] = []
self._routes: List[RouteHandler] = []
self._web_socket_routes: List[WebSocketRouteHandler] = []
self._bindings: Dict[str, Any] = {}
self._timeout_settings = TimeoutSettings(None)
self._owner_page: Optional[Page] = None
self._options: Dict[str, Any] = initializer["options"]
self._service_workers: Set[Worker] = set()
self._base_url: Optional[str] = self._options.get("baseURL")
self._videos_dir: Optional[str] = self._options.get("recordVideo")
self._tracing = cast(Tracing, from_channel(initializer["tracing"]))
self._debugger: Debugger = cast(Debugger, from_channel(initializer["debugger"]))
self._request: APIRequestContext = from_channel(initializer["requestContext"])
self._request._timeout_settings = self._timeout_settings
self._clock = Clock(self)
self._credentials = Credentials(self)
self._channel.on(
"bindingCall",
lambda params: self._on_binding(from_channel(params["binding"])),
)
self._channel.on("close", lambda _: self._on_close())
self._channel.on(
"page", lambda params: self._on_page(from_channel(params["page"]))
)
self._channel.on(
"route",
lambda params: self._loop.create_task(
self._on_route(
from_channel(params.get("route")),
)
),
)
self._channel.on(
"webSocketRoute",
lambda params: self._loop.create_task(
self._on_web_socket_route(
from_channel(params["webSocketRoute"]),
)
),
)
self._channel.on(
"serviceWorker",
lambda params: self._on_service_worker(from_channel(params["worker"])),
)
self._channel.on(
"console",
lambda event: self._on_console_message(event),
)
self._channel.on(
"dialog", lambda params: self._on_dialog(from_channel(params["dialog"]))
)
self._channel.on(
"dialogClosed",
lambda params: self._on_dialog_closed(from_channel(params["dialog"])),
)
self._channel.on(
"pageError",
lambda params: self._on_page_error(
parse_error(params["error"]["error"]),
from_nullable_channel(params["page"]),
cast(
WebErrorLocation,
params.get("location") or {"url": "", "line": 0, "column": 0},
),
),
)
self._channel.on(
"request",
lambda params: self._on_request(
from_channel(params["request"]),
from_nullable_channel(params.get("page")),
),
)
self._channel.on(
"response",
lambda params: self._on_response(
from_channel(params["response"]),
from_nullable_channel(params.get("page")),
),
)
self._channel.on(
"requestFailed",
lambda params: self._on_request_failed(
from_channel(params["request"]),
params["responseEndTiming"],
params.get("failureText"),
from_nullable_channel(params.get("page")),
),
)
self._channel.on(
"requestFinished",
lambda params: self._on_request_finished(
from_channel(params["request"]),
from_nullable_channel(params.get("response")),
params["responseEndTiming"],
from_nullable_channel(params.get("page")),
),
)
self._closed_future: asyncio.Future = asyncio.Future()
self.once(
self.Events.Close, lambda context: self._closed_future.set_result(True)
)
self._close_reason: Optional[str] = None
self._har_routers: List[HarRouter] = []
self._set_event_to_subscription_mapping(
{
BrowserContext.Events.Console: "console",
BrowserContext.Events.Dialog: "dialog",
BrowserContext.Events.DialogClosed: "dialogClosed",
BrowserContext.Events.Request: "request",
BrowserContext.Events.Response: "response",
BrowserContext.Events.RequestFinished: "requestFinished",
BrowserContext.Events.RequestFailed: "requestFailed",
}
)
self._closing_or_closed = False
def __repr__(self) -> str:
return f"<BrowserContext browser={self.browser}>"
def _on_page(self, page: Page) -> None:
self._pages.append(page)
self.emit(BrowserContext.Events.Page, page)
if page._opener and not page._opener.is_closed():
page._opener.emit(Page.Events.Popup, page)
async def _on_route(self, route: Route) -> None:
route._context = self
page = route.request._safe_page()
route_handlers = self._routes.copy()
for route_handler in route_handlers:
# If the page or the context was closed we stall all requests right away.
if (page and page._close_was_called) or self._closing_or_closed:
return
if not route_handler.matches(route.request.url):
continue
if route_handler not in self._routes:
continue
if route_handler.will_expire:
self._routes.remove(route_handler)
try:
handled = await route_handler.handle(route)
finally:
if len(self._routes) == 0:
create_task_and_ignore_exception(
self._loop,
self._connection.wrap_api_call(
lambda: self._update_interception_patterns(), True
),
)
if handled:
return
try:
# If the page is closed or unrouteAll() was called without waiting and interception disabled,
# the method will throw an error - silence it.
await route._inner_continue(True)
except Exception:
pass
async def _on_web_socket_route(self, web_socket_route: WebSocketRoute) -> None:
route_handler = next(
(
route_handler
for route_handler in self._web_socket_routes
if route_handler.matches(web_socket_route.url)
),
None,
)
if route_handler:
await route_handler.handle(web_socket_route)
else:
web_socket_route.connect_to_server()
def _on_binding(self, binding_call: BindingCall) -> None:
func = self._bindings.get(binding_call._initializer["name"])
if func is None:
return
asyncio.create_task(binding_call.call(func))
def set_default_navigation_timeout(self, timeout: float) -> None:
return self._set_default_navigation_timeout_impl(timeout)
def _set_default_navigation_timeout_impl(self, timeout: Optional[float]) -> None:
self._timeout_settings.set_default_navigation_timeout(timeout)
def set_default_timeout(self, timeout: float) -> None:
return self._set_default_timeout_impl(timeout)
def _set_default_timeout_impl(self, timeout: Optional[float]) -> None:
self._timeout_settings.set_default_timeout(timeout)
@property
def pages(self) -> List[Page]:
return self._pages.copy()
@property
def browser(self) -> Optional["Browser"]:
return self._browser
async def _initialize_har_from_options(
self,
record_har_path: Optional[Union[Path, str]],
record_har_content: Optional[HarContentPolicy],
record_har_omit_content: Optional[bool],
record_har_url_filter: Optional[Union[Pattern[str], str]],
record_har_mode: Optional[HarMode],
) -> None:
if not record_har_path:
return
record_har_path = str(record_har_path)
default_policy: HarContentPolicy = (
"attach" if record_har_path.endswith(".zip") else "embed"
)
content_policy: HarContentPolicy = record_har_content or (
"omit" if record_har_omit_content is True else default_policy
)
await self._tracing._record_into_har(
har=record_har_path,
page=None,
url=record_har_url_filter,
update_content=content_policy,
update_mode=(record_har_mode or "full"),
)
async def new_page(self) -> Page:
if self._owner_page:
raise Error("Please use browser.new_context()")
return from_channel(await self._channel.send("newPage", None))
async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]:
if urls is None:
urls = []
if isinstance(urls, str):
urls = [urls]
return await self._channel.send("cookies", None, dict(urls=urls))
async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None:
await self._channel.send("addCookies", None, dict(cookies=cookies))
async def clear_cookies(
self,
name: Union[str, Pattern[str]] = None,
domain: Union[str, Pattern[str]] = None,
path: Union[str, Pattern[str]] = None,
) -> None:
await self._channel.send(
"clearCookies", None, to_clear_cookies_params(name, domain, path)
)
async def grant_permissions(
self, permissions: Sequence[str], origin: str = None
) -> None:
await self._channel.send("grantPermissions", None, locals_to_params(locals()))
async def clear_permissions(self) -> None:
await self._channel.send("clearPermissions", None)
async def set_geolocation(self, geolocation: Geolocation = None) -> None:
await self._channel.send("setGeolocation", None, locals_to_params(locals()))
async def set_extra_http_headers(self, headers: Dict[str, str]) -> None:
await self._channel.send(
"setExtraHTTPHeaders", None, dict(headers=serialize_headers(headers))
)
async def set_offline(self, offline: bool) -> None:
await self._channel.send("setOffline", None, dict(offline=offline))
async def add_init_script(
self, script: str = None, path: Union[str, Path] = None
) -> Disposable:
if path:
script = (await async_readfile(path)).decode()
if not isinstance(script, str):
raise Error("Either path or script parameter must be specified")
return from_channel(
await self._channel.send("addInitScript", None, dict(source=script))
)
async def expose_binding(self, name: str, callback: Callable) -> Disposable:
for page in self._pages:
if name in page._bindings:
raise Error(
f'Function "{name}" has been already registered in one of the pages'
)
if name in self._bindings:
raise Error(f'Function "{name}" has been already registered')
self._bindings[name] = callback
return from_channel(
await self._channel.send("exposeBinding", None, dict(name=name))
)
async def expose_function(self, name: str, callback: Callable) -> Disposable:
return await self.expose_binding(name, lambda source, *args: callback(*args))
async def route(
self, url: URLMatch, handler: RouteHandlerCallback, times: int = None
) -> DisposableStub:
self._routes.insert(
0,
RouteHandler(
self._base_url,
url,
handler,
True if self._dispatcher_fiber else False,
times,
),
)
await self._update_interception_patterns()
return DisposableStub(lambda: self.unroute(url, handler), self)
async def unroute(
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None
) -> None:
removed = []
remaining = []
for route in self._routes:
if route.url != url or (handler and route.handler != handler):
remaining.append(route)
else:
removed.append(route)
await self._unroute_internal(removed, remaining, "default")
async def _unroute_internal(
self,
removed: List[RouteHandler],
remaining: List[RouteHandler],
behavior: Literal["default", "ignoreErrors", "wait"] = None,
) -> None:
self._routes = remaining
if behavior is not None and behavior != "default":
await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore
await self._update_interception_patterns()
async def route_web_socket(
self, url: URLMatch, handler: WebSocketRouteHandlerCallback
) -> None:
self._web_socket_routes.insert(
0,
WebSocketRouteHandler(self._base_url, url, handler),
)
await self._update_web_socket_interception_patterns()
def _dispose_har_routers(self) -> None:
for router in self._har_routers:
router.dispose()
self._har_routers = []
async def unroute_all(
self, behavior: Literal["default", "ignoreErrors", "wait"] = None
) -> None:
await self._unroute_internal(self._routes, [], behavior)
self._dispose_har_routers()
async def route_from_har(
self,
har: Union[Path, str],
url: Union[Pattern[str], str] = None,
notFound: RouteFromHarNotFoundPolicy = None,
update: bool = None,
updateContent: Literal["attach", "embed"] = None,
updateMode: HarMode = None,
) -> None:
if update:
await self._tracing._record_into_har(
har=har,
page=None,
url=url,
update_content=updateContent,
update_mode=updateMode,
)
return
router = await HarRouter.create(
local_utils=self._connection.local_utils,
file=str(har),
not_found_action=notFound or "abort",
url_matcher=url,
)
self._har_routers.append(router)
await router.add_context_route(self)
async def _update_interception_patterns(self) -> None:
patterns = RouteHandler.prepare_interception_patterns(self._routes)
await self._channel.send(
"setNetworkInterceptionPatterns", None, {"patterns": patterns}
)
async def _update_web_socket_interception_patterns(self) -> None:
patterns = WebSocketRouteHandler.prepare_interception_patterns(
self._web_socket_routes
)
await self._channel.send(
"setWebSocketInterceptionPatterns", None, {"patterns": patterns}
)
def expect_event(
self,
event: str,
predicate: Callable = None,
timeout: float = None,
) -> EventContextManagerImpl:
if timeout is None:
timeout = self._timeout_settings.timeout()
waiter = Waiter(self, f"browser_context.expect_event({event})")
waiter.reject_on_timeout(
timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"'
)
if event != BrowserContext.Events.Close:
waiter.reject_on_event(
self, BrowserContext.Events.Close, lambda: TargetClosedError()
)
waiter.wait_for_event(self, event, predicate)
return EventContextManagerImpl(waiter.result())
def _on_close(self) -> None:
self._closing_or_closed = True
if self._browser:
if self in self._browser._contexts:
self._browser._contexts.remove(self)
assert self._browser._browser_type is not None
if (
self
in self._browser._browser_type._playwright.selectors._contexts_for_selectors
):
self._browser._browser_type._playwright.selectors._contexts_for_selectors.remove(
self
)
self._dispose_har_routers()
self._tracing._reset_stack_counter()
self._request._tracing._reset_stack_counter()
self.emit(BrowserContext.Events.Close, self)
def is_closed(self) -> bool:
return self._closing_or_closed
async def close(self, reason: str = None) -> None:
if self._closing_or_closed:
return
self._close_reason = reason
self._closing_or_closed = True
await self.request.dispose(reason=reason)
async def _inner_close() -> None:
await self._tracing._export_all_hars()
await self._channel._connection.wrap_api_call(_inner_close, True)
await self._channel.send("close", None, {"reason": reason})
await self._closed_future
async def storage_state(
self,
path: Union[str, Path] = None,
indexedDB: bool = None,
opfs: bool = None,
credentials: bool = None,
) -> StorageState:
result = await self._channel.send_return_as_dict(
"storageState",
None,
{"indexedDB": indexedDB, "opfs": opfs, "credentials": credentials},
)
if path:
await async_writefile(path, json.dumps(result))
return result
async def set_storage_state(
self, storageState: Union[StorageState, str, Path]
) -> None:
if isinstance(storageState, (str, Path)):
state = json.loads(await async_readfile(storageState))
else:
state = storageState
await self._channel.send("setStorageState", None, {"storageState": state})
def _effective_close_reason(self) -> Optional[str]:
if self._close_reason:
return self._close_reason
if self._browser:
return self._browser._close_reason
return None
async def wait_for_event(
self, event: str, predicate: Callable = None, timeout: float = None
) -> Any:
async with self.expect_event(event, predicate, timeout) as event_info:
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,
timeout: float = None,
) -> EventContextManagerImpl[Page]:
return self.expect_event(BrowserContext.Events.Page, predicate, timeout)
def _on_service_worker(self, worker: Worker) -> None:
worker._context = self
self._service_workers.add(worker)
self.emit(BrowserContext.Events.ServiceWorker, worker)
def _on_request_failed(
self,
request: Request,
response_end_timing: float,
failure_text: Optional[str],
page: Optional[Page],
) -> None:
request._failure_text = failure_text
request._set_response_end_timing(response_end_timing)
self.emit(BrowserContext.Events.RequestFailed, request)
if page:
page.emit(Page.Events.RequestFailed, request)
def _on_request_finished(
self,
request: Request,
response: Optional[Response],
response_end_timing: float,
page: Optional[Page],
) -> None:
request._set_response_end_timing(response_end_timing)
self.emit(BrowserContext.Events.RequestFinished, request)
if page:
page.emit(Page.Events.RequestFinished, request)
if response:
response._finished_future.set_result(True)
def _on_console_message(self, event: Dict) -> None:
message = ConsoleMessage(event, self._loop, self._dispatcher_fiber)
worker = message.worker
if worker:
worker.emit(Worker.Events.Console, message)
page = message.page
if page:
page.emit(Page.Events.Console, message)
self.emit(BrowserContext.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":
create_task_and_ignore_exception(self._loop, dialog.accept())
else:
create_task_and_ignore_exception(self._loop, dialog.dismiss())
def _on_dialog_closed(self, dialog: Dialog) -> None:
self.emit(BrowserContext.Events.DialogClosed, dialog)
page = dialog.page
if page:
page.emit(Page.Events.DialogClosed, dialog)
def _on_page_error(
self, error: Error, page: Optional[Page], location: WebErrorLocation
) -> None:
self.emit(
BrowserContext.Events.WebError,
WebError(self._loop, self._dispatcher_fiber, page, error, location),
)
if page:
page.emit(Page.Events.PageError, error)
def _on_request(self, request: Request, page: Optional[Page]) -> None:
self.emit(BrowserContext.Events.Request, request)
if page:
page.emit(Page.Events.Request, request)
def _on_response(self, response: Response, page: Optional[Page]) -> None:
self.emit(BrowserContext.Events.Response, response)
if page:
page.emit(Page.Events.Response, response)
@property
def background_pages(self) -> List[Page]:
return []
@property
def service_workers(self) -> List[Worker]:
return list(self._service_workers)
async def new_cdp_session(self, page: Union[Page, Frame]) -> CDPSession:
page = to_impl(page)
params = {}
if isinstance(page, Page):
params["page"] = page._channel
elif isinstance(page, Frame):
params["frame"] = page._channel
else:
raise Error("page: expected Page or Frame")
return from_channel(await self._channel.send("newCDPSession", None, params))
@property
def tracing(self) -> Tracing:
return self._tracing
@property
def debugger(self) -> Debugger:
return self._debugger
@property
def request(self) -> "APIRequestContext":
return self._request
@property
def clock(self) -> Clock:
return self._clock
@property
def credentials(self) -> Credentials:
return self._credentials
@@ -0,0 +1,380 @@
# 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 json
import pathlib
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast
from playwright._impl._api_structures import (
ClientCertificate,
Geolocation,
HttpCredentials,
ProxySettings,
ViewportSize,
)
from playwright._impl._browser import Browser
from playwright._impl._browser_context import BrowserContext
from playwright._impl._connection import ChannelOwner, Connection, from_channel
from playwright._impl._errors import Error
from playwright._impl._helper import (
PLAYWRIGHT_MAX_DEADLINE,
ColorScheme,
Contrast,
Env,
ForcedColors,
HarContentPolicy,
HarMode,
ReducedMotion,
ServiceWorkersPolicy,
TimeoutSettings,
async_readfile,
locals_to_params,
)
from playwright._impl._json_pipe import JsonPipeTransport
from playwright._impl._network import (
serialize_headers,
to_client_certificates_protocol,
to_http_credentials_protocol,
)
from playwright._impl._waiter import throw_on_timeout
if TYPE_CHECKING:
from playwright._impl._playwright import Playwright
class BrowserType(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._playwright: "Playwright"
def __repr__(self) -> str:
return f"<BrowserType name={self.name} executable_path={self.executable_path}>"
@property
def name(self) -> str:
return self._initializer["name"]
@property
def executable_path(self) -> str:
return self._initializer["executablePath"]
async def launch(
self,
executablePath: Union[str, Path] = None,
channel: str = None,
args: Sequence[str] = None,
ignoreDefaultArgs: Union[bool, Sequence[str]] = None,
handleSIGINT: bool = None,
handleSIGTERM: bool = None,
handleSIGHUP: bool = None,
timeout: float = None,
env: Env = None,
headless: bool = None,
proxy: ProxySettings = None,
downloadsPath: Union[str, Path] = None,
slowMo: float = None,
tracesDir: Union[pathlib.Path, str] = None,
artifactsDir: Union[pathlib.Path, str] = None,
chromiumSandbox: bool = None,
firefoxUserPrefs: Dict[str, Union[str, float, bool]] = None,
) -> Browser:
params = locals_to_params(locals())
normalize_launch_params(params)
browser = cast(
Browser,
from_channel(
await self._channel.send(
"launch", TimeoutSettings.launch_timeout, params
)
),
)
browser._connect_to_browser_type(
self, str(tracesDir) if tracesDir is not None else None
)
return browser
async def launch_persistent_context(
self,
userDataDir: Union[str, Path],
channel: str = None,
executablePath: Union[str, Path] = None,
args: Sequence[str] = None,
ignoreDefaultArgs: Union[bool, Sequence[str]] = None,
handleSIGINT: bool = None,
handleSIGTERM: bool = None,
handleSIGHUP: bool = None,
timeout: float = None,
env: Env = None,
headless: bool = None,
proxy: ProxySettings = None,
downloadsPath: Union[str, Path] = None,
slowMo: float = None,
viewport: ViewportSize = None,
screen: ViewportSize = None,
noViewport: bool = None,
ignoreHTTPSErrors: bool = None,
javaScriptEnabled: bool = None,
bypassCSP: bool = None,
userAgent: str = None,
locale: str = None,
timezoneId: str = None,
geolocation: Geolocation = None,
permissions: Sequence[str] = None,
extraHTTPHeaders: Dict[str, str] = None,
offline: bool = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
deviceScaleFactor: float = None,
isMobile: bool = None,
hasTouch: bool = None,
colorScheme: ColorScheme = None,
reducedMotion: ReducedMotion = None,
forcedColors: ForcedColors = None,
contrast: Contrast = None,
acceptDownloads: bool = None,
tracesDir: Union[pathlib.Path, str] = None,
artifactsDir: Union[pathlib.Path, str] = None,
chromiumSandbox: bool = None,
firefoxUserPrefs: Dict[str, Union[str, float, bool]] = None,
recordHarPath: Union[Path, str] = None,
recordHarOmitContent: bool = None,
recordVideoDir: Union[Path, str] = None,
recordVideoSize: ViewportSize = None,
baseURL: str = None,
strictSelectors: bool = None,
serviceWorkers: ServiceWorkersPolicy = None,
recordHarUrlFilter: Union[Pattern[str], str] = None,
recordHarMode: HarMode = None,
recordHarContent: HarContentPolicy = None,
clientCertificates: List[ClientCertificate] = None,
recordVideoFps: int = None,
) -> BrowserContext:
userDataDir = self._user_data_dir(userDataDir)
params = locals_to_params(locals())
await self._prepare_browser_context_params(params)
normalize_launch_params(params)
result = await self._channel.send_return_as_dict(
"launchPersistentContext", TimeoutSettings.launch_timeout, params
)
browser = cast(
Browser,
from_channel(result["browser"]),
)
browser._connect_to_browser_type(
self, str(tracesDir) if tracesDir is not None else None
)
context = cast(BrowserContext, from_channel(result["context"]))
await context._initialize_har_from_options(
record_har_content=recordHarContent,
record_har_mode=recordHarMode,
record_har_omit_content=recordHarOmitContent,
record_har_path=recordHarPath,
record_har_url_filter=recordHarUrlFilter,
)
return context
def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str:
if not userDataDir:
return ""
if not Path(userDataDir).is_absolute():
return str(Path(userDataDir).resolve())
return str(Path(userDataDir))
async def connect_over_cdp(
self,
endpointURL: str,
timeout: float = None,
slowMo: float = None,
headers: Dict[str, str] = None,
isLocal: bool = None,
noDefaults: bool = None,
artifactsDir: Union[str, Path] = None,
) -> Browser:
params = locals_to_params(locals())
if params.get("headers"):
params["headers"] = serialize_headers(params["headers"])
response = await self._channel.send_return_as_dict(
"connectOverCDP", TimeoutSettings.launch_timeout, params
)
browser = cast(Browser, from_channel(response["browser"]))
browser._connect_to_browser_type(self, None)
return browser
async def connect(
self,
endpoint: str,
timeout: float = None,
slowMo: float = None,
headers: Dict[str, str] = None,
exposeNetwork: str = None,
) -> Browser:
if slowMo is None:
slowMo = 0
if timeout is None:
timeout = 0
headers = {**(headers if headers else {}), "x-playwright-browser": self.name}
local_utils = self._connection.local_utils
pipe_channel = (
await local_utils._channel.send_return_as_dict(
"connect",
lambda t: t or 0,
{
"endpoint": endpoint,
"headers": headers,
"slowMo": slowMo,
"timeout": timeout,
"exposeNetwork": exposeNetwork,
},
)
)["pipe"]
transport = JsonPipeTransport(self._connection._loop, pipe_channel)
connection = Connection(
self._connection._dispatcher_fiber,
self._connection._object_factory,
transport,
self._connection._loop,
local_utils=self._connection.local_utils,
)
connection.mark_as_remote()
browser = None
def handle_transport_close(reason: Optional[str]) -> None:
if browser:
for context in browser.contexts:
for page in context.pages:
page._on_close()
context._on_close()
connection.cleanup(reason)
# Give a chance to any API call promises to reject upon page/context closure.
# This happens naturally when we receive page.onClose and browser.onClose from the server
# in separate tasks. However, upon pipe closure we used to dispatch them all synchronously
# here and promises did not have a chance to reject.
# The order of rejects vs closure is a part of the API contract and our test runner
# relies on it to attribute rejections to the right test.
if browser:
connection._loop.call_soon(browser._on_close)
transport.once("close", handle_transport_close)
connection._is_sync = self._connection._is_sync
connection._loop.create_task(connection.run())
playwright_future = connection.playwright_future
timeout_future = throw_on_timeout(
timeout if timeout else PLAYWRIGHT_MAX_DEADLINE,
Error("Connection timed out"),
)
done, pending = await asyncio.wait(
{transport.on_error_future, playwright_future, timeout_future},
return_when=asyncio.FIRST_COMPLETED,
)
if not playwright_future.done():
playwright_future.cancel()
if not timeout_future.done():
timeout_future.cancel()
playwright: "Playwright" = next(iter(done)).result()
playwright._set_selectors(self._playwright.selectors)
self._connection._child_ws_connections.append(connection)
pre_launched_browser = playwright._initializer.get("preLaunchedBrowser")
assert pre_launched_browser
browser = cast(Browser, from_channel(pre_launched_browser))
browser._should_close_connection_on_close = True
browser._connect_to_browser_type(self, None)
return browser
async def _prepare_browser_context_params(self, params: Dict) -> None:
if params.get("noViewport"):
del params["noViewport"]
params["noDefaultViewport"] = True
if "defaultBrowserType" in params:
del params["defaultBrowserType"]
if "extraHTTPHeaders" in params:
params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"])
if "recordVideoDir" in params:
params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()}
if "recordVideoSize" in params:
params["recordVideo"]["size"] = params["recordVideoSize"]
del params["recordVideoSize"]
if "recordVideoFps" in params:
params["recordVideo"]["fps"] = params["recordVideoFps"]
del params["recordVideoDir"]
params.pop("recordVideoFps", None)
if "storageState" in params:
storageState = params["storageState"]
if not isinstance(storageState, dict):
params["storageState"] = json.loads(
(await async_readfile(storageState)).decode()
)
if params.get("colorScheme", None) == "null":
params["colorScheme"] = "no-override"
if params.get("reducedMotion", None) == "null":
params["reducedMotion"] = "no-override"
if params.get("forcedColors", None) == "null":
params["forcedColors"] = "no-override"
if params.get("contrast", None) == "null":
params["contrast"] = "no-override"
if "acceptDownloads" in params:
params["acceptDownloads"] = (
"accept" if params["acceptDownloads"] else "deny"
)
if "clientCertificates" in params:
params["clientCertificates"] = await to_client_certificates_protocol(
params["clientCertificates"]
)
if "httpCredentials" in params:
params["httpCredentials"] = to_http_credentials_protocol(
params["httpCredentials"]
)
params["selectorEngines"] = self._playwright.selectors._selector_engines
params["testIdAttributeName"] = (
self._playwright.selectors._test_id_attribute_name
)
# Remove HAR options
params.pop("recordHarPath", None)
params.pop("recordHarOmitContent", None)
params.pop("recordHarUrlFilter", None)
params.pop("recordHarMode", None)
params.pop("recordHarContent", None)
def normalize_launch_params(params: Dict) -> None:
if "env" in params:
params["env"] = [
{"name": name, "value": str(value)}
for [name, value] in params["env"].items()
]
if "ignoreDefaultArgs" in params:
if params["ignoreDefaultArgs"] is True:
params["ignoreAllDefaultArgs"] = True
del params["ignoreDefaultArgs"]
elif params["ignoreDefaultArgs"] is False:
del params["ignoreDefaultArgs"]
if "executablePath" in params:
params["executablePath"] = str(Path(params["executablePath"]))
if "downloadsPath" in params:
params["downloadsPath"] = str(Path(params["downloadsPath"]))
if "tracesDir" in params:
params["tracesDir"] = str(Path(params["tracesDir"]))
if "artifactsDir" in params:
params["artifactsDir"] = str(Path(params["artifactsDir"]))
@@ -0,0 +1,46 @@
# 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 Any, Dict
from playwright._impl._connection import ChannelOwner
from playwright._impl._helper import locals_to_params
class CDPSession(ChannelOwner):
Events = SimpleNamespace(
Event="event",
Close="close",
)
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))
self._channel.on("close", lambda _: self.emit(CDPSession.Events.Close, self))
def _on_event(self, params: Any) -> None:
self.emit(params["method"], params.get("params"))
self.emit(CDPSession.Events.Event, params)
async def send(self, method: str, params: Dict = None) -> Dict:
return await self._channel.send("send", None, locals_to_params(locals()))
async def detach(self) -> None:
await self._channel.send(
"detach",
None,
)
@@ -0,0 +1,104 @@
# 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 datetime
from typing import TYPE_CHECKING, Dict, Union
if TYPE_CHECKING:
from playwright._impl._browser_context import BrowserContext
class Clock:
def __init__(self, browser_context: "BrowserContext") -> None:
self._browser_context = browser_context
self._loop = browser_context._loop
self._dispatcher_fiber = browser_context._dispatcher_fiber
async def install(self, time: Union[float, str, datetime.datetime] = None) -> None:
await self._browser_context._channel.send(
"clockInstall",
None,
parse_time(time) if time is not None else {},
)
async def fast_forward(
self,
ticks: Union[int, str],
) -> None:
await self._browser_context._channel.send(
"clockFastForward",
None,
parse_ticks(ticks),
)
async def pause_at(
self,
time: Union[float, str, datetime.datetime],
) -> None:
await self._browser_context._channel.send(
"clockPauseAt",
None,
parse_time(time),
)
async def resume(
self,
) -> None:
await self._browser_context._channel.send("clockResume", None)
async def run_for(
self,
ticks: Union[int, str],
) -> None:
await self._browser_context._channel.send(
"clockRunFor",
None,
parse_ticks(ticks),
)
async def set_fixed_time(
self,
time: Union[float, str, datetime.datetime],
) -> None:
await self._browser_context._channel.send(
"clockSetFixedTime",
None,
parse_time(time),
)
async def set_system_time(
self,
time: Union[float, str, datetime.datetime],
) -> None:
await self._browser_context._channel.send(
"clockSetSystemTime",
None,
parse_time(time),
)
def parse_time(
time: Union[float, str, datetime.datetime],
) -> Dict[str, Union[int, str]]:
if isinstance(time, (float, int)):
return {"timeNumber": int(time * 1_000)}
if isinstance(time, str):
return {"timeString": time}
return {"timeNumber": int(time.timestamp() * 1_000)}
def parse_ticks(ticks: Union[int, str]) -> Dict[str, Union[int, str]]:
if isinstance(ticks, int):
return {"ticksNumber": ticks}
return {"ticksString": ticks}
@@ -0,0 +1,768 @@
# 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 collections.abc
import contextvars
import datetime
import inspect
import random
import string
import sys
import traceback
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Mapping,
Optional,
Tuple,
TypedDict,
Union,
cast,
)
from pyee import EventEmitter
from pyee.asyncio import AsyncIOEventEmitter
import playwright
import playwright._impl._impl_to_api_mapping
from playwright._impl._errors import TargetClosedError, rewrite_error
from playwright._impl._greenlets import EventGreenlet
from playwright._impl._helper import (
Error,
ParsedMessagePayload,
create_task_and_ignore_exception,
parse_error,
)
from playwright._impl._transport import Transport
if TYPE_CHECKING:
from playwright._impl._local_utils import LocalUtils
from playwright._impl._playwright import Playwright
TimeoutCalculator = Optional[Callable[[Optional[float]], float]]
_PLAYWRIGHT_MODULE_PATH = str(Path(playwright.__file__).parents[0])
class Channel(AsyncIOEventEmitter):
def __init__(self, connection: "Connection", object: "ChannelOwner") -> None:
super().__init__()
self._connection = connection
self._guid = object._guid
self._object = object
self.on("error", lambda exc: self._connection._on_event_listener_error(exc))
async def send(
self,
method: str,
timeout_calculator: TimeoutCalculator,
params: Dict = None,
is_internal: bool = False,
title: str = None,
) -> Any:
return await self._connection.wrap_api_call(
lambda: self._inner_send(method, timeout_calculator, params, False),
is_internal,
title,
)
def send_may_fail(
self,
method: str,
timeout_calculator: TimeoutCalculator,
params: Dict = None,
is_internal: bool = False,
title: str = None,
) -> None:
create_task_and_ignore_exception(
self._connection._loop,
self.send(method, timeout_calculator, params, is_internal, title),
)
async def send_return_as_dict(
self,
method: str,
timeout_calculator: TimeoutCalculator,
params: Dict = None,
is_internal: bool = False,
title: str = None,
) -> Any:
return await self._connection.wrap_api_call(
lambda: self._inner_send(method, timeout_calculator, params, True),
is_internal,
title,
)
def send_no_reply(
self,
method: str,
timeout_calculator: TimeoutCalculator,
params: Dict = None,
is_internal: bool = False,
title: str = None,
) -> None:
# No reply messages are used to e.g. __waitInfo__(after).
augmented_params, timeout = _augment_params(params, timeout_calculator)
self._connection.wrap_api_call_sync(
lambda: self._connection._send_message_to_server(
self._object,
method,
augmented_params,
timeout,
True,
),
is_internal,
title,
)
async def _inner_send(
self,
method: str,
timeout_calculator: TimeoutCalculator,
params: Optional[Dict],
return_as_dict: bool,
) -> Any:
if self._connection._error:
error = self._connection._error
self._connection._error = None
raise error
augmented_params, timeout = _augment_params(params, timeout_calculator)
callback = self._connection._send_message_to_server(
self._object, method, augmented_params, timeout
)
try:
done, _ = await asyncio.wait(
{
self._connection._transport.on_error_future,
callback.future,
},
return_when=asyncio.FIRST_COMPLETED,
)
except asyncio.CancelledError as exc:
await self._connection._abort(
self._object,
callback,
str(exc) or "Task was cancelled",
)
raise
if not callback.future.done():
callback.future.cancel()
result = next(iter(done)).result()
# Protocol now has named return values, assume result is one level deeper unless
# there is explicit ambiguity.
if not result:
return None
assert isinstance(result, dict)
if return_as_dict:
return result
if len(result) == 0:
return None
assert len(result) == 1
key = next(iter(result))
return result[key]
class ChannelOwner(AsyncIOEventEmitter):
def __init__(
self,
parent: Union["ChannelOwner", "Connection"],
type: str,
guid: str,
initializer: Dict,
) -> None:
super().__init__(loop=parent._loop)
self._loop: asyncio.AbstractEventLoop = parent._loop
self._dispatcher_fiber: Any = parent._dispatcher_fiber
self._type = type
self._guid: str = guid
self._connection: Connection = (
parent._connection if isinstance(parent, ChannelOwner) else parent
)
self._parent: Optional[ChannelOwner] = (
parent if isinstance(parent, ChannelOwner) else None
)
self._objects: Dict[str, "ChannelOwner"] = {}
self._channel: Channel = Channel(self._connection, self)
self._initializer = initializer
self._was_collected = False
self._connection._objects[guid] = self
if self._parent:
self._parent._objects[guid] = self
self._event_to_subscription_mapping: Dict[str, str] = {}
def _dispose(self, reason: Optional[str]) -> None:
# Clean up from parent and connection.
if self._parent:
del self._parent._objects[self._guid]
del self._connection._objects[self._guid]
self._was_collected = reason == "gc"
# Dispose all children.
for object in list(self._objects.values()):
object._dispose(reason)
self._objects.clear()
def _adopt(self, child: "ChannelOwner") -> None:
del cast("ChannelOwner", child._parent)._objects[child._guid]
self._objects[child._guid] = child
child._parent = self
def _set_event_to_subscription_mapping(self, mapping: Dict[str, str]) -> None:
self._event_to_subscription_mapping = mapping
def _update_subscription(self, event: str, enabled: bool) -> None:
protocol_event = self._event_to_subscription_mapping.get(event)
if protocol_event:
self._connection.wrap_api_call_sync(
lambda: self._channel.send_no_reply(
"updateSubscription",
None,
{"event": protocol_event, "enabled": enabled},
),
True,
)
def _add_event_handler(self, event: str, k: Any, v: Any) -> None:
if not self.listeners(event):
self._update_subscription(event, True)
super()._add_event_handler(event, k, v)
def remove_listener(self, event: str, f: Any) -> None:
super().remove_listener(event, f)
if not self.listeners(event):
self._update_subscription(event, False)
class ProtocolCallback:
def __init__(
self, loop: asyncio.AbstractEventLoop, id: str, no_reply: bool = False
) -> None:
self.id = id
self.stack_trace: traceback.StackSummary
self.no_reply = no_reply
self.future = loop.create_future()
if no_reply:
self.future.set_result(None)
# The outer task can get cancelled by the user, this forwards the cancellation to the inner task.
current_task = asyncio.current_task()
def cb(task: asyncio.Task) -> None:
if current_task:
current_task.remove_done_callback(cb)
if task.cancelled():
self.future.cancel()
if current_task:
current_task.add_done_callback(cb)
self.future.add_done_callback(
lambda _: (
current_task.remove_done_callback(cb) if current_task else None
)
)
class RootChannelOwner(ChannelOwner):
def __init__(self, connection: "Connection") -> None:
super().__init__(connection, "Root", "", {})
async def initialize(self) -> "Playwright":
return from_channel(
await self._channel.send(
"initialize",
None,
{
"sdkLanguage": "python",
},
)
)
class Connection(EventEmitter):
def __init__(
self,
dispatcher_fiber: Any,
object_factory: Callable[[ChannelOwner, str, str, Dict], ChannelOwner],
transport: Transport,
loop: asyncio.AbstractEventLoop,
local_utils: Optional["LocalUtils"] = None,
) -> None:
super().__init__()
self._dispatcher_fiber = dispatcher_fiber
self._transport = transport
self._transport.on_message = lambda msg: self.dispatch(msg)
# Call ids double as trace call ids, so they must be unique across connections.
self._call_id_prefix = "".join(random.choices(string.ascii_lowercase, k=4))
self._last_id = 0
self._objects: Dict[str, ChannelOwner] = {}
self._callbacks: Dict[str, ProtocolCallback] = {}
self._object_factory = object_factory
self._is_sync = False
self._child_ws_connections: List["Connection"] = []
self._loop = loop
self.playwright_future: asyncio.Future["Playwright"] = loop.create_future()
self._error: Optional[BaseException] = None
self.is_remote = False
self._init_task: Optional[asyncio.Task] = None
self._api_zone: contextvars.ContextVar[Optional[ParsedStackTrace]] = (
contextvars.ContextVar("ApiZone", default=None)
)
self._local_utils: Optional["LocalUtils"] = local_utils
self._tracing_count = 0
self._closed_error: Optional[Exception] = None
@property
def local_utils(self) -> "LocalUtils":
assert self._local_utils
return self._local_utils
def mark_as_remote(self) -> None:
self.is_remote = True
async def run_as_sync(self) -> None:
self._is_sync = True
await self.run()
async def run(self) -> None:
self._loop = asyncio.get_running_loop()
self._root_object = RootChannelOwner(self)
async def init() -> None:
try:
result = await self._root_object.initialize()
if not self.playwright_future.done():
self.playwright_future.set_result(result)
except Exception as exc:
# No re-raise: callers observe playwright_future; a task
# exception would log "never retrieved". Skip set_* if async
# __aenter__ already cancelled the future after a transport error.
if not self.playwright_future.done():
self.playwright_future.set_exception(exc)
await self._transport.connect()
self._init_task = self._loop.create_task(init())
await self._transport.run()
def stop_sync(self) -> None:
self._transport.request_stop()
self._dispatcher_fiber.switch()
self._loop.run_until_complete(self._transport.wait_until_stopped())
self.cleanup()
async def stop_async(self) -> None:
self._transport.request_stop()
await self._transport.wait_until_stopped()
self.cleanup()
def cleanup(self, cause: str = None) -> None:
self._closed_error = TargetClosedError(cause) if cause else TargetClosedError()
if self._init_task and not self._init_task.done():
self._init_task.cancel()
for ws_connection in self._child_ws_connections:
ws_connection._transport.dispose()
for callback in self._callbacks.values():
# To prevent 'Future exception was never retrieved' we ignore all callbacks that are no_reply.
if callback.no_reply:
continue
if callback.future.cancelled():
continue
callback.future.set_exception(self._closed_error)
self._callbacks.clear()
self.emit("close")
def set_is_tracing(self, is_tracing: bool) -> None:
if is_tracing:
self._tracing_count += 1
else:
self._tracing_count -= 1
def _send_message_to_server(
self,
object: ChannelOwner,
method: str,
params: Dict,
timeout: float,
no_reply: bool = False,
) -> ProtocolCallback:
if self._closed_error:
raise self._closed_error
if object._was_collected:
raise Error(
"The object has been collected to prevent unbounded heap growth."
)
self._last_id += 1
id = f"{self._call_id_prefix}@{self._last_id}"
callback = ProtocolCallback(self._loop, id, no_reply=no_reply)
task = asyncio.current_task(self._loop)
callback.stack_trace = cast(
traceback.StackSummary,
getattr(task, "__pw_stack_trace__", None)
or traceback.extract_stack(limit=10),
)
callback.no_reply = no_reply
stack_trace_information = cast(ParsedStackTrace, self._api_zone.get())
frames = stack_trace_information.get("frames", [])
location = (
{
"file": frames[0]["file"],
"line": frames[0]["line"],
"column": frames[0]["column"],
}
if frames
else None
)
metadata = {
"wallTime": int(datetime.datetime.now().timestamp() * 1000),
"apiName": stack_trace_information["apiName"],
"internal": not stack_trace_information["apiName"],
"timeout": timeout,
}
if location:
metadata["location"] = location # type: ignore
title = stack_trace_information["title"]
if title:
metadata["title"] = title
message = {
"id": id,
"guid": object._guid,
"method": method,
"params": self._replace_channels_with_guids(params),
"metadata": metadata,
}
if self._tracing_count > 0 and frames and object._guid != "localUtils":
self.local_utils.add_stack_to_tracing_no_reply(id, frames)
self._transport.send(message)
self._callbacks[id] = callback
return callback
async def _abort(
self, object: ChannelOwner, callback: ProtocolCallback, reason: str
) -> None:
try:
self._transport.send(
{
"guid": object._guid,
"method": "__abort__",
"params": {"id": callback.id, "reason": reason},
}
)
except (Error, OSError):
pass
try:
done, _ = await asyncio.wait(
{
self._transport.on_error_future,
callback.future,
},
return_when=asyncio.FIRST_COMPLETED,
)
finally:
if not callback.future.done():
callback.future.cancel()
for future in done:
if not future.cancelled():
future.exception()
def dispatch(self, msg: ParsedMessagePayload) -> None:
if self._closed_error:
return
id = msg.get("id")
if id:
callback = self._callbacks.pop(id)
if callback.future.cancelled():
return
# No reply messages are used to e.g. __waitInfo__(after) which returns exceptions on page close.
# To prevent 'Future exception was never retrieved' we just ignore such messages.
if callback.no_reply:
return
error = msg.get("error")
if error and not msg.get("result"):
parsed_error = parse_error(
error["error"], format_call_log(msg.get("log")) # type: ignore
)
parsed_error._log = msg.get("log")
parsed_error._details = self._replace_guids_with_channels(
msg.get("errorDetails")
)
parsed_error._stack = "".join(callback.stack_trace.format())
callback.future.set_exception(parsed_error)
else:
result = self._replace_guids_with_channels(msg.get("result"))
callback.future.set_result(result)
return
guid = msg["guid"]
method = msg["method"]
params = msg.get("params")
if method == "__create__":
assert params
parent = self._objects[guid]
self._create_remote_object(
parent, params["type"], params["guid"], params["initializer"]
)
return
object = self._objects.get(guid)
if not object:
raise Exception(f'Cannot find object to "{method}": {guid}')
if method == "__adopt__":
child_guid = cast(Dict[str, str], params)["guid"]
child = self._objects.get(child_guid)
if not child:
raise Exception(f"Unknown new child: {child_guid}")
object._adopt(child)
return
if method == "__dispose__":
assert isinstance(params, dict)
self._objects[guid]._dispose(cast(Optional[str], params.get("reason")))
return
object = self._objects[guid]
should_replace_guids_with_channels = "jsonPipe@" not in guid
try:
if self._is_sync:
for listener in object._channel.listeners(method):
# Event handlers like route/locatorHandlerTriggered require us to perform async work.
# In order to report their potential errors to the user, we need to catch it and store it in the connection
def _done_callback(future: asyncio.Future) -> None:
exc = future.exception()
if exc:
self._on_event_listener_error(exc)
def _listener_with_error_handler_attached(params: Any) -> None:
potential_future = listener(params)
if asyncio.isfuture(potential_future):
potential_future.add_done_callback(_done_callback)
# Each event handler is a potentilly blocking context, create a fiber for each
# and switch to them in order, until they block inside and pass control to each
# other and then eventually back to dispatcher as listener functions return.
g = EventGreenlet(_listener_with_error_handler_attached)
if should_replace_guids_with_channels:
g.switch(self._replace_guids_with_channels(params))
else:
g.switch(params)
else:
if should_replace_guids_with_channels:
object._channel.emit(
method, self._replace_guids_with_channels(params)
)
else:
object._channel.emit(method, params)
except BaseException as exc:
self._on_event_listener_error(exc)
def _on_event_listener_error(self, exc: BaseException) -> None:
print("Error occurred in event listener", file=sys.stderr)
traceback.print_exception(type(exc), exc, exc.__traceback__, file=sys.stderr)
# Save the error to throw at the next API call. This "replicates" unhandled rejection in Node.js.
self._error = exc
def _create_remote_object(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> ChannelOwner:
initializer = self._replace_guids_with_channels(initializer)
return self._object_factory(parent, type, guid, initializer)
def _replace_channels_with_guids(
self,
payload: Any,
) -> Any:
if payload is None:
return payload
if isinstance(payload, Path):
return str(payload)
if isinstance(payload, collections.abc.Sequence) and not isinstance(
payload, str
):
return list(map(self._replace_channels_with_guids, payload))
if isinstance(payload, Channel):
return dict(guid=payload._guid)
if isinstance(payload, dict):
result = {}
for key, value in payload.items():
result[key] = self._replace_channels_with_guids(value)
return result
return payload
def _replace_guids_with_channels(self, payload: Any) -> Any:
if payload is None:
return payload
if isinstance(payload, list):
return list(map(self._replace_guids_with_channels, payload))
if isinstance(payload, dict):
if payload.get("guid") in self._objects:
return self._objects[payload["guid"]]._channel
result = {}
for key, value in payload.items():
result[key] = self._replace_guids_with_channels(value)
return result
return payload
async def wrap_api_call(
self, cb: Callable[[], Any], is_internal: bool = False, title: str = None
) -> Any:
if self._api_zone.get():
return await cb()
task = asyncio.current_task(self._loop)
parsed_st = _attach_api_call_information(
getattr(task, "__pw_stack__", None) or _capture_stack_trace(),
is_internal,
title,
)
self._api_zone.set(parsed_st)
try:
return await cb()
except Exception as error:
raise rewrite_error(error, f"{parsed_st['apiName']}: {error}") from None
finally:
self._api_zone.set(None)
def wrap_api_call_sync(
self, cb: Callable[[], Any], is_internal: bool = False, title: str = None
) -> Any:
if self._api_zone.get():
return cb()
task = asyncio.current_task(self._loop)
parsed_st = _attach_api_call_information(
getattr(task, "__pw_stack__", None) or _capture_stack_trace(),
is_internal,
title,
)
self._api_zone.set(parsed_st)
try:
return cb()
except Exception as error:
raise rewrite_error(error, f"{parsed_st['apiName']}: {error}") from None
finally:
self._api_zone.set(None)
def from_channel(channel: Channel) -> Any:
return channel._object
def from_nullable_channel(channel: Optional[Channel]) -> Optional[Any]:
return channel._object if channel else None
class StackFrame(TypedDict):
file: str
line: int
column: int
function: Optional[str]
class ParsedStackTrace(TypedDict):
frames: List[StackFrame]
apiName: Optional[str]
title: Optional[str]
def _capture_stack_trace() -> ParsedStackTrace:
frame = inspect.currentframe()
# Skip this helper and the caller that only captures the stack.
frame = frame.f_back.f_back if frame and frame.f_back else None
last_internal_api_name = ""
api_name = ""
parsed_frames: List[StackFrame] = []
while frame:
code = frame.f_code
filename = code.co_filename
# Sync and Async implementations can have event handlers. When these are sync, they
# get evaluated in the context of the event loop, so they contain the stack trace of when
# the message was received. _impl_to_api_mapping is glue between the user-code and internal
# code to translate impl classes to api classes. We want to ignore these frames.
if playwright._impl._impl_to_api_mapping.__file__ == filename:
frame = frame.f_back
continue
is_playwright_internal = filename.startswith(_PLAYWRIGHT_MODULE_PATH)
method_name = ""
if "self" in frame.f_locals:
method_name = frame.f_locals["self"].__class__.__name__ + "."
method_name += code.co_name
if not is_playwright_internal:
parsed_frames.append(
{
"file": filename,
"line": frame.f_lineno,
"column": 0,
"function": method_name,
}
)
if is_playwright_internal:
last_internal_api_name = method_name
elif last_internal_api_name:
api_name = last_internal_api_name
last_internal_api_name = ""
frame = frame.f_back
if not api_name:
api_name = last_internal_api_name
return {
"frames": parsed_frames,
"apiName": api_name,
"title": None,
}
def _attach_api_call_information(
stack_trace_information: ParsedStackTrace,
is_internal: bool,
title: Optional[str],
) -> ParsedStackTrace:
if is_internal:
stack_trace_information["apiName"] = ""
stack_trace_information["title"] = title
return stack_trace_information
def _augment_params(
params: Optional[Dict],
timeout_calculator: Optional[Callable[[Optional[float]], float]],
) -> Tuple[Dict, float]:
if params is None:
params = {}
timeout_param = params.pop("timeout", None)
timeout: float = 0
if timeout_calculator:
timeout = timeout_calculator(timeout_param)
return _filter_none(params), timeout
def _filter_none(d: Mapping) -> Dict:
result = {}
for k, v in d.items():
if v is None:
continue
result[k] = _filter_none(v) if isinstance(v, dict) else v
return result
def format_call_log(log: Optional[List[str]]) -> str:
if not log:
return ""
if len(list(filter(lambda x: x.strip(), log))) == 0:
return ""
return "\nCall log:\n" + "\n".join(log) + "\n"
@@ -0,0 +1,98 @@
# 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 asyncio import AbstractEventLoop
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union
from playwright._impl._api_structures import SourceLocation
from playwright._impl._connection import from_channel, from_nullable_channel
from playwright._impl._js_handle import JSHandle
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._page import Page
from playwright._impl._worker import Worker
class ConsoleMessage:
def __init__(
self, event: Dict, loop: AbstractEventLoop, dispatcher_fiber: Any
) -> None:
self._event = event
self._loop = loop
self._dispatcher_fiber = dispatcher_fiber
self._page: Optional["Page"] = from_nullable_channel(event.get("page"))
self._worker: Optional["Worker"] = from_nullable_channel(event.get("worker"))
def __repr__(self) -> str:
return f"<ConsoleMessage type={self.type} text={self.text}>"
def __str__(self) -> str:
return self.text
@property
def type(self) -> Union[
Literal["assert"],
Literal["clear"],
Literal["count"],
Literal["debug"],
Literal["dir"],
Literal["dirxml"],
Literal["endGroup"],
Literal["error"],
Literal["info"],
Literal["log"],
Literal["profile"],
Literal["profileEnd"],
Literal["startGroup"],
Literal["startGroupCollapsed"],
Literal["table"],
Literal["time"],
Literal["timeEnd"],
Literal["trace"],
Literal["warning"],
]:
return self._event["type"]
@property
def text(self) -> str:
return self._event["text"]
@property
def args(self) -> List[JSHandle]:
return list(map(from_channel, self._event["args"]))
@property
def location(self) -> SourceLocation:
# Wire format uses `lineNumber`/`columnNumber`; docs expose both `line`/`column`
# (legacy) and `lineNumber`/`columnNumber` (added upstream in 1.60).
loc = self._event["location"]
return {
"url": loc["url"],
"line": loc["lineNumber"],
"column": loc["columnNumber"],
"lineNumber": loc["lineNumber"],
"columnNumber": loc["columnNumber"],
}
@property
def timestamp(self) -> float:
return self._event["timestamp"]
@property
def page(self) -> Optional["Page"]:
return self._page
@property
def worker(self) -> Optional["Worker"]:
return self._worker
@@ -0,0 +1,58 @@
# 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 TYPE_CHECKING, List
from playwright._impl._api_structures import VirtualCredential
from playwright._impl._helper import locals_to_params
if TYPE_CHECKING:
from playwright._impl._browser_context import BrowserContext
class Credentials:
def __init__(self, browser_context: "BrowserContext") -> None:
self._browser_context = browser_context
self._loop = browser_context._loop
self._dispatcher_fiber = browser_context._dispatcher_fiber
async def install(self) -> None:
await self._browser_context._channel.send("credentialsInstall", None)
async def create(
self,
rpId: str,
id: str = None,
userHandle: str = None,
privateKey: str = None,
publicKey: str = None,
signCount: int = None,
) -> VirtualCredential:
result = await self._browser_context._channel.send_return_as_dict(
"credentialsCreate", None, locals_to_params(locals())
)
return (result or {})["credential"]
async def delete(self, id: str) -> None:
await self._browser_context._channel.send("credentialsDelete", None, {"id": id})
async def get(
self,
rpId: str = None,
id: str = None,
) -> List[VirtualCredential]:
result = await self._browser_context._channel.send_return_as_dict(
"credentialsGet", None, locals_to_params(locals())
)
return (result or {}).get("credentials", [])
@@ -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 types import SimpleNamespace
from typing import Any, Dict, Optional
from playwright._impl._api_structures import DebuggerLocation, DebuggerPausedDetails
from playwright._impl._connection import ChannelOwner
class Debugger(ChannelOwner):
Events = SimpleNamespace(
PausedStateChanged="pausedstatechanged",
)
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._paused_details: Optional[DebuggerPausedDetails] = None
self._channel.on(
"pausedStateChanged", lambda params: self._on_paused_state_changed(params)
)
def _on_paused_state_changed(self, params: Dict[str, Any]) -> None:
self._paused_details = params.get("pausedDetails")
self.emit(Debugger.Events.PausedStateChanged)
async def request_pause(self) -> None:
await self._channel.send("requestPause", None)
async def resume(self) -> None:
await self._channel.send("resume", None)
async def next(self) -> None:
await self._channel.send("next", None)
async def run_to(self, location: DebuggerLocation) -> None:
await self._channel.send("runTo", None, {"location": location})
@property
def paused_details(self) -> Optional[DebuggerPausedDetails]:
return self._paused_details
@@ -0,0 +1,63 @@
# 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 TYPE_CHECKING, Dict, Optional
from playwright._impl._connection import ChannelOwner, from_nullable_channel
from playwright._impl._errors import is_target_closed_error
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"<Dialog type={self.type} message={self.message} default_value={self.default_value}>"
@property
def type(self) -> str:
return self._initializer["type"]
@property
def message(self) -> str:
return self._initializer["message"]
@property
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", None, locals_to_params(locals()))
async def dismiss(self) -> None:
try:
await self._channel.send(
"dismiss",
None,
)
except Exception as e:
if is_target_closed_error(e):
return
raise
@@ -0,0 +1,93 @@
# 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 traceback
from typing import Any, Awaitable, Callable, Coroutine, Dict
from playwright._impl._connection import ChannelOwner, _capture_stack_trace
from playwright._impl._errors import Error, is_target_closed_error
from playwright._impl._greenlets import connection_closed_error, wait_for_future
class Disposable(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
async def dispose(self) -> None:
try:
await self._channel.send(
"dispose",
None,
)
except Exception as e:
if not is_target_closed_error(e):
raise e
async def close(self) -> None:
await self.dispose()
def __repr__(self) -> str:
return "<Disposable>"
class DisposableStub:
def __init__(
self,
dispose_fn: Callable[[], Awaitable[None]],
parent: ChannelOwner,
) -> None:
self._dispose_fn = dispose_fn
self._loop = parent._loop
self._dispatcher_fiber = parent._dispatcher_fiber
async def dispose(self) -> None:
await self._dispose_fn()
async def __aenter__(self) -> "DisposableStub":
return self
async def __aexit__(self, *args: object) -> None:
await self.dispose()
def __enter__(self) -> "DisposableStub":
return self
def __exit__(self, *args: object) -> None:
self._sync(self.dispose())
def _sync(self, coro: Coroutine[Any, Any, Any]) -> object:
__tracebackhide__ = True
if self._loop.is_closed():
coro.close()
raise Error("Event loop is closed! Is Playwright already stopped?")
if self._dispatcher_fiber.dead:
coro.close()
raise connection_closed_error()
task = self._loop.create_task(coro)
setattr(
task,
"__pw_stack__",
_capture_stack_trace(),
)
setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10))
wait_for_future(self._loop, self._dispatcher_fiber, task)
return task.result()
async def close(self) -> None:
await self.dispose()
def __repr__(self) -> str:
return "<Disposable>"
@@ -0,0 +1,64 @@
# 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 pathlib
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union
from playwright._impl._artifact import Artifact
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._page import Page
class Download:
def __init__(
self, page: "Page", url: str, suggested_filename: str, artifact: Artifact
) -> None:
self._page = page
self._loop = page._loop
self._dispatcher_fiber = page._dispatcher_fiber
self._url = url
self._suggested_filename = suggested_filename
self._artifact = artifact
def __repr__(self) -> str:
return f"<Download url={self.url!r} suggested_filename={self.suggested_filename!r}>"
@property
def page(self) -> "Page":
return self._page
@property
def url(self) -> str:
return self._url
@property
def suggested_filename(self) -> str:
return self._suggested_filename
async def delete(self) -> None:
await self._artifact.delete()
async def failure(self) -> Optional[str]:
return await self._artifact.failure()
async def path(self) -> pathlib.Path:
return await self._artifact.path_after_finished()
async def save_as(self, path: Union[str, Path]) -> None:
await self._artifact.save_as(path)
async def cancel(self) -> None:
return await self._artifact.cancel()
@@ -0,0 +1,41 @@
# 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 inspect
import os
import sys
from pathlib import Path
from typing import Tuple
import playwright
from playwright._repo_version import version
def compute_driver_executable() -> Tuple[str, str]:
driver_path = Path(inspect.getfile(playwright)).parent / "driver"
cli_path = str(driver_path / "package" / "cli.js")
if sys.platform == "win32":
return (
os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node.exe")),
cli_path,
)
return (os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node")), cli_path)
def get_driver_env() -> dict:
env = os.environ.copy()
env["PW_LANG_NAME"] = "python"
env["PW_LANG_NAME_VERSION"] = f"{sys.version_info.major}.{sys.version_info.minor}"
env["PW_CLI_DISPLAY_VERSION"] = version
return env
@@ -0,0 +1,480 @@
# 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 base64
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Sequence,
Union,
cast,
)
from playwright._impl._api_structures import FilePayload, FloatRect, Position
from playwright._impl._connection import ChannelOwner, from_nullable_channel
from playwright._impl._helper import (
Error,
KeyboardModifier,
MouseButton,
async_writefile,
locals_to_params,
make_dirs_for_file,
)
from playwright._impl._js_handle import (
JSHandle,
Serializable,
parse_result,
serialize_argument,
)
from playwright._impl._set_input_files_helpers import convert_input_files
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._frame import Frame
from playwright._impl._locator import Locator
class ElementHandle(JSHandle):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._frame = cast("Frame", parent)
async def _createSelectorForTest(self, name: str) -> Optional[str]:
return await self._channel.send(
"createSelectorForTest", self._frame._timeout, dict(name=name)
)
def as_element(self) -> Optional["ElementHandle"]:
return self
async def owner_frame(self) -> Optional["Frame"]:
return from_nullable_channel(await self._channel.send("ownerFrame", None))
async def content_frame(self) -> Optional["Frame"]:
return from_nullable_channel(await self._channel.send("contentFrame", None))
async def get_attribute(self, name: str) -> Optional[str]:
return await self._channel.send("getAttribute", None, dict(name=name))
async def text_content(self) -> Optional[str]:
return await self._channel.send("textContent", None)
async def inner_text(self) -> str:
return await self._channel.send("innerText", None)
async def inner_html(self) -> str:
return await self._channel.send("innerHTML", None)
async def is_checked(self) -> bool:
return await self._channel.send("isChecked", None)
async def is_disabled(self) -> bool:
return await self._channel.send("isDisabled", None)
async def is_editable(self) -> bool:
return await self._channel.send("isEditable", None)
async def is_enabled(self) -> bool:
return await self._channel.send("isEnabled", None)
async def is_hidden(self) -> bool:
return await self._channel.send("isHidden", None)
async def is_visible(self) -> bool:
return await self._channel.send("isVisible", None)
async def dispatch_event(self, type: str, eventInit: Dict = None) -> None:
await self._channel.send(
"dispatchEvent",
None,
dict(type=type, eventInit=serialize_argument(eventInit)),
)
async def scroll_into_view_if_needed(self, timeout: float = None) -> None:
await self._channel.send(
"scrollIntoViewIfNeeded", self._frame._timeout, locals_to_params(locals())
)
async def hover(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"hover", self._frame._timeout, locals_to_params(locals())
)
async def click(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
delay: float = None,
button: MouseButton = None,
clickCount: int = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
steps: int = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"click", self._frame._timeout, locals_to_params(locals())
)
async def dblclick(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
delay: float = None,
button: MouseButton = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
steps: int = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"dblclick", self._frame._timeout, locals_to_params(locals())
)
async def select_option(
self,
value: Union[str, Sequence[str]] = None,
index: Union[int, Sequence[int]] = None,
label: Union[str, Sequence[str]] = None,
element: Union["ElementHandle", Sequence["ElementHandle"]] = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
) -> List[str]:
params = locals_to_params(
dict(
timeout=timeout,
force=force,
**convert_select_option_values(value, index, label, element),
)
)
return await self._channel.send("selectOption", self._frame._timeout, params)
async def tap(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"tap", self._frame._timeout, locals_to_params(locals())
)
async def fill(
self,
value: str,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
) -> None:
await self._channel.send(
"fill", self._frame._timeout, locals_to_params(locals())
)
async def select_text(self, force: bool = None, timeout: float = None) -> None:
await self._channel.send(
"selectText", self._frame._timeout, locals_to_params(locals())
)
async def input_value(self, timeout: float = None) -> str:
return await self._channel.send(
"inputValue", self._frame._timeout, locals_to_params(locals())
)
async def set_input_files(
self,
files: Union[
str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload]
],
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
frame = await self.owner_frame()
if not frame:
raise Error("Cannot set input files to detached element")
converted = await convert_input_files(files, frame.page.context)
await self._channel.send(
"setInputFiles",
self._frame._timeout,
{
"timeout": timeout,
**converted,
},
)
async def focus(self) -> None:
await self._channel.send("focus", None)
async def type(
self,
text: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self._channel.send(
"type", self._frame._timeout, locals_to_params(locals())
)
async def press(
self,
key: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self._channel.send(
"press", self._frame._timeout, locals_to_params(locals())
)
async def set_checked(
self,
checked: bool,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
if checked:
await self.check(
position=position,
timeout=timeout,
force=force,
trial=trial,
scroll=scroll,
)
else:
await self.uncheck(
position=position,
timeout=timeout,
force=force,
trial=trial,
scroll=scroll,
)
async def check(
self,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"check", self._frame._timeout, locals_to_params(locals())
)
async def uncheck(
self,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"uncheck", self._frame._timeout, locals_to_params(locals())
)
async def bounding_box(self) -> Optional[FloatRect]:
return await self._channel.send("boundingBox", None)
async def screenshot(
self,
timeout: float = None,
type: Literal["jpeg", "png", "webp"] = None,
path: Union[str, Path] = None,
quality: int = None,
omitBackground: bool = None,
animations: Literal["allow", "disabled"] = None,
caret: Literal["hide", "initial"] = None,
scale: Literal["css", "device"] = None,
mask: Sequence["Locator"] = None,
maskColor: str = None,
style: str = None,
) -> bytes:
params = locals_to_params(locals())
if "path" in params:
if "type" not in params:
params["type"] = determine_screenshot_type(params["path"])
del params["path"]
if "mask" in params:
params["mask"] = list(
map(
lambda locator: (
{
"frame": locator._frame._channel,
"selector": locator._selector,
}
),
params["mask"],
)
)
encoded_binary = await self._channel.send(
"screenshot", self._frame._timeout, params
)
decoded_binary = base64.b64decode(encoded_binary)
if path:
make_dirs_for_file(path)
await async_writefile(path, decoded_binary)
return decoded_binary
async def query_selector(self, selector: str) -> Optional["ElementHandle"]:
return from_nullable_channel(
await self._channel.send("querySelector", None, dict(selector=selector))
)
async def query_selector_all(self, selector: str) -> List["ElementHandle"]:
return list(
map(
cast(Callable[[Any], Any], from_nullable_channel),
await self._channel.send(
"querySelectorAll", None, dict(selector=selector)
),
)
)
async def eval_on_selector(
self,
selector: str,
expression: str,
arg: Serializable = None,
) -> Any:
return parse_result(
await self._channel.send(
"evalOnSelector",
None,
dict(
selector=selector,
expression=expression,
arg=serialize_argument(arg),
),
)
)
async def eval_on_selector_all(
self,
selector: str,
expression: str,
arg: Serializable = None,
) -> Any:
return parse_result(
await self._channel.send(
"evalOnSelectorAll",
None,
dict(
selector=selector,
expression=expression,
arg=serialize_argument(arg),
),
)
)
async def wait_for_element_state(
self,
state: Literal[
"disabled", "editable", "enabled", "hidden", "stable", "visible"
],
timeout: float = None,
) -> None:
await self._channel.send(
"waitForElementState", self._frame._timeout, locals_to_params(locals())
)
async def wait_for_selector(
self,
selector: str,
state: Literal["attached", "detached", "hidden", "visible"] = None,
timeout: float = None,
strict: bool = None,
) -> Optional["ElementHandle"]:
return from_nullable_channel(
await self._channel.send(
"waitForSelector", self._frame._timeout, locals_to_params(locals())
)
)
def convert_select_option_values(
value: Union[str, Sequence[str]] = None,
index: Union[int, Sequence[int]] = None,
label: Union[str, Sequence[str]] = None,
element: Union["ElementHandle", Sequence["ElementHandle"]] = None,
) -> Any:
if value is None and index is None and label is None and element is None:
return {}
options: Any = None
elements: Any = None
if value is not None:
if isinstance(value, str):
value = [value]
options = (options or []) + list(map(lambda e: dict(valueOrLabel=e), value))
if index is not None:
if isinstance(index, int):
index = [index]
options = (options or []) + list(map(lambda e: dict(index=e), index))
if label is not None:
if isinstance(label, str):
label = [label]
options = (options or []) + list(map(lambda e: dict(label=e), label))
if element:
if isinstance(element, ElementHandle):
element = [element]
elements = list(map(lambda e: e._channel, element))
return dict(options=options, elements=elements)
def determine_screenshot_type(path: Union[str, Path]) -> Literal["jpeg", "png", "webp"]:
# Detect by file extension rather than mimetypes.guess_type, whose result is
# OS-dependent (e.g. Windows does not register image/webp), mirroring
# upstream's getMimeTypeForPath extension map:
# https://github.com/microsoft/playwright/blob/e0e814deed7b0a4c4d2bdf98481e6be7419cda16/packages/isomorphic/mimeType.ts#L28
suffix = Path(path).suffix.lower()
if suffix == ".png":
return "png"
if suffix in (".jpeg", ".jpg"):
return "jpeg"
if suffix == ".webp":
return "webp"
raise Error(f'path: unsupported mime type "{suffix}"')
@@ -0,0 +1,64 @@
# 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.
# These are types that we use in the API. They are public and are a part of the
# stable API.
from typing import Any, List, Optional
def is_target_closed_error(error: Exception) -> bool:
return isinstance(error, TargetClosedError)
class Error(Exception):
def __init__(self, message: str) -> None:
self._message = message
self._name: Optional[str] = None
self._stack: Optional[str] = None
self._details: Optional[Any] = None
self._log: Optional[List[str]] = None
super().__init__(message)
@property
def message(self) -> str:
return self._message
@property
def name(self) -> Optional[str]:
return self._name
@property
def stack(self) -> Optional[str]:
return self._stack
class TimeoutError(Error):
pass
class TargetClosedError(Error):
def __init__(self, message: str = None) -> None:
super().__init__(message or "Target page, context or browser has been closed")
def rewrite_error(error: Exception, message: str) -> Exception:
rewritten_exc = type(error)(message)
if isinstance(rewritten_exc, Error) and isinstance(error, Error):
rewritten_exc._name = error.name
rewritten_exc._stack = error.stack
rewritten_exc._details = error._details
rewritten_exc._log = error._log
return rewritten_exc
@@ -0,0 +1,33 @@
# 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
from typing import Any, Generic, TypeVar
T = TypeVar("T")
class EventContextManagerImpl(Generic[T]):
def __init__(self, future: asyncio.Future) -> None:
self._future: asyncio.Future = future
@property
def future(self) -> asyncio.Future:
return self._future
async def __aenter__(self) -> asyncio.Future:
return self._future
async def __aexit__(self, *args: Any) -> None:
await self._future
@@ -0,0 +1,656 @@
# 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 base64
import json
import mimetypes
import pathlib
import typing
from pathlib import Path
from typing import Any, Dict, List, Optional, Pattern, Sequence, Union, cast
import playwright._impl._network as network
from playwright._impl._api_structures import (
ClientCertificate,
Cookie,
FilePayload,
HttpCredentials,
ProxySettings,
RemoteAddr,
ResourceTiming,
SecurityDetails,
SetCookieParam,
StorageState,
)
from playwright._impl._connection import ChannelOwner, from_channel
from playwright._impl._errors import is_target_closed_error
from playwright._impl._form_data import FormData
from playwright._impl._helper import (
Error,
NameValue,
TargetClosedError,
TimeoutSettings,
async_readfile,
async_writefile,
is_file_payload,
locals_to_params,
object_to_array,
to_impl,
)
from playwright._impl._internal_structures import FormField, Headers, ServerFilePayload
from playwright._impl._network import (
serialize_headers,
to_clear_cookies_params,
to_client_certificates_protocol,
to_http_credentials_protocol,
)
from playwright._impl._tracing import Tracing
if typing.TYPE_CHECKING:
from playwright._impl._playwright import Playwright
FormType = Union[Dict[str, Union[bool, float, str]], FormData]
DataType = Union[Any, bytes, str]
MultipartType = Union[Dict[str, Union[bytes, bool, float, str, FilePayload]], FormData]
ParamsType = Union[Dict[str, Union[bool, float, str]], str]
class APIRequest:
def __init__(self, playwright: "Playwright") -> None:
self.playwright = playwright
self._loop = playwright._loop
self._dispatcher_fiber = playwright._connection._dispatcher_fiber
async def new_context(
self,
baseURL: str = None,
extraHTTPHeaders: Dict[str, str] = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
ignoreHTTPSErrors: bool = None,
proxy: ProxySettings = None,
userAgent: str = None,
timeout: float = None,
storageState: Union[StorageState, str, Path] = None,
clientCertificates: List[ClientCertificate] = None,
failOnStatusCode: bool = None,
maxRedirects: int = None,
) -> "APIRequestContext":
params = locals_to_params(locals())
if "storageState" in params:
storage_state = params["storageState"]
if not isinstance(storage_state, dict) and storage_state:
params["storageState"] = json.loads(
(await async_readfile(storage_state)).decode()
)
if "extraHTTPHeaders" in params:
params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"])
params["clientCertificates"] = await to_client_certificates_protocol(
params.get("clientCertificates")
)
if "httpCredentials" in params:
params["httpCredentials"] = to_http_credentials_protocol(
params["httpCredentials"]
)
context = cast(
APIRequestContext,
from_channel(
await self.playwright._channel.send("newRequest", None, params)
),
)
context._timeout_settings.set_default_timeout(timeout)
return context
class APIRequestContext(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._tracing: Tracing = from_channel(initializer["tracing"])
self._close_reason: Optional[str] = None
self._timeout_settings = TimeoutSettings(None)
async def dispose(self, reason: str = None) -> None:
self._close_reason = reason
await self._tracing._export_all_hars()
try:
await self._channel.send("dispose", None, {"reason": reason})
except Error as e:
if is_target_closed_error(e):
return
raise e
self._tracing._reset_stack_counter()
@property
def tracing(self) -> Tracing:
return self._tracing
async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]:
if urls is None:
urls = []
if isinstance(urls, str):
urls = [urls]
return await self._channel.send("cookies", None, dict(urls=urls))
async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None:
await self._channel.send("addCookies", None, dict(cookies=cookies))
async def clear_cookies(
self,
name: Union[str, Pattern[str]] = None,
domain: Union[str, Pattern[str]] = None,
path: Union[str, Pattern[str]] = None,
) -> None:
await self._channel.send(
"clearCookies", None, to_clear_cookies_params(name, domain, path)
)
async def delete(
self,
url: str,
params: ParamsType = None,
headers: Headers = None,
data: DataType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
return await self.fetch(
url,
method="DELETE",
params=params,
headers=headers,
data=data,
form=form,
multipart=multipart,
timeout=timeout,
failOnStatusCode=failOnStatusCode,
ignoreHTTPSErrors=ignoreHTTPSErrors,
maxRedirects=maxRedirects,
maxRetries=maxRetries,
)
async def head(
self,
url: str,
params: ParamsType = None,
headers: Headers = None,
data: DataType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
return await self.fetch(
url,
method="HEAD",
params=params,
headers=headers,
data=data,
form=form,
multipart=multipart,
timeout=timeout,
failOnStatusCode=failOnStatusCode,
ignoreHTTPSErrors=ignoreHTTPSErrors,
maxRedirects=maxRedirects,
maxRetries=maxRetries,
)
async def get(
self,
url: str,
params: ParamsType = None,
headers: Headers = None,
data: DataType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
return await self.fetch(
url,
method="GET",
params=params,
headers=headers,
data=data,
form=form,
multipart=multipart,
timeout=timeout,
failOnStatusCode=failOnStatusCode,
ignoreHTTPSErrors=ignoreHTTPSErrors,
maxRedirects=maxRedirects,
maxRetries=maxRetries,
)
async def patch(
self,
url: str,
params: ParamsType = None,
headers: Headers = None,
data: DataType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
return await self.fetch(
url,
method="PATCH",
params=params,
headers=headers,
data=data,
form=form,
multipart=multipart,
timeout=timeout,
failOnStatusCode=failOnStatusCode,
ignoreHTTPSErrors=ignoreHTTPSErrors,
maxRedirects=maxRedirects,
maxRetries=maxRetries,
)
async def put(
self,
url: str,
params: ParamsType = None,
headers: Headers = None,
data: DataType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
return await self.fetch(
url,
method="PUT",
params=params,
headers=headers,
data=data,
form=form,
multipart=multipart,
timeout=timeout,
failOnStatusCode=failOnStatusCode,
ignoreHTTPSErrors=ignoreHTTPSErrors,
maxRedirects=maxRedirects,
maxRetries=maxRetries,
)
async def post(
self,
url: str,
params: ParamsType = None,
headers: Headers = None,
data: DataType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
return await self.fetch(
url,
method="POST",
params=params,
headers=headers,
data=data,
form=form,
multipart=multipart,
timeout=timeout,
failOnStatusCode=failOnStatusCode,
ignoreHTTPSErrors=ignoreHTTPSErrors,
maxRedirects=maxRedirects,
maxRetries=maxRetries,
)
async def fetch(
self,
urlOrRequest: Union[str, network.Request],
params: ParamsType = None,
method: str = None,
headers: Headers = None,
data: DataType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
url = urlOrRequest if isinstance(urlOrRequest, str) else None
request = (
cast(network.Request, to_impl(urlOrRequest))
if isinstance(to_impl(urlOrRequest), network.Request)
else None
)
if not request and not isinstance(urlOrRequest, str):
raise Error("First argument must be either URL string or Request")
return await self._inner_fetch(
request,
url,
method,
headers,
data,
params,
form,
multipart,
timeout,
failOnStatusCode,
ignoreHTTPSErrors,
maxRedirects,
maxRetries,
)
async def _inner_fetch(
self,
request: Optional[network.Request],
url: Optional[str],
method: str = None,
headers: Headers = None,
data: DataType = None,
params: ParamsType = None,
form: FormType = None,
multipart: MultipartType = None,
timeout: float = None,
failOnStatusCode: bool = None,
ignoreHTTPSErrors: bool = None,
maxRedirects: int = None,
maxRetries: int = None,
) -> "APIResponse":
if self._close_reason:
raise TargetClosedError(self._close_reason)
if (
(1 if data is not None else 0)
+ (1 if form is not None else 0)
+ (1 if multipart is not None else 0)
) > 1:
raise Error("Only one of 'data', 'form' or 'multipart' can be specified")
if maxRedirects is not None and maxRedirects < 0:
raise Error("'max_redirects' must be greater than or equal to '0'")
if maxRetries is not None and maxRetries < 0:
raise Error("'max_retries' must be greater than or equal to '0'")
url = url or (request.url if request else url)
method = method or (request.method if request else "GET")
# Cannot call allHeaders() here as the request may be paused inside route handler.
headers_obj = headers or (request.headers if request else None)
serialized_headers = serialize_headers(headers_obj) if headers_obj else None
json_data: Any = None
form_data: Optional[List[NameValue]] = None
multipart_data: Optional[List[FormField]] = None
post_data_buffer: Optional[bytes] = None
if data is not None:
if isinstance(data, str):
if is_json_content_type(serialized_headers):
json_data = data if is_json_parsable(data) else json.dumps(data)
else:
post_data_buffer = data.encode()
elif isinstance(data, bytes):
post_data_buffer = data
elif isinstance(data, (dict, list, int, float, bool)):
try:
# NaN and Infinity are not valid JSON.
json_data = json.dumps(data, allow_nan=False)
except ValueError as e:
raise Error(str(e)) from None
else:
raise Error(f"Unsupported 'data' type: {type(data)}")
elif form is not None:
if isinstance(form, FormData):
form_data = []
for fd_name, fd_value in form._fields:
if isinstance(fd_value, (pathlib.Path, dict)):
raise Error(
f"Form field {fd_name!r} must be a string, number or boolean. Use 'multipart' for file uploads."
)
form_data.append(NameValue(name=fd_name, value=str(fd_value)))
else:
form_data = object_to_array(form) or []
elif multipart is not None:
multipart_data = []
if isinstance(multipart, FormData):
for fd_name, fd_value in multipart._fields:
multipart_data.append(
await _form_data_field_to_form_field(fd_name, fd_value)
)
else:
for name, value in multipart.items():
multipart_data.append(
await _form_data_field_to_form_field(name, value)
)
if (
post_data_buffer is None
and json_data is None
and form_data is None
and multipart_data is None
):
post_data_buffer = request.post_data_buffer if request else None
post_data = (
base64.b64encode(post_data_buffer).decode() if post_data_buffer else None
)
response = await self._channel.send(
"fetch",
self._timeout_settings.timeout,
{
"url": url,
"timeout": timeout,
"params": object_to_array(params) if isinstance(params, dict) else None,
"encodedParams": params if isinstance(params, str) else None,
"method": method,
"headers": serialized_headers,
"postData": post_data,
"jsonData": json_data,
"formData": form_data,
"multipartData": multipart_data,
"failOnStatusCode": failOnStatusCode,
"ignoreHTTPSErrors": ignoreHTTPSErrors,
"maxRedirects": maxRedirects,
"maxRetries": maxRetries,
},
)
return APIResponse(self, response)
async def storage_state(
self,
path: Union[pathlib.Path, str] = None,
indexedDB: bool = None,
opfs: bool = None,
) -> StorageState:
result = await self._channel.send_return_as_dict(
"storageState", None, {"indexedDB": indexedDB, "opfs": opfs}
)
if path:
await async_writefile(path, json.dumps(result))
return result
def file_payload_to_json(payload: FilePayload) -> ServerFilePayload:
return ServerFilePayload(
name=payload["name"],
mimeType=payload["mimeType"],
buffer=base64.b64encode(payload["buffer"]).decode(),
)
async def _form_data_field_to_form_field(name: str, value: Any) -> FormField:
if isinstance(value, pathlib.Path):
mime_type, _ = mimetypes.guess_type(str(value))
return FormField(
name=name,
file=ServerFilePayload(
name=value.name,
mimeType=mime_type or "application/octet-stream",
buffer=base64.b64encode(await async_readfile(str(value))).decode(),
),
)
if is_file_payload(value):
payload = cast(FilePayload, value)
if not isinstance(payload["buffer"], bytes):
raise Error(f"Unexpected buffer type of form field {name!r}")
return FormField(name=name, file=file_payload_to_json(payload))
if isinstance(value, (str, int, float, bool)):
return FormField(name=name, value=str(value))
raise Error(f"Unsupported form field {name!r} value type: {type(value).__name__}")
class APIResponse:
def __init__(self, context: APIRequestContext, initializer: Dict) -> None:
self._loop = context._loop
self._dispatcher_fiber = context._connection._dispatcher_fiber
self._request = context
self._initializer = initializer
self._headers = network.RawHeaders(initializer["headers"])
def __repr__(self) -> str:
return f"<APIResponse url={self.url!r} status={self.status!r} status_text={self.status_text!r}>"
@property
def ok(self) -> bool:
return self.status >= 200 and self.status <= 299
@property
def url(self) -> str:
return self._initializer["url"]
@property
def status(self) -> int:
return self._initializer["status"]
@property
def status_text(self) -> str:
return self._initializer["statusText"]
@property
def headers(self) -> Headers:
return self._headers.headers()
@property
def headers_array(self) -> network.HeadersArray:
return self._headers.headers_array()
@property
def timing(self) -> ResourceTiming:
return cast(
ResourceTiming,
{
"startTime": -1,
"domainLookupStart": -1,
"domainLookupEnd": -1,
"connectStart": -1,
"secureConnectionStart": -1,
"connectEnd": -1,
"requestStart": -1,
"responseStart": -1,
**self._initializer.get("timing", {}),
"responseEnd": self._initializer.get("responseEndTiming", -1),
},
)
async def body(self) -> bytes:
try:
result = await self._request._connection.wrap_api_call(
lambda: self._request._channel.send_return_as_dict(
"fetchResponseBody",
None,
{
"fetchUid": self._fetch_uid,
},
),
True,
)
if result is None:
raise Error("Response has been disposed")
return base64.b64decode(result["binary"])
except Error as exc:
if is_target_closed_error(exc):
raise Error("Response has been disposed")
raise exc
async def text(self) -> str:
content = await self.body()
return content.decode()
async def json(self) -> Any:
content = await self.text()
return json.loads(content)
async def security_details(self) -> Optional[SecurityDetails]:
return self._initializer.get("securityDetails") or None
async def server_addr(self) -> Optional[RemoteAddr]:
return self._initializer.get("serverAddr") or None
async def dispose(self) -> None:
await self._request._channel.send(
"disposeAPIResponse",
None,
{
"fetchUid": self._fetch_uid,
},
)
@property
def _fetch_uid(self) -> str:
return self._initializer["fetchUid"]
async def _fetch_log(self) -> List[str]:
return await self._request._channel.send(
"fetchLog",
None,
{
"fetchUid": self._fetch_uid,
},
)
def is_json_content_type(headers: network.HeadersArray = None) -> bool:
if not headers:
return False
for header in headers:
if header["name"].lower() == "content-type":
return header["value"].startswith("application/json")
return False
def is_json_parsable(value: Any) -> bool:
if not isinstance(value, str):
return False
try:
# NaN and Infinity are not valid JSON, unlike what json.loads() accepts by default.
json.loads(value, parse_constant=_reject_json_constant)
return True
except ValueError:
return False
def _reject_json_constant(value: str) -> Any:
raise ValueError(f"{value} is not valid JSON")
@@ -0,0 +1,57 @@
# 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 pathlib import Path
from typing import TYPE_CHECKING, Sequence, Union
from playwright._impl._api_structures import FilePayload
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._element_handle import ElementHandle
from playwright._impl._page import Page
class FileChooser:
def __init__(
self, page: "Page", element_handle: "ElementHandle", is_multiple: bool
) -> None:
self._page = page
self._loop = page._loop
self._dispatcher_fiber = page._dispatcher_fiber
self._element_handle = element_handle
self._is_multiple = is_multiple
def __repr__(self) -> str:
return f"<FileChooser page={self._page} element={self._element_handle}>"
@property
def page(self) -> "Page":
return self._page
@property
def element(self) -> "ElementHandle":
return self._element_handle
def is_multiple(self) -> bool:
return self._is_multiple
async def set_files(
self,
files: Union[
str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload]
],
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self._element_handle.set_input_files(files, timeout, noWaitAfter)
@@ -0,0 +1,34 @@
# 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 pathlib
from typing import List, Tuple, Union
from playwright._impl._api_structures import FilePayload
FormDataValue = Union[bool, float, str, pathlib.Path, FilePayload]
class FormData:
def __init__(self) -> None:
self._fields: List[Tuple[str, FormDataValue]] = []
def set(self, name: str, value: FormDataValue) -> "FormData":
self._fields = [(n, v) for (n, v) in self._fields if n != name]
self._fields.append((name, value))
return self
def append(self, name: str, value: FormDataValue) -> "FormData":
self._fields.append((name, value))
return self
@@ -0,0 +1,990 @@
# 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
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Literal,
Optional,
Pattern,
Sequence,
Set,
Union,
cast,
)
from pyee import EventEmitter
from playwright._impl._api_structures import DropPayload, FilePayload, Position
from playwright._impl._connection import (
ChannelOwner,
from_channel,
from_nullable_channel,
)
from playwright._impl._element_handle import ElementHandle, convert_select_option_values
from playwright._impl._errors import Error
from playwright._impl._event_context_manager import EventContextManagerImpl
from playwright._impl._helper import (
DocumentLoadState,
FrameNavigatedEvent,
KeyboardModifier,
MouseButton,
TimeoutSettings,
URLMatch,
async_readfile,
locals_to_params,
monotonic_time,
url_matches,
)
from playwright._impl._internal_structures import (
AriaRole,
FrameExpectOptions,
FrameExpectResult,
)
from playwright._impl._js_handle import (
JSHandle,
Serializable,
add_source_url_to_script,
parse_result,
parse_value,
serialize_argument,
)
from playwright._impl._locator import (
ANY_FRAME_SELECTOR,
FrameLocator,
Locator,
get_by_alt_text_selector,
get_by_label_selector,
get_by_placeholder_selector,
get_by_role_selector,
get_by_test_id_selector,
get_by_text_selector,
get_by_title_selector,
test_id_attribute_name,
)
from playwright._impl._network import Response
from playwright._impl._set_input_files_helpers import convert_input_files
from playwright._impl._waiter import Waiter
from playwright._impl._webmcp import WebMCP
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._page import Page
class Frame(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._parent_frame = from_nullable_channel(initializer.get("parentFrame"))
if self._parent_frame:
self._parent_frame._child_frames.append(self)
self._name = initializer["name"]
self._url = initializer["url"]
self._detached = False
self._child_frames: List[Frame] = []
self._page: Optional[Page] = None
self._load_states: Set[str] = set(initializer["loadStates"])
self._event_emitter = EventEmitter()
self._webmcp = WebMCP(self)
self._channel.on(
"loadstate",
lambda params: self._on_load_state(params.get("add"), params.get("remove")),
)
self._channel.on(
"navigated",
lambda params: self._on_frame_navigated(params),
)
def __repr__(self) -> str:
return f"<Frame name={self.name} url={self.url!r}>"
def _on_load_state(
self, add: DocumentLoadState = None, remove: DocumentLoadState = None
) -> None:
if add:
self._load_states.add(add)
self._event_emitter.emit("loadstate", add)
elif remove and remove in self._load_states:
self._load_states.remove(remove)
if not self._parent_frame and add == "load" and self._page:
self._page.emit("load", self._page)
self._page.context.emit("pageload", self._page)
if not self._parent_frame and add == "domcontentloaded" and self._page:
self._page.emit("domcontentloaded", self._page)
def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None:
self._url = event["url"]
self._name = event["name"]
self._event_emitter.emit("navigated", event)
if "error" not in event and self._page:
self._page.emit("framenavigated", self)
self._page.context.emit("framenavigated", self)
async def _query_count(self, selector: str) -> int:
return await self._channel.send("queryCount", None, {"selector": selector})
@property
def page(self) -> "Page":
assert self._page
return self._page
async def goto(
self,
url: str,
timeout: float = None,
waitUntil: DocumentLoadState = None,
referer: str = None,
) -> Optional[Response]:
return cast(
Optional[Response],
from_nullable_channel(
await self._channel.send(
"goto", self._navigation_timeout, locals_to_params(locals())
)
),
)
def _setup_navigation_waiter(self, wait_name: str, timeout: float = None) -> Waiter:
assert self._page
waiter = Waiter(self._page, f"frame.{wait_name}")
waiter.reject_on_event(
self._page,
"close",
lambda: cast("Page", self._page)._close_error_with_reason(),
)
waiter.reject_on_event(
self._page, "crash", Error("Navigation failed because page crashed!")
)
waiter.reject_on_event(
self._page,
"framedetached",
Error("Navigating frame was detached!"),
lambda frame: frame == self,
)
timeout = self._page._timeout_settings.navigation_timeout(timeout)
waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.")
return waiter
async def _expect(
self,
selector: Optional[str],
expression: str,
options: FrameExpectOptions,
title: str = None,
) -> FrameExpectResult:
if "expectedValue" in options:
options["expectedValue"] = serialize_argument(options["expectedValue"])
try:
await self._channel.send(
"expect",
self._timeout,
{
"selector": selector,
"expression": expression,
**options,
},
title=title,
)
return {"matches": not options.get("isNot")}
except Error as e:
if not e._details:
raise e
details = cast(Dict[str, Any], e._details)
received = details.get("received")
if received:
received = {
"value": (
parse_value(received["value"]) if "value" in received else None
),
"ariaSnapshot": received.get("ariaSnapshot"),
}
return {
"matches": bool(options.get("isNot")),
"received": received,
"timedOut": details.get("timedOut"),
"errorMessage": (
"Error: " + details["customErrorMessage"]
if details.get("customErrorMessage")
else None
),
"log": e._log,
}
def expect_navigation(
self,
url: URLMatch = None,
waitUntil: DocumentLoadState = None,
timeout: float = None,
) -> EventContextManagerImpl[Response]:
assert self._page
if not waitUntil:
waitUntil = "load"
if timeout is None:
timeout = self._page._timeout_settings.navigation_timeout()
deadline = monotonic_time() + timeout
waiter = self._setup_navigation_waiter("expect_navigation", timeout)
to_url = f' to "{url}"' if url else ""
waiter.log(f"waiting for navigation{to_url} until '{waitUntil}'")
def predicate(event: Any) -> bool:
# Any failed navigation results in a rejection.
if event.get("error"):
return True
waiter.log(f' navigated to "{event["url"]}"')
return url_matches(
cast("Page", self._page)._browser_context._base_url,
event["url"],
url,
)
waiter.wait_for_event(
self._event_emitter,
"navigated",
predicate=predicate,
)
async def continuation() -> Optional[Response]:
event = await waiter.result()
if "error" in event:
raise Error(event["error"])
if waitUntil not in self._load_states:
t = deadline - monotonic_time()
if t > 0:
await self._wait_for_load_state_impl(state=waitUntil, timeout=t)
if "newDocument" in event and "request" in event["newDocument"]:
request = from_channel(event["newDocument"]["request"])
return await request.response()
return None
return EventContextManagerImpl(asyncio.create_task(continuation()))
async def wait_for_url(
self,
url: URLMatch,
waitUntil: DocumentLoadState = None,
timeout: float = None,
) -> None:
assert self._page
if url_matches(self._page._browser_context._base_url, self.url, url):
await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout)
return
async with self.expect_navigation(
url=url, waitUntil=waitUntil, timeout=timeout
):
pass
async def wait_for_load_state(
self,
state: Literal["domcontentloaded", "load", "networkidle"] = None,
timeout: float = None,
) -> None:
return await self._wait_for_load_state_impl(state, timeout)
async def _wait_for_load_state_impl(
self, state: DocumentLoadState = None, timeout: float = None
) -> None:
if not state:
state = "load"
if state not in ("load", "domcontentloaded", "networkidle", "commit"):
raise Error(
"state: expected one of (load|domcontentloaded|networkidle|commit)"
)
waiter = self._setup_navigation_waiter("wait_for_load_state", timeout)
if state in self._load_states:
waiter.log(f' not waiting, "{state}" event already fired')
# TODO: align with upstream
waiter._fulfill(None)
else:
def handle_load_state_event(actual_state: str) -> bool:
waiter.log(f'"{actual_state}" event fired')
return actual_state == state
waiter.wait_for_event(
self._event_emitter,
"loadstate",
handle_load_state_event,
)
await waiter.result()
def _timeout(self, timeout: Optional[float]) -> float:
timeout_settings = (
self._page._timeout_settings if self._page else TimeoutSettings(None)
)
return timeout_settings.timeout(timeout)
def _navigation_timeout(self, timeout: Optional[float]) -> float:
timeout_settings = (
self._page._timeout_settings if self._page else TimeoutSettings(None)
)
return timeout_settings.navigation_timeout(timeout)
async def frame_element(self) -> ElementHandle:
return from_channel(await self._channel.send("frameElement", None))
async def evaluate(self, expression: str, arg: Serializable = None) -> Any:
return parse_result(
await self._channel.send(
"evaluateExpression",
None,
dict(
expression=expression,
arg=serialize_argument(arg),
),
)
)
async def evaluate_handle(
self, expression: str, arg: Serializable = None
) -> JSHandle:
return from_channel(
await self._channel.send(
"evaluateExpressionHandle",
None,
dict(
expression=expression,
arg=serialize_argument(arg),
),
)
)
async def query_selector(
self, selector: str, strict: bool = None
) -> Optional[ElementHandle]:
return from_nullable_channel(
await self._channel.send("querySelector", None, locals_to_params(locals()))
)
async def query_selector_all(self, selector: str) -> List[ElementHandle]:
return list(
map(
from_channel,
await self._channel.send(
"querySelectorAll", None, dict(selector=selector)
),
)
)
async def wait_for_selector(
self,
selector: str,
strict: bool = None,
timeout: float = None,
state: Literal["attached", "detached", "hidden", "visible"] = None,
) -> Optional[ElementHandle]:
return from_nullable_channel(
await self._channel.send(
"waitForSelector", self._timeout, locals_to_params(locals())
)
)
async def is_checked(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send(
"isChecked", self._timeout, locals_to_params(locals())
)
async def is_disabled(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send(
"isDisabled", self._timeout, locals_to_params(locals())
)
async def is_editable(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send(
"isEditable", self._timeout, locals_to_params(locals())
)
async def is_enabled(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send(
"isEnabled", self._timeout, locals_to_params(locals())
)
async def is_hidden(self, selector: str, strict: bool = None) -> bool:
return await self._channel.send(
"isHidden", self._timeout, locals_to_params(locals())
)
async def is_visible(self, selector: str, strict: bool = None) -> bool:
return await self._channel.send(
"isVisible", self._timeout, locals_to_params(locals())
)
async def dispatch_event(
self,
selector: str,
type: str,
eventInit: Dict = None,
strict: bool = None,
timeout: float = None,
) -> None:
await self._channel.send(
"dispatchEvent",
self._timeout,
locals_to_params(
dict(
selector=selector,
type=type,
eventInit=serialize_argument(eventInit),
strict=strict,
timeout=timeout,
),
),
)
async def eval_on_selector(
self,
selector: str,
expression: str,
arg: Serializable = None,
strict: bool = None,
) -> Any:
return parse_result(
await self._channel.send(
"evalOnSelector",
None,
locals_to_params(
dict(
selector=selector,
expression=expression,
arg=serialize_argument(arg),
strict=strict,
)
),
)
)
async def eval_on_selector_all(
self,
selector: str,
expression: str,
arg: Serializable = None,
) -> Any:
return parse_result(
await self._channel.send(
"evalOnSelectorAll",
None,
dict(
selector=selector,
expression=expression,
arg=serialize_argument(arg),
),
)
)
async def content(self, includeShadow: bool = None) -> str:
return await self._channel.send("content", None, locals_to_params(locals()))
async def set_content(
self,
html: str,
timeout: float = None,
waitUntil: DocumentLoadState = None,
) -> None:
await self._channel.send(
"setContent", self._navigation_timeout, locals_to_params(locals())
)
@property
def name(self) -> str:
return self._name or ""
@property
def url(self) -> str:
return self._url or ""
@property
def webmcp(self) -> WebMCP:
return self._webmcp
@property
def parent_frame(self) -> Optional["Frame"]:
return self._parent_frame
@property
def child_frames(self) -> List["Frame"]:
return self._child_frames.copy()
def is_detached(self) -> bool:
return self._detached
async def add_script_tag(
self,
url: str = None,
path: Union[str, Path] = None,
content: str = None,
type: str = None,
) -> ElementHandle:
params = locals_to_params(locals())
if path:
params["content"] = add_source_url_to_script(
(await async_readfile(path)).decode(), path
)
del params["path"]
return from_channel(await self._channel.send("addScriptTag", None, params))
async def add_style_tag(
self, url: str = None, path: Union[str, Path] = None, content: str = None
) -> ElementHandle:
params = locals_to_params(locals())
if path:
params["content"] = (
(await async_readfile(path)).decode()
+ "\n/*# sourceURL="
+ str(Path(path))
+ "*/"
)
del params["path"]
return from_channel(await self._channel.send("addStyleTag", None, params))
async def click(
self,
selector: str,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
delay: float = None,
button: MouseButton = None,
clickCount: int = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._click(**locals_to_params(locals()))
async def _click(
self,
selector: str,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
delay: float = None,
button: MouseButton = None,
clickCount: int = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
trial: bool = None,
steps: int = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send("click", self._timeout, locals_to_params(locals()))
async def dblclick(
self,
selector: str,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
delay: float = None,
button: MouseButton = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"dblclick", self._timeout, locals_to_params(locals()), title="Double click"
)
async def tap(
self,
selector: str,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send("tap", self._timeout, locals_to_params(locals()))
async def fill(
self,
selector: str,
value: str,
timeout: float = None,
noWaitAfter: bool = None,
strict: bool = None,
force: bool = None,
) -> None:
await self._fill(**locals_to_params(locals()))
async def _fill(
self,
selector: str,
value: str,
timeout: float = None,
noWaitAfter: bool = None,
strict: bool = None,
force: bool = None,
title: str = None,
) -> None:
await self._channel.send("fill", self._timeout, locals_to_params(locals()))
def locator(
self,
selector: str,
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: Locator = None,
hasNot: Locator = None,
) -> Locator:
return Locator(
self,
selector,
has_text=hasText,
has_not_text=hasNotText,
has=has,
has_not=hasNot,
)
def get_by_alt_text(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_alt_text_selector(text, exact=exact))
def get_by_label(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_label_selector(text, exact=exact))
def get_by_placeholder(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_placeholder_selector(text, exact=exact))
def get_by_role(
self,
role: AriaRole,
checked: bool = None,
disabled: bool = None,
expanded: bool = None,
includeHidden: bool = None,
level: int = None,
name: Union[str, Pattern[str]] = None,
pressed: bool = None,
selected: bool = None,
exact: bool = None,
description: Union[str, Pattern[str]] = None,
) -> "Locator":
return self.locator(
get_by_role_selector(
role,
checked=checked,
disabled=disabled,
expanded=expanded,
includeHidden=includeHidden,
level=level,
name=name,
pressed=pressed,
selected=selected,
exact=exact,
description=description,
)
)
def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator":
return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId))
def get_by_text(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_text_selector(text, exact=exact))
def get_by_title(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_title_selector(text, exact=exact))
def frame_locator(self, selector: str = None) -> FrameLocator:
return FrameLocator(self, ANY_FRAME_SELECTOR if selector is None else selector)
async def focus(
self, selector: str, strict: bool = None, timeout: float = None
) -> None:
await self._channel.send("focus", self._timeout, locals_to_params(locals()))
async def text_content(
self, selector: str, strict: bool = None, timeout: float = None
) -> Optional[str]:
return await self._channel.send(
"textContent", self._timeout, locals_to_params(locals())
)
async def inner_text(
self, selector: str, strict: bool = None, timeout: float = None
) -> str:
return await self._channel.send(
"innerText", self._timeout, locals_to_params(locals())
)
async def inner_html(
self, selector: str, strict: bool = None, timeout: float = None
) -> str:
return await self._channel.send(
"innerHTML", self._timeout, locals_to_params(locals())
)
async def get_attribute(
self, selector: str, name: str, strict: bool = None, timeout: float = None
) -> Optional[str]:
return await self._channel.send(
"getAttribute", self._timeout, locals_to_params(locals())
)
async def hover(
self,
selector: str,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
strict: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send("hover", self._timeout, locals_to_params(locals()))
async def drag_and_drop(
self,
source: str,
target: str,
sourcePosition: Position = None,
targetPosition: Position = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
timeout: float = None,
trial: bool = None,
steps: int = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"dragAndDrop", self._timeout, locals_to_params(locals())
)
async def select_option(
self,
selector: str,
value: Union[str, Sequence[str]] = None,
index: Union[int, Sequence[int]] = None,
label: Union[str, Sequence[str]] = None,
element: Union["ElementHandle", Sequence["ElementHandle"]] = None,
timeout: float = None,
noWaitAfter: bool = None,
strict: bool = None,
force: bool = None,
) -> List[str]:
params = locals_to_params(
dict(
selector=selector,
timeout=timeout,
strict=strict,
force=force,
**convert_select_option_values(value, index, label, element),
)
)
return await self._channel.send("selectOption", self._timeout, params)
async def input_value(
self,
selector: str,
strict: bool = None,
timeout: float = None,
) -> str:
return await self._channel.send(
"inputValue", self._timeout, locals_to_params(locals())
)
async def set_input_files(
self,
selector: str,
files: Union[
str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload]
],
strict: bool = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
converted = await convert_input_files(files, self.page.context)
await self._channel.send(
"setInputFiles",
self._timeout,
{
"selector": selector,
"strict": strict,
"timeout": self._timeout(timeout),
**converted,
},
)
async def _drop(
self,
selector: str,
payload: "DropPayload",
strict: bool = None,
position: Position = None,
timeout: float = None,
) -> None:
params: Dict[str, Any] = {
"selector": selector,
"strict": strict,
"position": position,
"timeout": self._timeout(timeout),
}
files = payload.get("files") if payload else None
if files is not None:
converted = await convert_input_files(files, self.page.context)
if "directoryStream" in converted or "directoryLocalPath" in converted:
raise Error(
"Dropping a directory is not supported, pass individual files instead."
)
params.update(converted)
data = payload.get("data") if payload else None
if data is not None:
params["data"] = [{"mimeType": k, "value": v} for k, v in data.items()]
await self._channel.send("drop", self._timeout, params)
async def type(
self,
selector: str,
text: str,
delay: float = None,
strict: bool = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self._channel.send("type", self._timeout, locals_to_params(locals()))
async def press(
self,
selector: str,
key: str,
delay: float = None,
strict: bool = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self._channel.send("press", self._timeout, locals_to_params(locals()))
async def check(
self,
selector: str,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send("check", self._timeout, locals_to_params(locals()))
async def uncheck(
self,
selector: str,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send("uncheck", self._timeout, locals_to_params(locals()))
async def wait_for_timeout(self, timeout: float) -> None:
await self._channel.send("waitForTimeout", None, {"waitTimeout": timeout})
async def wait_for_function(
self,
expression: str,
arg: Serializable = None,
timeout: float = None,
polling: Union[float, Literal["raf"]] = None,
) -> JSHandle:
if isinstance(polling, str) and polling != "raf":
raise Error(f"Unknown polling option: {polling}")
params = locals_to_params(locals())
params["arg"] = serialize_argument(arg)
if polling is not None and polling != "raf":
params["pollingInterval"] = polling
return from_channel(
await self._channel.send("waitForFunction", self._timeout, params)
)
async def title(self) -> str:
return await self._channel.send("title", None)
async def set_checked(
self,
selector: str,
checked: bool,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
strict: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
if checked:
await self.check(
selector=selector,
position=position,
timeout=timeout,
force=force,
strict=strict,
trial=trial,
scroll=scroll,
)
else:
await self.uncheck(
selector=selector,
position=position,
timeout=timeout,
force=force,
strict=strict,
trial=trial,
scroll=scroll,
)
async def _highlight(self, selector: str, style: str = None) -> None:
await self._channel.send(
"highlight", None, {"selector": selector, "style": style}
)
async def _hide_highlight(self, selector: str) -> None:
await self._channel.send("hideHighlight", None, {"selector": selector})
@@ -0,0 +1,65 @@
# 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.
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"}
def glob_to_regex_pattern(glob: str) -> str:
tokens = ["^"]
in_group = False
i = 0
while i < len(glob):
c = glob[i]
if c == "\\" and i + 1 < len(glob):
char = glob[i + 1]
tokens.append("\\" + char if char in escaped_chars else char)
i += 1
elif c == "*":
char_before = glob[i - 1] if i > 0 else None
star_count = 1
while i + 1 < len(glob) and glob[i + 1] == "*":
star_count += 1
i += 1
if star_count > 1:
char_after = glob[i + 1] if i + 1 < len(glob) else None
if char_after == "/":
if char_before == "/":
tokens.append("((.+/)|)")
else:
tokens.append("(.*/)")
i += 1
else:
tokens.append("(.*)")
else:
tokens.append("([^/]*)")
else:
if c == "{":
in_group = True
tokens.append("(")
elif c == "}":
in_group = False
tokens.append(")")
elif c == ",":
if in_group:
tokens.append("|")
else:
tokens.append("\\" + c)
else:
tokens.append("\\" + c if c in escaped_chars else c)
i += 1
tokens.append("$")
return "".join(tokens)
@@ -0,0 +1,94 @@
# 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 os
from typing import Tuple
import greenlet
from playwright._impl._errors import TargetClosedError
def _greenlet_trace_callback(
event: str, args: Tuple[greenlet.greenlet, greenlet.greenlet]
) -> None:
if event in ("switch", "throw"):
origin, target = args
print(f"Transfer from {origin} to {target} with {event}")
if os.environ.get("INTERNAL_PW_GREENLET_DEBUG"):
greenlet.settrace(_greenlet_trace_callback)
class MainGreenlet(greenlet.greenlet):
def __str__(self) -> str:
return "<MainGreenlet>"
class RouteGreenlet(greenlet.greenlet):
def __str__(self) -> str:
return "<RouteGreenlet>"
class LocatorHandlerGreenlet(greenlet.greenlet):
def __str__(self) -> str:
return "<LocatorHandlerGreenlet>"
class EventGreenlet(greenlet.greenlet):
def __str__(self) -> str:
return "<EventGreenlet>"
def connection_closed_error() -> TargetClosedError:
return TargetClosedError("Playwright connection closed")
def wait_for_future(
loop: asyncio.AbstractEventLoop,
dispatcher_fiber: greenlet.greenlet,
future: "asyncio.Future",
) -> None:
__tracebackhide__ = True
g_self = greenlet.getcurrent()
def resume(_: "asyncio.Future") -> None:
g_self.switch()
# The wait owns the callback that resumes this greenlet, so that it can
# remove it again when the wait is abandoned. A callback left behind would
# resume this greenlet at an arbitrary later point.
future.add_done_callback(resume)
try:
while not future.done():
# The dispatcher fiber exits once the connection to the driver ends,
# e.g. when the driver process dies. Nothing can settle the future
# after that, and switching to a dead greenlet returns right away,
# so we would spin.
if dispatcher_fiber.dead:
raise connection_closed_error()
# Raises when a signal handler interrupted the event loop while it
# waited for the driver, see greenlet_main in _context_manager.py.
dispatcher_fiber.switch()
except BaseException:
future.remove_done_callback(resume)
future.cancel()
raise
finally:
# The loop is only running for as long as the dispatcher fiber is alive.
# Also after a signal handler interrupted it: run_forever() cleared this
# on its way out, but the dispatcher is going to run the loop again.
if not dispatcher_fiber.dead:
asyncio._set_running_loop(loop)
@@ -0,0 +1,122 @@
# 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 base64
from typing import TYPE_CHECKING, Optional, cast
from playwright._impl._helper import (
HarLookupResult,
RouteFromHarNotFoundPolicy,
URLMatch,
)
from playwright._impl._internal_structures import HeadersArray
from playwright._impl._local_utils import LocalUtils
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._browser_context import BrowserContext
from playwright._impl._network import Route
from playwright._impl._page import Page
class HarRouter:
def __init__(
self,
local_utils: LocalUtils,
har_id: str,
not_found_action: RouteFromHarNotFoundPolicy,
url_matcher: Optional[URLMatch] = None,
) -> None:
self._local_utils: LocalUtils = local_utils
self._har_id: str = har_id
self._not_found_action: RouteFromHarNotFoundPolicy = not_found_action
self._options_url_match: Optional[URLMatch] = url_matcher
@staticmethod
async def create(
local_utils: LocalUtils,
file: str,
not_found_action: RouteFromHarNotFoundPolicy,
url_matcher: Optional[URLMatch] = None,
) -> "HarRouter":
har_id = await local_utils._channel.send("harOpen", None, {"file": file})
return HarRouter(
local_utils=local_utils,
har_id=har_id,
not_found_action=not_found_action,
url_matcher=url_matcher,
)
async def _handle(self, route: "Route") -> None:
request = route.request
response: HarLookupResult = await self._local_utils.har_lookup(
harId=self._har_id,
url=request.url,
method=request.method,
headers=await request.headers_array(),
postData=request.post_data_buffer,
isNavigationRequest=request.is_navigation_request(),
)
action = response["action"]
if action == "redirect":
redirect_url = response["redirectURL"]
assert redirect_url
await route._redirected_navigation_request(redirect_url)
return
if action == "fulfill":
# If the response status is -1, the request was canceled or stalled, so we just stall it here.
# See https://github.com/microsoft/playwright/issues/29311.
# TODO: it'd be better to abort such requests, but then we likely need to respect the timing,
# because the request might have been stalled for a long time until the very end of the
# test when HAR was recorded but we'd abort it immediately.
if response.get("status") == -1:
return
body = response["body"]
assert body is not None
await route.fulfill(
status=response.get("status"),
headers={
v["name"]: v["value"]
for v in cast(HeadersArray, response.get("headers", []))
},
body=base64.b64decode(body),
)
return
if action == "error":
pass
# Report the error, but fall through to the default handler.
if self._not_found_action == "abort":
await route.abort()
return
await route.fallback()
async def add_context_route(self, context: "BrowserContext") -> None:
await context.route(
url=self._options_url_match or "**/*",
handler=lambda route, _: asyncio.create_task(self._handle(route)),
)
async def add_page_route(self, page: "Page") -> None:
await page.route(
url=self._options_url_match or "**/*",
handler=lambda route, _: asyncio.create_task(self._handle(route)),
)
def dispose(self) -> None:
self._local_utils._channel.send_may_fail(
"harClose", None, {"harId": self._har_id}
)
@@ -0,0 +1,616 @@
# 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 datetime
import math
import os
import re
import time
import traceback
from pathlib import Path
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
List,
Literal,
Optional,
Pattern,
Set,
Tuple,
TypedDict,
TypeVar,
Union,
cast,
overload,
)
from urllib.parse import ParseResult, urljoin, urlparse, urlunparse
from playwright._impl._api_structures import NameValue
from playwright._impl._errors import (
Error,
TargetClosedError,
TimeoutError,
is_target_closed_error,
rewrite_error,
)
from playwright._impl._glob import glob_to_regex_pattern
from playwright._impl._greenlets import RouteGreenlet
from playwright._impl._str_utils import escape_regex_flags
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._internal_structures import HeadersArray
from playwright._impl._network import Request, Response, Route, WebSocketRoute
URLMatch = Union[str, Pattern[str], Callable[[str], bool]]
URLMatchRequest = Union[
str, Pattern[str], Callable[["Request"], Union[bool, Awaitable[bool]]]
]
URLMatchResponse = Union[
str, Pattern[str], Callable[["Response"], Union[bool, Awaitable[bool]]]
]
RouteHandlerCallback = Union[
Callable[["Route"], Any], Callable[["Route", "Request"], Any]
]
WebSocketRouteHandlerCallback = Callable[["WebSocketRoute"], Any]
ColorScheme = Literal["dark", "light", "no-preference", "null"]
ForcedColors = Literal["active", "none", "null"]
Contrast = Literal["more", "no-preference", "null"]
ReducedMotion = Literal["no-preference", "null", "reduce"]
DocumentLoadState = Literal["commit", "domcontentloaded", "load", "networkidle"]
KeyboardModifier = Literal["Alt", "Control", "ControlOrMeta", "Meta", "Shift"]
MouseButton = Literal["left", "middle", "right"]
ServiceWorkersPolicy = Literal["allow", "block"]
HarMode = Literal["full", "minimal"]
HarContentPolicy = Literal["attach", "embed", "omit"]
RouteFromHarNotFoundPolicy = Literal["abort", "fallback"]
class ErrorPayload(TypedDict, total=False):
message: str
name: str
stack: str
value: Optional[Any]
class HarRecordingMetadata(TypedDict, total=False):
path: str
content: Optional[HarContentPolicy]
def prepare_record_har_options(params: Dict) -> Dict[str, Any]:
out_params: Dict[str, Any] = {"path": str(params["recordHarPath"])}
if "recordHarUrlFilter" in params:
opt = params["recordHarUrlFilter"]
if isinstance(opt, str):
out_params["urlGlob"] = opt
if isinstance(opt, Pattern):
out_params["urlRegexSource"] = opt.pattern
out_params["urlRegexFlags"] = escape_regex_flags(opt)
del params["recordHarUrlFilter"]
if "recordHarMode" in params:
out_params["mode"] = params["recordHarMode"]
del params["recordHarMode"]
new_content_api = None
old_content_api = None
if "recordHarContent" in params:
new_content_api = params["recordHarContent"]
del params["recordHarContent"]
if "recordHarOmitContent" in params:
old_content_api = params["recordHarOmitContent"]
del params["recordHarOmitContent"]
content = new_content_api or ("omit" if old_content_api else None)
if content:
out_params["content"] = content
return out_params
class ParsedMessageParams(TypedDict):
type: str
guid: str
initializer: Dict
class ParsedMessagePayload(TypedDict, total=False):
id: str
guid: str
method: str
params: ParsedMessageParams
result: Any
error: ErrorPayload
errorDetails: Any
log: List[str]
class Document(TypedDict):
request: Optional[Any]
class FrameNavigatedEvent(TypedDict):
url: str
name: str
newDocument: Optional[Document]
error: Optional[str]
Env = Dict[str, Union[str, float, bool]]
def url_matches(
base_url: Optional[str],
url_string: str,
match: Optional[URLMatch],
websocket_url: bool = None,
) -> bool:
if not match:
return True
if isinstance(match, str):
match = re.compile(
resolve_glob_to_regex_pattern(base_url, match, websocket_url)
)
if isinstance(match, Pattern):
return bool(match.search(url_string))
return match(url_string)
def resolve_glob_to_regex_pattern(
base_url: Optional[str], glob: str, websocket_url: bool = None
) -> str:
if websocket_url:
base_url = to_websocket_base_url(base_url)
glob = resolve_glob_base(base_url, glob)
return glob_to_regex_pattern(glob)
def to_websocket_base_url(base_url: Optional[str]) -> Optional[str]:
if base_url is not None and re.match(r"^https?://", base_url):
base_url = re.sub(r"^http", "ws", base_url)
return base_url
def resolve_glob_base(base_url: Optional[str], match: str) -> str:
if match[0] == "*":
return match
token_map: Dict[str, str] = {}
def map_token(original: str, replacement: str) -> str:
if len(original) == 0:
return ""
token_map[replacement] = original
return replacement
# Escaped `\\?` behaves the same as `?` in our glob patterns.
match = match.replace(r"\\?", "?")
# Special case about: URLs as they are not relative to base_url
if (
match.startswith("about:")
or match.startswith("data:")
or match.startswith("chrome:")
or match.startswith("edge:")
or match.startswith("file:")
):
# about: and data: URLs are not relative to base_url, so we return them as is.
return match
# Glob symbols may be escaped in the URL and some of them such as ? affect resolution,
# so we replace them with safe components first.
processed_parts = []
for index, token in enumerate(match.split("/")):
if token in (".", "..", ""):
processed_parts.append(token)
continue
# Handle special case of http*://, note that the new schema has to be
# a web schema so that slashes are properly inserted after domain.
if index == 0 and token.endswith(":"):
# Replace any pattern with http:
if "*" in token or "{" in token:
processed_parts.append(map_token(token, "http:"))
else:
# Preserve explicit schema as is as it may affect trailing slashes after domain.
processed_parts.append(token)
continue
question_index = token.find("?")
if question_index == -1:
processed_parts.append(map_token(token, f"$_{index}_$"))
else:
new_prefix = map_token(token[:question_index], f"$_{index}_$")
new_suffix = map_token(token[question_index:], f"?$_{index}_$")
processed_parts.append(new_prefix + new_suffix)
relative_path = "/".join(processed_parts)
resolved, case_insensitive_part = resolve_base_url(base_url, relative_path)
for token, original in token_map.items():
normalize = case_insensitive_part and token in case_insensitive_part
resolved = resolved.replace(
token, original.lower() if normalize else original, 1
)
return resolved
def resolve_base_url(
base_url: Optional[str], given_url: str
) -> Tuple[str, Optional[str]]:
try:
url = nodelike_urlparse(
urljoin(base_url if base_url is not None else "", given_url)
)
resolved = urlunparse(url)
# Schema and domain are case-insensitive.
hostname_port = (
url.hostname or ""
) # can't use parsed.netloc because it includes userinfo (username:password)
if url.port:
hostname_port += f":{url.port}"
case_insensitive_prefix = f"{url.scheme}://{hostname_port}"
return resolved, case_insensitive_prefix
except Exception:
return given_url, None
def nodelike_urlparse(url: str) -> ParseResult:
parsed = urlparse(url, allow_fragments=True)
# https://url.spec.whatwg.org/#special-scheme
is_special_url = parsed.scheme in ["http", "https", "ws", "wss", "ftp", "file"]
if is_special_url:
# special urls have a list path, list paths are serialized as follows: https://url.spec.whatwg.org/#url-path-serializer
# urllib diverges, so we patch it here
if parsed.path == "":
parsed = parsed._replace(path="/")
return parsed
class HarLookupResult(TypedDict, total=False):
action: Literal["error", "redirect", "fulfill", "noentry"]
message: Optional[str]
redirectURL: Optional[str]
status: Optional[int]
headers: Optional["HeadersArray"]
body: Optional[str]
DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS = 30000
DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS = 180000
PLAYWRIGHT_MAX_DEADLINE = 2147483647 # 2^31-1
class TimeoutSettings:
@staticmethod
def launch_timeout(timeout: Optional[float] = None) -> float:
return (
timeout
if timeout is not None
else DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS
)
def __init__(self, parent: Optional["TimeoutSettings"]) -> None:
self._parent = parent
self._default_timeout: Optional[float] = None
self._default_navigation_timeout: Optional[float] = None
def set_default_timeout(self, timeout: Optional[float]) -> None:
self._default_timeout = timeout
def timeout(self, timeout: float = None) -> float:
if timeout is not None:
return timeout
if self._default_timeout is not None:
return self._default_timeout
if self._parent:
return self._parent.timeout()
return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS
def set_default_navigation_timeout(
self, navigation_timeout: Optional[float]
) -> None:
self._default_navigation_timeout = navigation_timeout
def default_navigation_timeout(self) -> Optional[float]:
return self._default_navigation_timeout
def default_timeout(self) -> Optional[float]:
return self._default_timeout
def navigation_timeout(self, timeout: float = None) -> float:
if timeout is not None:
return timeout
if self._default_navigation_timeout is not None:
return self._default_navigation_timeout
if self._default_timeout is not None:
return self._default_timeout
if self._parent:
return self._parent.navigation_timeout()
return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS
def serialize_error(ex: Exception, tb: Optional[TracebackType]) -> ErrorPayload:
return ErrorPayload(
message=str(ex), name="Error", stack="".join(traceback.format_tb(tb))
)
def parse_error(error: ErrorPayload, log: Optional[str] = None) -> Error:
base_error_class = Error
if error.get("name") == "TimeoutError":
base_error_class = TimeoutError
if error.get("name") == "TargetClosedError":
base_error_class = TargetClosedError
if not log:
log = ""
exc = base_error_class(patch_error_message(error["message"]) + log)
exc._name = error["name"]
exc._stack = error["stack"]
return exc
def patch_error_message(message: str) -> str:
match = re.match(r"(\w+)(: expected .*)", message)
if match:
message = to_snake_case(match.group(1)) + match.group(2)
message = message.replace(
"Pass { acceptDownloads: true }", "Pass 'accept_downloads=True'"
)
return message
def locals_to_params(args: Dict) -> Dict:
copy = {}
for key in args:
if key == "self":
continue
if args[key] is not None:
copy[key] = (
args[key]
if not isinstance(args[key], Dict)
else locals_to_params(args[key])
)
return copy
def monotonic_time() -> int:
return math.floor(time.monotonic() * 1000)
def create_task_and_ignore_exception(
loop: asyncio.AbstractEventLoop, coro: Awaitable[Any]
) -> None:
async def _ignore_exception() -> None:
try:
await coro
except Exception:
pass
loop.create_task(_ignore_exception())
class RouteHandlerInvocation:
complete: "asyncio.Future"
route: "Route"
def __init__(self, complete: "asyncio.Future", route: "Route") -> None:
self.complete = complete
self.route = route
class RouteHandler:
def __init__(
self,
base_url: Optional[str],
url: URLMatch,
handler: RouteHandlerCallback,
is_sync: bool,
times: Optional[int] = None,
):
self._base_url = base_url
self.url = url
self.handler = handler
self._times = times if times else math.inf
self._handled_count = 0
self._is_sync = is_sync
self._ignore_exception = False
self._active_invocations: Set[RouteHandlerInvocation] = set()
def matches(self, request_url: str) -> bool:
return url_matches(self._base_url, request_url, self.url)
async def handle(self, route: "Route") -> bool:
handler_invocation = RouteHandlerInvocation(
asyncio.get_running_loop().create_future(), route
)
self._active_invocations.add(handler_invocation)
try:
return await self._handle_internal(route)
except Exception as e:
# If the handler was stopped (without waiting for completion), we ignore all exceptions.
if self._ignore_exception:
return False
if is_target_closed_error(e):
# We are failing in the handler because the target has closed.
# Give user a hint!
optional_async_prefix = "await " if not self._is_sync else ""
raise rewrite_error(
e,
f"\"{str(e)}\" while running route callback.\nConsider awaiting `{optional_async_prefix}page.unroute_all(behavior='ignoreErrors')`\nbefore the end of the test to ignore remaining routes in flight.",
)
raise e
finally:
handler_invocation.complete.set_result(None)
self._active_invocations.remove(handler_invocation)
async def _handle_internal(self, route: "Route") -> bool:
handled_future = route._start_handling()
self._handled_count += 1
if self._is_sync:
handler_finished_future = route._loop.create_future()
def _handler() -> None:
try:
self.handler(route, route.request) # type: ignore
handler_finished_future.set_result(None)
except Exception as e:
handler_finished_future.set_exception(e)
# As with event handlers, each route handler is a potentially blocking context
# so it needs a fiber.
g = RouteGreenlet(_handler)
g.switch()
await handler_finished_future
else:
coro_or_future = self.handler(route, route.request) # type: ignore
if coro_or_future:
# separate task so that we get a proper stack trace for exceptions / tracing api_name extraction
await asyncio.ensure_future(coro_or_future)
return await handled_future
async def stop(self, behavior: Literal["ignoreErrors", "wait"]) -> None:
# When a handler is manually unrouted or its page/context is closed we either
# - wait for the current handler invocations to finish
# - or do not wait, if the user opted out of it, but swallow all exceptions
# that happen after the unroute/close.
if behavior == "ignoreErrors":
self._ignore_exception = True
else:
tasks = []
for activation in self._active_invocations:
if not activation.route._did_throw:
tasks.append(activation.complete)
await asyncio.gather(*tasks)
@property
def will_expire(self) -> bool:
return self._handled_count + 1 >= self._times
@staticmethod
def prepare_interception_patterns(
handlers: List["RouteHandler"],
) -> List[Dict[str, str]]:
patterns = []
all = False
for handler in handlers:
if isinstance(handler.url, str):
patterns.append({"glob": handler.url})
elif isinstance(handler.url, re.Pattern):
patterns.append(
{
"regexSource": handler.url.pattern,
"regexFlags": escape_regex_flags(handler.url),
}
)
else:
all = True
if all:
return [{"glob": "**/*"}]
return patterns
to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))")
def to_snake_case(name: str) -> str:
return to_snake_case_regex.sub(r"_\1", name).lower()
def make_dirs_for_file(path: Union[Path, str]) -> None:
if not os.path.isabs(path):
path = Path.cwd() / path
os.makedirs(os.path.dirname(path), exist_ok=True)
async def async_writefile(file: Union[str, Path], data: Union[str, bytes]) -> None:
def inner() -> None:
with open(file, "w" if isinstance(data, str) else "wb") as fh:
fh.write(data)
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, inner)
async def async_readfile(file: Union[str, Path]) -> bytes:
def inner() -> bytes:
with open(file, "rb") as fh:
return fh.read()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, inner)
T = TypeVar("T")
def to_impl(obj: T) -> T:
if hasattr(obj, "_impl_obj"):
return cast(Any, obj)._impl_obj
return obj
def object_to_array(obj: Optional[Dict]) -> Optional[List[NameValue]]:
if not obj:
return None
result = []
for key, value in obj.items():
result.append(NameValue(name=key, value=str(value)))
return result
def is_file_payload(value: Optional[Any]) -> bool:
return (
isinstance(value, dict)
and "name" in value
and "mimeType" in value
and "buffer" in value
)
TEXTUAL_MIME_TYPE = re.compile(
r"^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$"
)
def is_textual_mime_type(mime_type: str) -> bool:
return bool(TEXTUAL_MIME_TYPE.match(mime_type))
@overload
def to_milliseconds(value: Union[float, datetime.timedelta]) -> float: ...
@overload
def to_milliseconds(value: None) -> None: ...
@overload
def to_milliseconds(
value: Union[float, datetime.timedelta, None],
) -> Optional[float]: ...
def to_milliseconds(
value: Union[float, datetime.timedelta, None],
) -> Optional[float]:
if value is None:
return None
if isinstance(value, datetime.timedelta):
return value / datetime.timedelta(milliseconds=1)
return value
@@ -0,0 +1,159 @@
# 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 inspect
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
from playwright._impl._errors import Error
from playwright._impl._map import Map
from playwright._impl._signature import signature
API_ATTR = "_pw_api_instance_"
IMPL_ATTR = "_pw_impl_instance_"
class ImplWrapper:
def __init__(self, impl_obj: Any) -> None:
self._impl_obj = impl_obj
def __repr__(self) -> str:
return self._impl_obj.__repr__()
class ImplToApiMapping:
def __init__(self) -> None:
self._mapping: Dict[type, type] = {}
def register(self, impl_class: type, api_class: type) -> None:
self._mapping[impl_class] = api_class
def from_maybe_impl(
self, obj: Any, visited: Optional[Map[Any, Union[List, Dict]]] = None
) -> Any:
# Python does share default arguments between calls, so we need to
# create a new map if it is not provided.
if not visited:
visited = Map()
if not obj:
return obj
if isinstance(obj, dict):
if obj in visited:
return visited[obj]
o: Dict = {}
visited[obj] = o
for name, value in obj.items():
o[name] = self.from_maybe_impl(value, visited)
return o
if isinstance(obj, list):
if obj in visited:
return visited[obj]
a: List = []
visited[obj] = a
for item in obj:
a.append(self.from_maybe_impl(item, visited))
return a
api_class = self._mapping.get(type(obj))
if api_class:
api_instance = getattr(obj, API_ATTR, None)
if not api_instance:
api_instance = api_class(obj)
setattr(obj, API_ATTR, api_instance)
return api_instance
else:
return obj
def from_impl(self, obj: Any) -> Any:
assert obj
result = self.from_maybe_impl(obj)
assert result
return result
def from_impl_nullable(self, obj: Any = None) -> Optional[Any]:
return self.from_impl(obj) if obj else None
def from_impl_list(self, items: Sequence[Any]) -> List[Any]:
return list(map(lambda a: self.from_impl(a), items))
def from_impl_dict(self, map: Dict[str, Any]) -> Dict[str, Any]:
return {name: self.from_impl(value) for name, value in map.items()}
def to_impl(
self, obj: Any, visited: Optional[Map[Any, Union[List, Dict]]] = None
) -> Any:
if visited is None:
visited = Map()
try:
if not obj:
return obj
if isinstance(obj, dict):
if obj in visited:
return visited[obj]
o: Dict = {}
visited[obj] = o
for name, value in obj.items():
o[name] = self.to_impl(value, visited)
return o
if isinstance(obj, list):
if obj in visited:
return visited[obj]
a: List = []
visited[obj] = a
for item in obj:
a.append(self.to_impl(item, visited))
return a
if isinstance(obj, ImplWrapper):
return obj._impl_obj
return obj
except RecursionError:
raise Error("Maximum argument depth exceeded")
def wrap_handler(self, handler: Callable[..., Any]) -> Callable[..., None]:
def wrapper_func(*args: Any) -> Any:
parameters = signature(handler).parameters
has_varargs = any(
parameter.kind == inspect.Parameter.VAR_POSITIONAL
for parameter in parameters.values()
)
arg_count = (
len(args)
if has_varargs
else sum(
parameter.kind
in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
)
for parameter in parameters.values()
)
)
return handler(
*list(map(lambda a: self.from_maybe_impl(a), args))[:arg_count]
)
if inspect.ismethod(handler):
wrapper = getattr(handler.__self__, IMPL_ATTR + handler.__name__, None)
if not wrapper:
wrapper = wrapper_func
setattr(
handler.__self__,
IMPL_ATTR + handler.__name__,
wrapper,
)
return wrapper
wrapper = getattr(handler, IMPL_ATTR, None)
if not wrapper:
wrapper = wrapper_func
setattr(handler, IMPL_ATTR, wrapper)
return wrapper
@@ -0,0 +1,111 @@
# 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._impl._connection import Channel
from playwright._impl._helper import MouseButton, locals_to_params
class Keyboard:
def __init__(self, channel: Channel) -> None:
self._channel = channel
self._loop = channel._connection._loop
self._dispatcher_fiber = channel._connection._dispatcher_fiber
async def down(self, key: str) -> None:
await self._channel.send("keyboardDown", None, locals_to_params(locals()))
async def up(self, key: str) -> None:
await self._channel.send("keyboardUp", None, locals_to_params(locals()))
async def insert_text(self, text: str) -> None:
await self._channel.send("keyboardInsertText", None, locals_to_params(locals()))
async def type(self, text: str, delay: float = None) -> None:
await self._channel.send("keyboardType", None, locals_to_params(locals()))
async def press(self, key: str, delay: float = None) -> None:
await self._channel.send("keyboardPress", None, locals_to_params(locals()))
class Mouse:
def __init__(self, channel: Channel) -> None:
self._channel = channel
self._loop = channel._connection._loop
self._dispatcher_fiber = channel._connection._dispatcher_fiber
async def move(self, x: float, y: float, steps: int = None) -> None:
await self._channel.send("mouseMove", None, locals_to_params(locals()))
async def down(
self,
button: MouseButton = None,
clickCount: int = None,
) -> None:
await self._channel.send("mouseDown", None, locals_to_params(locals()))
async def up(
self,
button: MouseButton = None,
clickCount: int = None,
) -> None:
await self._channel.send("mouseUp", None, locals_to_params(locals()))
async def _click(
self,
x: float,
y: float,
delay: float = None,
button: MouseButton = None,
clickCount: int = None,
title: str = None,
) -> None:
await self._channel.send(
"mouseClick", None, locals_to_params(locals()), title=title
)
async def click(
self,
x: float,
y: float,
delay: float = None,
button: MouseButton = None,
clickCount: int = None,
) -> None:
params = locals()
del params["self"]
await self._click(**params)
async def dblclick(
self,
x: float,
y: float,
delay: float = None,
button: MouseButton = None,
) -> None:
await self._click(
x, y, delay=delay, button=button, clickCount=2, title="Double click"
)
async def wheel(self, deltaX: float, deltaY: float) -> None:
await self._channel.send("mouseWheel", None, locals_to_params(locals()))
class Touchscreen:
def __init__(self, channel: Channel) -> None:
self._channel = channel
self._loop = channel._connection._loop
self._dispatcher_fiber = channel._connection._dispatcher_fiber
async def tap(self, x: float, y: float) -> None:
await self._channel.send("touchscreenTap", None, locals_to_params(locals()))
@@ -0,0 +1,149 @@
# 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.
# Types that the implementation uses internally. The public TypedDicts are
# generated into _api_structures.py.
from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict
from playwright._impl._api_structures import NameValue
HeadersArray = List[NameValue]
Headers = Dict[str, str]
class ServerFilePayload(TypedDict):
name: str
mimeType: str
buffer: str
class FormField(TypedDict, total=False):
name: str
value: Optional[str]
file: Optional[ServerFilePayload]
class ExpectedTextValue(TypedDict, total=False):
string: str
regexSource: str
regexFlags: str
matchSubstring: bool
normalizeWhiteSpace: bool
ignoreCase: Optional[bool]
class FrameExpectOptions(TypedDict, total=False):
expressionArg: Any
expectedText: Optional[Sequence[ExpectedTextValue]]
expectedNumber: Optional[float]
expectedValue: Optional[Any]
useInnerText: Optional[bool]
isNot: bool
timeout: Optional[float]
pseudo: Optional[str]
class FrameExpectResult(TypedDict, total=False):
matches: bool
received: Any
log: Optional[List[str]]
errorMessage: Optional[str]
timedOut: Optional[bool]
AriaRole = Literal[
"alert",
"alertdialog",
"application",
"article",
"banner",
"blockquote",
"button",
"caption",
"cell",
"checkbox",
"code",
"columnheader",
"combobox",
"complementary",
"contentinfo",
"definition",
"deletion",
"dialog",
"directory",
"document",
"emphasis",
"feed",
"figure",
"form",
"generic",
"grid",
"gridcell",
"group",
"heading",
"img",
"insertion",
"link",
"list",
"listbox",
"listitem",
"log",
"main",
"marquee",
"math",
"menu",
"menubar",
"menuitem",
"menuitemcheckbox",
"menuitemradio",
"meter",
"navigation",
"none",
"note",
"option",
"paragraph",
"presentation",
"progressbar",
"radio",
"radiogroup",
"region",
"row",
"rowgroup",
"rowheader",
"scrollbar",
"search",
"searchbox",
"separator",
"slider",
"spinbutton",
"status",
"strong",
"subscript",
"superscript",
"switch",
"tab",
"table",
"tablist",
"tabpanel",
"term",
"textbox",
"time",
"timer",
"toolbar",
"tooltip",
"tree",
"treegrid",
"treeitem",
]

Some files were not shown because too many files have changed in this diff Show More