This commit is contained in:
kgod
2026-05-26 21:02:17 +08:00
commit 8697477a53
10000 changed files with 1541403 additions and 0 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,350 @@
# 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 Any, Dict, List, Literal, Optional, Sequence, TypedDict, Union
# These are the structures that we like keeping in a JSON form for their potential
# reuse between SDKs / services. They are public and are a part of the
# stable API.
# Explicitly mark optional params as such for the documentation
# If there is at least one optional param, set total=False for better mypy handling.
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 StorageStateCookie(TypedDict, total=False):
name: str
value: str
domain: str
path: str
expires: float
httpOnly: bool
secure: bool
sameSite: Literal["Lax", "None", "Strict"]
# TODO: We are waiting for PEP705 so SetCookieParam can be readonly and matches Cookie.
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 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 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 StorageState(TypedDict, total=False):
cookies: List[StorageStateCookie]
origins: List[OriginState]
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 ResourceTiming(TypedDict):
startTime: float
domainLookupStart: float
domainLookupEnd: float
connectStart: float
secureConnectionStart: float
connectEnd: float
requestStart: float
responseStart: float
responseEnd: float
class RequestSizes(TypedDict):
requestBodySize: int
requestHeadersSize: int
responseBodySize: int
responseHeadersSize: int
class ViewportSize(TypedDict):
width: int
height: int
class SourceLocation(TypedDict):
url: str
line: int
column: int
lineNumber: int
columnNumber: int
class WebErrorLocation(TypedDict):
url: str
line: int
column: int
class FilePayload(TypedDict):
name: str
mimeType: str
buffer: bytes
class DropPayload(TypedDict, total=False):
files: Optional[
Union[str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload]]
]
data: Optional[Dict[str, str]]
class RemoteAddr(TypedDict):
ipAddress: str
port: int
class BrowserBindResult(TypedDict):
endpoint: str
class SecurityDetails(TypedDict):
issuer: Optional[str]
protocol: Optional[str]
subjectName: Optional[str]
validFrom: Optional[float]
validTo: Optional[float]
class NameValue(TypedDict):
name: str
value: str
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):
matches: bool
received: Any
log: List[str]
errorMessage: Optional[str]
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",
]
class TracingGroupLocation(TypedDict):
file: str
line: Optional[int]
column: Optional[int]
class DebuggerLocation(TypedDict):
file: str
line: Optional[int]
column: Optional[int]
class DebuggerPausedDetails(TypedDict):
location: DebuggerLocation
title: str
class ScreencastFrame(TypedDict):
data: bytes
viewportWidth: int
viewportHeight: int
@@ -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,105 @@
# 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]
) -> Callable[..., None]:
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,293 @@
# 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
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: 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,
) -> 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: 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,
) -> 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,743 @@
# 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._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,
locals_to_params,
parse_error,
to_impl,
)
from playwright._impl._network import (
Request,
Response,
Route,
WebSocketRoute,
WebSocketRouteHandler,
serialize_headers,
)
from playwright._impl._page import BindingCall, Page, Worker
from playwright._impl._str_utils import escape_regex_flags
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",
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._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(
"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.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:
asyncio.create_task(
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,
{
"name": name if isinstance(name, str) else None,
"nameRegexSource": name.pattern if isinstance(name, Pattern) else None,
"nameRegexFlags": (
escape_regex_flags(name) if isinstance(name, Pattern) else None
),
"domain": domain if isinstance(domain, str) else None,
"domainRegexSource": (
domain.pattern if isinstance(domain, Pattern) else None
),
"domainRegexFlags": (
escape_regex_flags(domain) if isinstance(domain, Pattern) else None
),
"path": path if isinstance(path, str) else None,
"pathRegexSource": path.pattern if isinstance(path, Pattern) else None,
"pathRegexFlags": (
escape_regex_flags(path) if isinstance(path, Pattern) else None
),
},
)
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.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
) -> StorageState:
result = await self._channel.send_return_as_dict(
"storageState", None, {"indexedDB": indexedDB}
)
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":
asyncio.create_task(dialog.accept())
else:
asyncio.create_task(dialog.dismiss())
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
@@ -0,0 +1,370 @@
# 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
import sys
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
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: 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,
) -> 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():
# Can be dropped once we drop Python 3.9 support (10/2025):
# https://github.com/python/cpython/issues/82852
if sys.platform == "win32" and sys.version_info[:2] < (3, 10):
return str(pathlib.Path.cwd() / userDataDir)
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,
) -> 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
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",
None,
{
"endpoint": endpoint,
"headers": headers,
"slowMo": slowMo,
"timeout": timeout if timeout is not None else 0,
"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()
browser._on_close()
connection.cleanup(reason)
# TODO: Backport https://github.com/microsoft/playwright/commit/d8d5289e8692c9b1265d23ee66988d1ac5122f33
# 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.
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 is not None 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"]
del params["recordVideoDir"]
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"]
)
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,673 @@
# 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 sys
import traceback
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Mapping,
Optional,
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, 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]]
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,
)
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. waitForEventInfo(after).
self._connection.wrap_api_call_sync(
lambda: self._connection._send_message_to_server(
self._object,
method,
_augment_params(params, timeout_calculator),
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
callback = self._connection._send_message_to_server(
self._object, method, _augment_params(params, timeout_calculator)
)
done, _ = await asyncio.wait(
{
self._connection._transport.on_error_future,
callback.future,
},
return_when=asyncio.FIRST_COMPLETED,
)
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, no_reply: bool = False) -> None:
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)
self._waiting_for_object: Dict[str, Callable[[ChannelOwner], None]] = {}
self._last_id = 0
self._objects: Dict[str, ChannelOwner] = {}
self._callbacks: Dict[int, 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:
self.playwright_future.set_result(await self._root_object.initialize())
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 call_on_object_with_known_name(
self, guid: str, callback: Callable[[ChannelOwner], None]
) -> None:
self._waiting_for_object[guid] = callback
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, 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 = self._last_id
callback = ProtocolCallback(self._loop, no_reply=no_reply)
task = asyncio.current_task(self._loop)
callback.stack_trace = cast(
traceback.StackSummary,
getattr(task, "__pw_stack_trace__", 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"],
}
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._callbacks[id] = callback
self._transport.send(message)
return callback
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. waitForEventInfo(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._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)
result = self._object_factory(parent, type, guid, initializer)
if guid in self._waiting_for_object:
self._waiting_for_object.pop(guid)(result)
return result
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)
st: List[inspect.FrameInfo] = getattr(
task, "__pw_stack__", None
) or inspect.stack(0)
parsed_st = _extract_stack_trace_information_from_stack(st, 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)
st: List[inspect.FrameInfo] = getattr(
task, "__pw_stack__", None
) or inspect.stack(0)
parsed_st = _extract_stack_trace_information_from_stack(st, 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 _extract_stack_trace_information_from_stack(
st: List[inspect.FrameInfo], is_internal: bool, title: str = None
) -> ParsedStackTrace:
playwright_module_path = str(Path(playwright.__file__).parents[0])
last_internal_api_name = ""
api_name = ""
parsed_frames: List[StackFrame] = []
for frame in st:
# 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__ == frame.filename:
continue
is_playwright_internal = frame.filename.startswith(playwright_module_path)
method_name = ""
if "self" in frame[0].f_locals:
method_name = frame[0].f_locals["self"].__class__.__name__ + "."
method_name += frame[0].f_code.co_name
if not is_playwright_internal:
parsed_frames.append(
{
"file": frame.filename,
"line": frame.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 = ""
if not api_name:
api_name = last_internal_api_name
return {
"frames": parsed_frames,
"apiName": "" if is_internal else api_name,
"title": title,
}
def _augment_params(
params: Optional[Dict],
timeout_calculator: Optional[Callable[[Optional[float]], float]],
) -> Dict:
if params is None:
params = {}
if timeout_calculator:
params["timeout"] = timeout_calculator(params.get("timeout"))
return _filter_none(params)
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,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 asyncio
import inspect
import traceback
from typing import Awaitable, Callable, Dict
import greenlet
from playwright._impl._connection import ChannelOwner
from playwright._impl._errors import Error, is_target_closed_error
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: object) -> object:
__tracebackhide__ = True
if self._loop.is_closed():
coro.close() # type: ignore
raise Error("Event loop is closed! Is Playwright already stopped?")
g_self = greenlet.getcurrent()
task = self._loop.create_task(coro) # type: ignore
setattr(task, "__pw_stack__", inspect.stack(0))
setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10))
task.add_done_callback(lambda _: g_self.switch())
while not task.done():
self._dispatcher_fiber.switch() # type: ignore
asyncio._set_running_loop(self._loop)
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,466 @@
# 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 mimetypes
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,
) -> 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,
) -> 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,
) -> 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,
) -> 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,
) -> None:
if checked:
await self.check(
position=position,
timeout=timeout,
force=force,
trial=trial,
)
else:
await self.uncheck(
position=position,
timeout=timeout,
force=force,
trial=trial,
)
async def check(
self,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = 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,
) -> 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"] = 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"]:
mime_type, _ = mimetypes.guess_type(path)
if mime_type == "image/png":
return "png"
if mime_type == "image/jpeg":
return "jpeg"
raise Error(f'Unsupported screenshot mime type for path "{path}": {mime_type}')
@@ -0,0 +1,60 @@
# 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 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
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
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,599 @@
# 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, Union, cast
import playwright._impl._network as network
from playwright._impl._api_structures import (
ClientCertificate,
FilePayload,
FormField,
Headers,
HttpCredentials,
ProxySettings,
ServerFilePayload,
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._network import serialize_headers, to_client_certificates_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: 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")
)
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 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
)
assert request or isinstance(
urlOrRequest, str
), "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)
assert (
(1 if data else 0) + (1 if form else 0) + (1 if multipart else 0)
) <= 1, "Only one of 'data', 'form' or 'multipart' can be specified"
assert (
maxRedirects is None or maxRedirects >= 0
), "'max_redirects' must be greater than or equal to '0'"
assert (
maxRetries is None or maxRetries >= 0
), "'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, bool)):
json_data = json.dumps(data)
else:
raise Error(f"Unsupported 'data' type: {type(data)}")
elif form:
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)
elif multipart:
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:
# Convert file-like values to ServerFilePayload structs.
for name, value in multipart.items():
if is_file_payload(value):
payload = cast(FilePayload, value)
assert isinstance(
payload["buffer"], bytes
), f"Unexpected buffer type of 'data.{name}'"
multipart_data.append(
FormField(name=name, file=file_payload_to_json(payload))
)
elif isinstance(value, str):
multipart_data.append(FormField(name=name, value=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,
) -> StorageState:
result = await self._channel.send_return_as_dict(
"storageState", None, {"indexedDB": indexedDB}
)
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)
assert isinstance(
payload["buffer"], bytes
), 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()
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 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"] == "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:
json.loads(value)
return True
except json.JSONDecodeError:
return False
@@ -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,952 @@
# 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 (
AriaRole,
DropPayload,
FilePayload,
FrameExpectOptions,
FrameExpectResult,
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._js_handle import (
JSHandle,
Serializable,
add_source_url_to_script,
parse_result,
parse_value,
serialize_argument,
)
from playwright._impl._locator import (
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
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._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"])
result = await self._channel.send_return_as_dict(
"expect",
self._timeout,
{
"selector": selector,
"expression": expression,
**options,
},
title=title,
)
if result.get("received"):
result["received"] = parse_value(result["received"])
return result
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) -> str:
return await self._channel.send("content", None)
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 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,
) -> 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,
) -> 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,
) -> 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,
) -> 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) -> FrameLocator:
return FrameLocator(self, 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,
) -> 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,
) -> 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,
) -> 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,
) -> 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,
) -> None:
if checked:
await self.check(
selector=selector,
position=position,
timeout=timeout,
force=force,
strict=strict,
trial=trial,
)
else:
await self.uncheck(
selector=selector,
position=position,
timeout=timeout,
force=force,
strict=strict,
trial=trial,
)
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,49 @@
# 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 Tuple
import greenlet
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>"
@@ -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._api_structures import HeadersArray
from playwright._impl._helper import (
HarLookupResult,
RouteFromHarNotFoundPolicy,
URLMatch,
)
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:
asyncio.create_task(
self._local_utils._channel.send("harClose", None, {"harId": self._har_id})
)
@@ -0,0 +1,587 @@
# 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,
)
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._api_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: int
guid: str
method: str
params: ParsedMessageParams
result: Any
error: ErrorPayload
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)
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))
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,142 @@
# 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
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:
arg_count = len(inspect.signature(handler).parameters)
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,336 @@
# 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 collections.abc
import datetime
import math
import struct
import traceback
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from urllib.parse import ParseResult, urlparse, urlunparse
from playwright._impl._connection import Channel, ChannelOwner, from_channel
from playwright._impl._errors import Error, is_target_closed_error
from playwright._impl._map import Map
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._element_handle import ElementHandle
Serializable = Any
class VisitorInfo:
visited: Map[Any, int]
last_id: int
def __init__(self) -> None:
self.visited = Map()
self.last_id = 0
def visit(self, obj: Any) -> int:
assert obj not in self.visited
self.last_id += 1
self.visited[obj] = self.last_id
return self.last_id
class JSHandle(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._preview = self._initializer["preview"]
self._channel.on(
"previewUpdated", lambda params: self._on_preview_updated(params["preview"])
)
def __repr__(self) -> str:
return f"<JSHandle preview={self._preview}>"
def __str__(self) -> str:
return self._preview
def _on_preview_updated(self, preview: str) -> None:
self._preview = preview
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 get_property(self, propertyName: str) -> "JSHandle":
return from_channel(
await self._channel.send("getProperty", None, dict(name=propertyName))
)
async def get_properties(self) -> Dict[str, "JSHandle"]:
return {
prop["name"]: from_channel(prop["value"])
for prop in await self._channel.send(
"getPropertyList",
None,
)
}
def as_element(self) -> Optional["ElementHandle"]:
return None
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 json_value(self) -> Any:
return parse_result(
await self._channel.send(
"jsonValue",
None,
)
)
def serialize_value(
value: Any, handles: List[Channel], visitor_info: Optional[VisitorInfo] = None
) -> Any:
if visitor_info is None:
visitor_info = VisitorInfo()
if isinstance(value, JSHandle):
h = len(handles)
handles.append(value._channel)
return dict(h=h)
if value is None:
return dict(v="null")
if isinstance(value, float):
if value == float("inf"):
return dict(v="Infinity")
if value == float("-inf"):
return dict(v="-Infinity")
if value == float("-0"):
return dict(v="-0")
if math.isnan(value):
return dict(v="NaN")
if isinstance(value, datetime.datetime):
# Node.js Date objects are always in UTC.
return {
"d": datetime.datetime.strftime(
value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ"
)
}
if isinstance(value, Exception):
return {
"e": {
"m": str(value),
"n": (
(value.name or "")
if isinstance(value, Error)
else value.__class__.__name__
),
"s": (
(value.stack or "")
if isinstance(value, Error)
else "".join(
traceback.format_exception(type(value), value=value, tb=None)
)
),
}
}
if isinstance(value, bool):
return {"b": value}
if isinstance(value, (int, float)):
return {"n": value}
if isinstance(value, str):
return {"s": value}
if isinstance(value, ParseResult):
return {"u": urlunparse(value)}
if value in visitor_info.visited:
return dict(ref=visitor_info.visited[value])
if isinstance(value, collections.abc.Sequence) and not isinstance(value, str):
id = visitor_info.visit(value)
a = []
for e in value:
a.append(serialize_value(e, handles, visitor_info))
return dict(a=a, id=id)
if isinstance(value, dict):
id = visitor_info.visit(value)
o = []
for name in value:
o.append(
{"k": name, "v": serialize_value(value[name], handles, visitor_info)}
)
return dict(o=o, id=id)
return dict(v="undefined")
def serialize_argument(arg: Serializable = None) -> Any:
handles: List[Channel] = []
value = serialize_value(arg, handles)
return dict(value=value, handles=handles)
def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any:
if refs is None:
refs = {}
if value is None:
return None
if isinstance(value, dict):
if "ref" in value:
return refs[value["ref"]]
if "v" in value:
v = value["v"]
if v == "Infinity":
return float("inf")
if v == "-Infinity":
return float("-inf")
if v == "-0":
return float("-0")
if v == "NaN":
return float("nan")
if v == "undefined":
return None
if v == "null":
return None
return v
if "u" in value:
return urlparse(value["u"])
if "bi" in value:
return int(value["bi"])
if "e" in value:
error = Error(value["e"]["m"])
error._name = value["e"]["n"]
error._stack = value["e"]["s"]
return error
if "a" in value:
a: List = []
refs[value["id"]] = a
for e in value["a"]:
a.append(parse_value(e, refs))
return a
if "d" in value:
# Node.js Date objects are always in UTC.
return datetime.datetime.strptime(
value["d"], "%Y-%m-%dT%H:%M:%S.%fZ"
).replace(tzinfo=datetime.timezone.utc)
if "o" in value:
o: Dict = {}
refs[value["id"]] = o
for e in value["o"]:
o[e["k"]] = parse_value(e["v"], refs)
return o
if "n" in value:
return value["n"]
if "s" in value:
return value["s"]
if "b" in value:
return value["b"]
if "ta" in value:
encoded_bytes = value["ta"]["b"]
decoded_bytes = base64.b64decode(encoded_bytes)
array_type = value["ta"]["k"]
if array_type == "i8":
word_size = 1
fmt = "b"
elif array_type == "ui8" or array_type == "ui8c":
word_size = 1
fmt = "B"
elif array_type == "i16":
word_size = 2
fmt = "h"
elif array_type == "ui16":
word_size = 2
fmt = "H"
elif array_type == "i32":
word_size = 4
fmt = "i"
elif array_type == "ui32":
word_size = 4
fmt = "I"
elif array_type == "f32":
word_size = 4
fmt = "f"
elif array_type == "f64":
word_size = 8
fmt = "d"
elif array_type == "bi64":
word_size = 8
fmt = "q"
elif array_type == "bui64":
word_size = 8
fmt = "Q"
else:
raise ValueError(f"Unsupported array type: {array_type}")
byte_len = len(decoded_bytes)
if byte_len % word_size != 0:
raise ValueError(
f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}"
)
if byte_len == 0:
return []
array_len = byte_len // word_size
# "<" denotes little-endian
format_string = f"<{array_len}{fmt}"
return list(struct.unpack(format_string, decoded_bytes))
return value
def parse_result(result: Any) -> Any:
return parse_value(result)
def add_source_url_to_script(source: str, path: Union[str, Path]) -> str:
return source + "\n//# sourceURL=" + str(path).replace("\n", "")
@@ -0,0 +1,77 @@
# 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 Dict, Optional, cast
from pyee.asyncio import AsyncIOEventEmitter
from playwright._impl._connection import Channel
from playwright._impl._errors import TargetClosedError
from playwright._impl._helper import Error, ParsedMessagePayload
from playwright._impl._transport import Transport
class JsonPipeTransport(AsyncIOEventEmitter, Transport):
def __init__(
self,
loop: asyncio.AbstractEventLoop,
pipe_channel: Channel,
) -> None:
super().__init__(loop)
Transport.__init__(self, loop)
self._stop_requested = False
self._pipe_channel = pipe_channel
def request_stop(self) -> None:
self._stop_requested = True
self._pipe_channel.send_no_reply("close", None, {})
def dispose(self) -> None:
self.on_error_future.cancel()
self._stopped_future.cancel()
async def wait_until_stopped(self) -> None:
await self._stopped_future
async def connect(self) -> None:
self._stopped_future: asyncio.Future = asyncio.Future()
def handle_message(message: Dict) -> None:
if self._stop_requested:
return
self.on_message(cast(ParsedMessagePayload, message))
def handle_closed(reason: Optional[str]) -> None:
self.emit("close", reason)
if reason:
self.on_error_future.set_exception(TargetClosedError(reason))
self._stopped_future.set_result(None)
self._pipe_channel.on(
"message",
lambda params: handle_message(params["message"]),
)
self._pipe_channel.on(
"closed",
lambda params: handle_closed(params.get("reason")),
)
async def run(self) -> None:
await self._stopped_future
def send(self, message: Dict) -> None:
if self._stop_requested:
raise Error("Playwright connection closed")
self._pipe_channel.send_no_reply("send", None, {"message": message})
@@ -0,0 +1,95 @@
# 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 typing import Dict, List, Optional, cast
from playwright._impl._api_structures import HeadersArray
from playwright._impl._connection import ChannelOwner, StackFrame
from playwright._impl._helper import HarLookupResult, locals_to_params
class LocalUtils(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self.devices = {
device["name"]: parse_device_descriptor(device["descriptor"])
for device in initializer["deviceDescriptors"]
}
async def zip(self, params: Dict) -> None:
await self._channel.send("zip", None, params)
async def har_open(self, file: str) -> None:
params = locals_to_params(locals())
await self._channel.send("harOpen", None, params)
async def har_lookup(
self,
harId: str,
url: str,
method: str,
headers: HeadersArray,
isNavigationRequest: bool,
postData: Optional[bytes] = None,
) -> HarLookupResult:
params = locals_to_params(locals())
if "postData" in params:
params["postData"] = base64.b64encode(params["postData"]).decode()
return cast(
HarLookupResult,
await self._channel.send_return_as_dict("harLookup", None, params),
)
async def har_close(self, harId: str) -> None:
params = locals_to_params(locals())
await self._channel.send("harClose", None, params)
async def har_unzip(self, zipFile: str, harFile: str) -> None:
params = locals_to_params(locals())
await self._channel.send("harUnzip", None, params)
async def tracing_started(
self, tracesDir: Optional[str], traceName: str, live: bool = False
) -> str:
params = locals_to_params(locals())
return await self._channel.send("tracingStarted", None, params)
async def trace_discarded(self, stacks_id: str) -> None:
return await self._channel.send("traceDiscarded", None, {"stacksId": stacks_id})
def add_stack_to_tracing_no_reply(self, id: int, frames: List[StackFrame]) -> None:
self._channel.send_no_reply(
"addStackToTracingNoReply",
None,
{
"callData": {
"stack": frames,
"id": id,
}
},
)
def parse_device_descriptor(dict: Dict) -> Dict:
return {
"user_agent": dict["userAgent"],
"viewport": dict["viewport"],
"device_scale_factor": dict["deviceScaleFactor"],
"is_mobile": dict["isMobile"],
"has_touch": dict["hasTouch"],
"default_browser_type": dict["defaultBrowserType"],
}
@@ -0,0 +1,996 @@
# 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 json
import pathlib
import re
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
List,
Literal,
Optional,
Pattern,
Sequence,
Tuple,
TypeVar,
Union,
)
from playwright._impl._api_structures import (
AriaRole,
DropPayload,
FilePayload,
FloatRect,
FrameExpectOptions,
FrameExpectResult,
Position,
)
from playwright._impl._element_handle import ElementHandle
from playwright._impl._helper import (
Error,
KeyboardModifier,
MouseButton,
locals_to_params,
monotonic_time,
to_impl,
)
from playwright._impl._js_handle import Serializable
from playwright._impl._str_utils import (
escape_for_attribute_selector,
escape_for_text_selector,
)
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._frame import Frame
from playwright._impl._js_handle import JSHandle
from playwright._impl._page import Page
T = TypeVar("T")
class Locator:
def __init__(
self,
frame: "Frame",
selector: str,
has_text: Union[str, Pattern[str]] = None,
has_not_text: Union[str, Pattern[str]] = None,
has: "Locator" = None,
has_not: "Locator" = None,
visible: bool = None,
) -> None:
self._frame = frame
self._selector = selector
self._loop = frame._loop
self._dispatcher_fiber = frame._connection._dispatcher_fiber
if has_text:
self._selector += f" >> internal:has-text={escape_for_text_selector(has_text, exact=False)}"
if has:
if has._frame != frame:
raise Error('Inner "has" locator must belong to the same frame.')
self._selector += " >> internal:has=" + json.dumps(
has._selector, ensure_ascii=False
)
if has_not_text:
self._selector += f" >> internal:has-not-text={escape_for_text_selector(has_not_text, exact=False)}"
if has_not:
locator = has_not
if locator._frame != frame:
raise Error('Inner "has_not" locator must belong to the same frame.')
self._selector += " >> internal:has-not=" + json.dumps(locator._selector)
if visible is not None:
self._selector += f" >> visible={bool_to_js_bool(visible)}"
def __repr__(self) -> str:
return f"<Locator frame={self._frame!r} selector={self._selector!r}>"
async def _with_element(
self,
task: Callable[[ElementHandle, float], Awaitable[T]],
timeout: float = None,
) -> T:
timeout = self._frame._timeout(timeout)
deadline = (monotonic_time() + timeout) if timeout else 0
handle = await self.element_handle(timeout=timeout)
if not handle:
raise Error(f"Could not resolve {self._selector} to DOM Element")
try:
return await task(
handle,
(deadline - monotonic_time()) if deadline else 0,
)
finally:
await handle.dispose()
def _equals(self, locator: "Locator") -> bool:
return self._frame == locator._frame and self._selector == locator._selector
@property
def page(self) -> "Page":
return self._frame.page
async def bounding_box(self, timeout: float = None) -> Optional[FloatRect]:
return await self._with_element(
lambda h, _: h.bounding_box(),
timeout,
)
async def check(
self,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.check(self._selector, strict=True, **params)
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,
) -> None:
params = locals_to_params(locals())
return await self._frame._click(self._selector, strict=True, **params)
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,
) -> None:
params = locals_to_params(locals())
return await self._frame.dblclick(self._selector, strict=True, **params)
async def dispatch_event(
self,
type: str,
eventInit: Dict = None,
timeout: float = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.dispatch_event(self._selector, strict=True, **params)
async def evaluate(
self, expression: str, arg: Serializable = None, timeout: float = None
) -> Any:
return await self._with_element(
lambda h, _: h.evaluate(expression, arg),
timeout,
)
async def evaluate_all(self, expression: str, arg: Serializable = None) -> Any:
params = locals_to_params(locals())
return await self._frame.eval_on_selector_all(self._selector, **params)
async def evaluate_handle(
self, expression: str, arg: Serializable = None, timeout: float = None
) -> "JSHandle":
return await self._with_element(
lambda h, _: h.evaluate_handle(expression, arg), timeout
)
async def fill(
self,
value: str,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.fill(self._selector, strict=True, **params)
async def clear(
self,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
) -> None:
params = locals_to_params(locals())
await self._frame._fill(self._selector, value="", title="Clear", **params)
def locator(
self,
selectorOrLocator: Union[str, "Locator"],
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: "Locator" = None,
hasNot: "Locator" = None,
) -> "Locator":
if isinstance(selectorOrLocator, str):
return Locator(
self._frame,
f"{self._selector} >> {selectorOrLocator}",
has_text=hasText,
has_not_text=hasNotText,
has_not=hasNot,
has=has,
)
selectorOrLocator = to_impl(selectorOrLocator)
if selectorOrLocator._frame != self._frame:
raise Error("Locators must belong to the same frame.")
return Locator(
self._frame,
f"{self._selector} >> internal:chain={json.dumps(selectorOrLocator._selector)}",
has_text=hasText,
has_not_text=hasNotText,
has_not=hasNot,
has=has,
)
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) -> "FrameLocator":
return FrameLocator(self._frame, self._selector + " >> " + selector)
async def element_handle(
self,
timeout: float = None,
) -> ElementHandle:
params = locals_to_params(locals())
handle = await self._frame.wait_for_selector(
self._selector, strict=True, state="attached", **params
)
assert handle
return handle
async def element_handles(self) -> List[ElementHandle]:
return await self._frame.query_selector_all(self._selector)
@property
def first(self) -> "Locator":
return Locator(self._frame, f"{self._selector} >> nth=0")
@property
def last(self) -> "Locator":
return Locator(self._frame, f"{self._selector} >> nth=-1")
def nth(self, index: int) -> "Locator":
return Locator(self._frame, f"{self._selector} >> nth={index}")
@property
def content_frame(self) -> "FrameLocator":
return FrameLocator(self._frame, self._selector)
def describe(self, description: str) -> "Locator":
return Locator(
self._frame,
f"{self._selector} >> internal:describe={json.dumps(description)}",
)
@property
def description(self) -> Optional[str]:
try:
match = re.search(
r' >> internal:describe=("(?:[^"\\]|\\.)*")$', self._selector
)
if match:
description = json.loads(match.group(1))
if isinstance(description, str):
return description
except (json.JSONDecodeError, ValueError):
pass
return None
def filter(
self,
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: "Locator" = None,
hasNot: "Locator" = None,
visible: bool = None,
) -> "Locator":
return Locator(
self._frame,
self._selector,
has_text=hasText,
has_not_text=hasNotText,
has=has,
has_not=hasNot,
visible=visible,
)
def or_(self, locator: "Locator") -> "Locator":
if locator._frame != self._frame:
raise Error("Locators must belong to the same frame.")
return Locator(
self._frame,
self._selector + " >> internal:or=" + json.dumps(locator._selector),
)
def and_(self, locator: "Locator") -> "Locator":
if locator._frame != self._frame:
raise Error("Locators must belong to the same frame.")
return Locator(
self._frame,
self._selector + " >> internal:and=" + json.dumps(locator._selector),
)
async def focus(self, timeout: float = None) -> None:
params = locals_to_params(locals())
return await self._frame.focus(self._selector, strict=True, **params)
async def blur(self, timeout: float = None) -> None:
await self._frame._channel.send(
"blur",
self._frame._timeout,
{
"selector": self._selector,
"strict": True,
**locals_to_params(locals()),
},
)
async def all(
self,
) -> List["Locator"]:
result = []
for index in range(await self.count()):
result.append(self.nth(index))
return result
async def count(
self,
) -> int:
return await self._frame._query_count(self._selector)
async def drag_to(
self,
target: "Locator",
force: bool = None,
noWaitAfter: bool = None,
timeout: float = None,
trial: bool = None,
sourcePosition: Position = None,
targetPosition: Position = None,
steps: int = None,
) -> None:
params = locals_to_params(locals())
del params["target"]
return await self._frame.drag_and_drop(
self._selector, target._selector, strict=True, **params
)
async def drop(
self,
payload: DropPayload,
position: Position = None,
timeout: float = None,
) -> None:
await self._frame._drop(
self._selector,
payload,
strict=True,
position=position,
timeout=timeout,
)
async def get_attribute(self, name: str, timeout: float = None) -> Optional[str]:
params = locals_to_params(locals())
return await self._frame.get_attribute(
self._selector,
strict=True,
**params,
)
async def hover(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.hover(
self._selector,
strict=True,
**params,
)
async def inner_html(self, timeout: float = None) -> str:
params = locals_to_params(locals())
return await self._frame.inner_html(
self._selector,
strict=True,
**params,
)
async def inner_text(self, timeout: float = None) -> str:
params = locals_to_params(locals())
return await self._frame.inner_text(
self._selector,
strict=True,
**params,
)
async def input_value(self, timeout: float = None) -> str:
params = locals_to_params(locals())
return await self._frame.input_value(
self._selector,
strict=True,
**params,
)
async def is_checked(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_checked(
self._selector,
strict=True,
**params,
)
async def is_disabled(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_disabled(
self._selector,
strict=True,
**params,
)
async def is_editable(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_editable(
self._selector,
strict=True,
**params,
)
async def is_enabled(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_enabled(
self._selector,
strict=True,
**params,
)
async def is_hidden(self, timeout: float = None) -> bool:
# timeout is deprecated and does nothing
return await self._frame.is_hidden(
self._selector,
strict=True,
)
async def is_visible(self, timeout: float = None) -> bool:
# timeout is deprecated and does nothing
return await self._frame.is_visible(
self._selector,
strict=True,
)
async def press(
self,
key: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.press(self._selector, strict=True, **params)
async def screenshot(
self,
timeout: float = None,
type: Literal["jpeg", "png"] = None,
path: Union[str, pathlib.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())
return await self._with_element(
lambda h, timeout: h.screenshot(
**{**params, "timeout": timeout},
),
)
async def aria_snapshot(
self,
timeout: float = None,
depth: int = None,
mode: Literal["ai", "default"] = None,
boxes: bool = None,
) -> str:
return await self._frame._channel.send(
"ariaSnapshot",
self._frame._timeout,
{
"selector": self._selector,
**locals_to_params(locals()),
},
)
async def normalize(self) -> "Locator":
result = await self._frame._channel.send(
"resolveSelector",
None,
{"selector": self._selector},
)
return Locator(self._frame, result)
async def scroll_into_view_if_needed(
self,
timeout: float = None,
) -> None:
return await self._with_element(
lambda h, timeout: h.scroll_into_view_if_needed(timeout=timeout),
timeout,
)
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,
noWaitAfter: bool = None,
force: bool = None,
) -> List[str]:
params = locals_to_params(locals())
return await self._frame.select_option(
self._selector,
strict=True,
**params,
)
async def select_text(self, force: bool = None, timeout: float = None) -> None:
params = locals_to_params(locals())
return await self._with_element(
lambda h, timeout: h.select_text(**{**params, "timeout": timeout}),
timeout,
)
async def set_input_files(
self,
files: Union[
str,
pathlib.Path,
FilePayload,
Sequence[Union[str, pathlib.Path]],
Sequence[FilePayload],
],
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.set_input_files(
self._selector,
strict=True,
**params,
)
async def tap(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.tap(
self._selector,
strict=True,
**params,
)
async def text_content(self, timeout: float = None) -> Optional[str]:
params = locals_to_params(locals())
return await self._frame.text_content(
self._selector,
strict=True,
**params,
)
async def type(
self,
text: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.type(
self._selector,
strict=True,
**params,
)
async def press_sequentially(
self,
text: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self.type(text, delay=delay, timeout=timeout)
async def uncheck(
self,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.uncheck(
self._selector,
strict=True,
**params,
)
async def all_inner_texts(
self,
) -> List[str]:
return await self._frame.eval_on_selector_all(
self._selector, "ee => ee.map(e => e.innerText)"
)
async def all_text_contents(
self,
) -> List[str]:
return await self._frame.eval_on_selector_all(
self._selector, "ee => ee.map(e => e.textContent || '')"
)
async def wait_for(
self,
timeout: float = None,
state: Literal["attached", "detached", "hidden", "visible"] = None,
) -> None:
await self._frame.wait_for_selector(
self._selector, strict=True, timeout=timeout, state=state
)
async def set_checked(
self,
checked: bool,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
if checked:
await self.check(
position=position,
timeout=timeout,
force=force,
trial=trial,
)
else:
await self.uncheck(
position=position,
timeout=timeout,
force=force,
trial=trial,
)
async def _expect(
self,
expression: str,
options: FrameExpectOptions,
title: str = None,
) -> FrameExpectResult:
return await self._frame._expect(self._selector, expression, options, title)
async def highlight(self, style: str = None) -> None:
await self._frame._highlight(self._selector, style)
async def hide_highlight(self) -> None:
await self._frame._hide_highlight(self._selector)
class FrameLocator:
def __init__(self, frame: "Frame", frame_selector: str) -> None:
self._frame = frame
self._loop = frame._loop
self._dispatcher_fiber = frame._connection._dispatcher_fiber
self._frame_selector = frame_selector
def locator(
self,
selectorOrLocator: Union["Locator", str],
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: Locator = None,
hasNot: Locator = None,
) -> Locator:
if isinstance(selectorOrLocator, str):
return Locator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator}",
has_text=hasText,
has_not_text=hasNotText,
has=has,
has_not=hasNot,
)
selectorOrLocator = to_impl(selectorOrLocator)
if selectorOrLocator._frame != self._frame:
raise ValueError("Locators must belong to the same frame.")
return Locator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator._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) -> "FrameLocator":
return FrameLocator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selector}",
)
@property
def first(self) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth=0")
@property
def last(self) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth=-1")
@property
def owner(self) -> "Locator":
return Locator(self._frame, self._frame_selector)
def nth(self, index: int) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth={index}")
def __repr__(self) -> str:
return f"<FrameLocator frame={self._frame!r} selector={self._frame_selector!r}>"
_test_id_attribute_name: str = "data-testid"
def test_id_attribute_name() -> str:
return _test_id_attribute_name
def set_test_id_attribute_name(attribute_name: str) -> None:
global _test_id_attribute_name
_test_id_attribute_name = attribute_name
def get_by_test_id_selector(
test_id_attribute_name: str, test_id: Union[str, Pattern[str]]
) -> str:
return f"internal:testid=[{test_id_attribute_name}={escape_for_attribute_selector(test_id, True)}]"
def get_by_attribute_text_selector(
attr_name: str, text: Union[str, Pattern[str]], exact: bool = None
) -> str:
return f"internal:attr=[{attr_name}={escape_for_attribute_selector(text, exact=exact)}]"
def get_by_label_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return "internal:label=" + escape_for_text_selector(text, exact=exact)
def get_by_alt_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return get_by_attribute_text_selector("alt", text, exact=exact)
def get_by_title_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return get_by_attribute_text_selector("title", text, exact=exact)
def get_by_placeholder_selector(
text: Union[str, Pattern[str]], exact: bool = None
) -> str:
return get_by_attribute_text_selector("placeholder", text, exact=exact)
def get_by_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return "internal:text=" + escape_for_text_selector(text, exact=exact)
def bool_to_js_bool(value: bool) -> str:
return "true" if value else "false"
def get_by_role_selector(
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,
) -> str:
props: List[Tuple[str, str]] = []
if checked is not None:
props.append(("checked", bool_to_js_bool(checked)))
if disabled is not None:
props.append(("disabled", bool_to_js_bool(disabled)))
if selected is not None:
props.append(("selected", bool_to_js_bool(selected)))
if expanded is not None:
props.append(("expanded", bool_to_js_bool(expanded)))
if includeHidden is not None:
props.append(("include-hidden", bool_to_js_bool(includeHidden)))
if level is not None:
props.append(("level", str(level)))
if name is not None:
props.append(
(
"name",
escape_for_attribute_selector(name, exact=exact),
)
)
if description is not None:
props.append(
(
"description",
escape_for_attribute_selector(description, exact=exact),
)
)
if pressed is not None:
props.append(("pressed", bool_to_js_bool(pressed)))
props_str = "".join([f"[{t[0]}={t[1]}]" for t in props])
return f"internal:role={role}{props_str}"
@@ -0,0 +1,31 @@
# 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 Dict, Generic, Tuple, TypeVar
K = TypeVar("K")
V = TypeVar("V")
class Map(Generic[K, V]):
def __init__(self) -> None:
self._entries: Dict[int, Tuple[K, V]] = {}
def __contains__(self, item: K) -> bool:
return id(item) in self._entries
def __setitem__(self, idx: K, value: V) -> None:
self._entries[id(idx)] = (idx, value)
def __getitem__(self, obj: K) -> V:
return self._entries[id(obj)][1]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
# 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 Dict, cast
from playwright._impl._artifact import Artifact
from playwright._impl._browser import Browser
from playwright._impl._browser_context import BrowserContext
from playwright._impl._browser_type import BrowserType
from playwright._impl._cdp_session import CDPSession
from playwright._impl._connection import ChannelOwner
from playwright._impl._debugger import Debugger
from playwright._impl._dialog import Dialog
from playwright._impl._disposable import Disposable
from playwright._impl._element_handle import ElementHandle
from playwright._impl._fetch import APIRequestContext
from playwright._impl._frame import Frame
from playwright._impl._js_handle import JSHandle
from playwright._impl._local_utils import LocalUtils
from playwright._impl._network import (
Request,
Response,
Route,
WebSocket,
WebSocketRoute,
)
from playwright._impl._page import BindingCall, Page, Worker
from playwright._impl._playwright import Playwright
from playwright._impl._stream import Stream
from playwright._impl._tracing import Tracing
from playwright._impl._writable_stream import WritableStream
class DummyObject(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
def create_remote_object(
parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> ChannelOwner:
if type == "Artifact":
return Artifact(parent, type, guid, initializer)
if type == "APIRequestContext":
return APIRequestContext(parent, type, guid, initializer)
if type == "BindingCall":
return BindingCall(parent, type, guid, initializer)
if type == "Browser":
return Browser(cast(BrowserType, parent), type, guid, initializer)
if type == "BrowserType":
return BrowserType(parent, type, guid, initializer)
if type == "BrowserContext":
return BrowserContext(parent, type, guid, initializer)
if type == "CDPSession":
return CDPSession(parent, type, guid, initializer)
if type == "Debugger":
return Debugger(parent, type, guid, initializer)
if type == "Dialog":
return Dialog(parent, type, guid, initializer)
if type == "Disposable":
return Disposable(parent, type, guid, initializer)
if type == "ElementHandle":
return ElementHandle(parent, type, guid, initializer)
if type == "Frame":
return Frame(parent, type, guid, initializer)
if type == "JSHandle":
return JSHandle(parent, type, guid, initializer)
if type == "LocalUtils":
local_utils = LocalUtils(parent, type, guid, initializer)
if not local_utils._connection._local_utils:
local_utils._connection._local_utils = local_utils
return local_utils
if type == "Page":
return Page(parent, type, guid, initializer)
if type == "Playwright":
return Playwright(parent, type, guid, initializer)
if type == "Request":
return Request(parent, type, guid, initializer)
if type == "Response":
return Response(parent, type, guid, initializer)
if type == "Route":
return Route(parent, type, guid, initializer)
if type == "Stream":
return Stream(parent, type, guid, initializer)
if type == "Tracing":
return Tracing(parent, type, guid, initializer)
if type == "WebSocket":
return WebSocket(parent, type, guid, initializer)
if type == "WebSocketRoute":
return WebSocketRoute(parent, type, guid, initializer)
if type == "Worker":
return Worker(parent, type, guid, initializer)
if type == "WritableStream":
return WritableStream(parent, type, guid, initializer)
return DummyObject(parent, type, guid, initializer)

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