generated from kgod/ai-review-template
提交
This commit is contained in:
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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__)]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
Reference in New Issue
Block a user