first commit

This commit is contained in:
jianxin committed 2025-01-24 21:03:41 +08:00
commit 8757705ea7
1488 files changed
+325179

No files matched your search

Whitespace-only changes.
@@ -0,0 +1,20 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import List
def get_hook_dirs() -> List[str]:
return [os.path.dirname(__file__)]
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyInstaller.utils.hooks import collect_data_files # type: ignore
datas = collect_data_files("playwright")
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyInstaller.utils.hooks import collect_data_files # type: ignore
datas = collect_data_files("playwright")
@@ -0,0 +1,69 @@
# 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, Optional
from playwright._impl._connection import Channel
from playwright._impl._element_handle import ElementHandle
from playwright._impl._helper import locals_to_params
def _ax_node_from_protocol(axNode: Dict) -> Dict:
result = {**axNode}
if "valueNumber" in axNode:
result["value"] = axNode["valueNumber"]
elif "valueString" in axNode:
result["value"] = axNode["valueString"]
if "checked" in axNode:
result["checked"] = (
True
if axNode.get("checked") == "checked"
else (
False if axNode.get("checked") == "unchecked" else axNode.get("checked")
)
)
if "pressed" in axNode:
result["pressed"] = (
True
if axNode.get("pressed") == "pressed"
else (
False if axNode.get("pressed") == "released" else axNode.get("pressed")
)
)
if axNode.get("children"):
result["children"] = list(map(_ax_node_from_protocol, axNode["children"]))
if "valueNumber" in result:
del result["valueNumber"]
if "valueString" in result:
del result["valueString"]
return result
class Accessibility:
def __init__(self, channel: Channel) -> None:
self._channel = channel
self._loop = channel._connection._loop
self._dispatcher_fiber = channel._connection._dispatcher_fiber
async def snapshot(
self, interestingOnly: bool = None, root: ElementHandle = None
) -> Optional[Dict]:
params = locals_to_params(locals())
if root:
params["root"] = root._channel
result = await self._channel.send("accessibilitySnapshot", params)
return _ax_node_from_protocol(result) if result else None
@@ -0,0 +1,299 @@
# 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"]
# 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"]]
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[Cookie]
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
lineNumber: int
columnNumber: int
class FilePayload(TypedDict):
name: str
mimeType: str
buffer: bytes
class RemoteAddr(TypedDict):
ipAddress: str
port: int
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]
class FrameExpectResult(TypedDict):
matches: bool
received: Any
log: List[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]
@@ -0,0 +1,59 @@
# 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")
return pathlib.Path(path)
async def save_as(self, path: Union[str, Path]) -> None:
stream = cast(Stream, from_channel(await self._channel.send("saveAsStream")))
make_dirs_for_file(path)
await stream.save_as(path)
async def failure(self) -> Optional[str]:
reason = await self._channel.send("failure")
if reason is None:
return None
return patch_error_message(reason)
async def delete(self) -> None:
await self._channel.send("delete")
async def read_info_buffer(self) -> bytes:
stream = cast(Stream, from_channel(await self._channel.send("stream")))
buffer = await stream.read_all()
return buffer
async def cancel(self) -> None:
await self._channel.send("cancel")
@@ -0,0 +1,895 @@
# 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 collections.abc
from typing import Any, List, Optional, Pattern, Sequence, Union
from urllib.parse import urljoin
from playwright._impl._api_structures import (
AriaRole,
ExpectedTextValue,
FrameExpectOptions,
)
from playwright._impl._connection import format_call_log
from playwright._impl._errors import Error
from playwright._impl._fetch import APIResponse
from playwright._impl._helper import is_textual_mime_type
from playwright._impl._locator import Locator
from playwright._impl._page import Page
from playwright._impl._str_utils import escape_regex_flags
class AssertionsBase:
def __init__(
self,
locator: Locator,
timeout: float = None,
is_not: bool = False,
message: Optional[str] = None,
) -> None:
self._actual_locator = locator
self._loop = locator._loop
self._dispatcher_fiber = locator._dispatcher_fiber
self._timeout = timeout
self._is_not = is_not
self._custom_message = message
async def _expect_impl(
self,
expression: str,
expect_options: FrameExpectOptions,
expected: Any,
message: str,
) -> None:
__tracebackhide__ = True
expect_options["isNot"] = self._is_not
if expect_options.get("timeout") is None:
expect_options["timeout"] = self._timeout or 5_000
if expect_options["isNot"]:
message = message.replace("expected to", "expected not to")
if "useInnerText" in expect_options and expect_options["useInnerText"] is None:
del expect_options["useInnerText"]
result = await self._actual_locator._expect(expression, expect_options)
if result["matches"] == self._is_not:
actual = result.get("received")
if self._custom_message:
out_message = self._custom_message
if expected is not None:
out_message += f"\nExpected value: '{expected or '<None>'}'"
else:
out_message = (
f"{message} '{expected}'" if expected is not None else f"{message}"
)
raise AssertionError(
f"{out_message}\nActual value: {actual} {format_call_log(result.get('log'))}"
)
class PageAssertions(AssertionsBase):
def __init__(
self,
page: Page,
timeout: float = None,
is_not: bool = False,
message: Optional[str] = None,
) -> None:
super().__init__(page.locator(":root"), timeout, is_not, message)
self._actual_page = page
@property
def _not(self) -> "PageAssertions":
return PageAssertions(
self._actual_page, self._timeout, not self._is_not, self._custom_message
)
async def to_have_title(
self, titleOrRegExp: Union[Pattern[str], str], timeout: float = None
) -> None:
__tracebackhide__ = True
expected_values = to_expected_text_values(
[titleOrRegExp], normalize_white_space=True
)
await self._expect_impl(
"to.have.title",
FrameExpectOptions(expectedText=expected_values, timeout=timeout),
titleOrRegExp,
"Page title expected to be",
)
async def not_to_have_title(
self, titleOrRegExp: Union[Pattern[str], str], timeout: float = None
) -> None:
__tracebackhide__ = True
await self._not.to_have_title(titleOrRegExp, timeout)
async def to_have_url(
self,
urlOrRegExp: Union[str, Pattern[str]],
timeout: float = None,
ignoreCase: bool = None,
) -> None:
__tracebackhide__ = True
base_url = self._actual_page.context._options.get("baseURL")
if isinstance(urlOrRegExp, str) and base_url:
urlOrRegExp = urljoin(base_url, urlOrRegExp)
expected_text = to_expected_text_values([urlOrRegExp], ignoreCase=ignoreCase)
await self._expect_impl(
"to.have.url",
FrameExpectOptions(expectedText=expected_text, timeout=timeout),
urlOrRegExp,
"Page URL expected to be",
)
async def not_to_have_url(
self,
urlOrRegExp: Union[Pattern[str], str],
timeout: float = None,
ignoreCase: bool = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_url(urlOrRegExp, timeout, ignoreCase)
class LocatorAssertions(AssertionsBase):
def __init__(
self,
locator: Locator,
timeout: float = None,
is_not: bool = False,
message: Optional[str] = None,
) -> None:
super().__init__(locator, timeout, is_not, message)
self._actual_locator = locator
@property
def _not(self) -> "LocatorAssertions":
return LocatorAssertions(
self._actual_locator, self._timeout, not self._is_not, self._custom_message
)
async def to_contain_text(
self,
expected: Union[
Sequence[str],
Sequence[Pattern[str]],
Sequence[Union[Pattern[str], str]],
Pattern[str],
str,
],
useInnerText: bool = None,
timeout: float = None,
ignoreCase: bool = None,
) -> None:
__tracebackhide__ = True
if isinstance(expected, collections.abc.Sequence) and not isinstance(
expected, str
):
expected_text = to_expected_text_values(
expected,
match_substring=True,
normalize_white_space=True,
ignoreCase=ignoreCase,
)
await self._expect_impl(
"to.contain.text.array",
FrameExpectOptions(
expectedText=expected_text,
useInnerText=useInnerText,
timeout=timeout,
),
expected,
"Locator expected to contain text",
)
else:
expected_text = to_expected_text_values(
[expected],
match_substring=True,
normalize_white_space=True,
ignoreCase=ignoreCase,
)
await self._expect_impl(
"to.have.text",
FrameExpectOptions(
expectedText=expected_text,
useInnerText=useInnerText,
timeout=timeout,
),
expected,
"Locator expected to contain text",
)
async def not_to_contain_text(
self,
expected: Union[
Sequence[str],
Sequence[Pattern[str]],
Sequence[Union[Pattern[str], str]],
Pattern[str],
str,
],
useInnerText: bool = None,
timeout: float = None,
ignoreCase: bool = None,
) -> None:
__tracebackhide__ = True
await self._not.to_contain_text(expected, useInnerText, timeout, ignoreCase)
async def to_have_attribute(
self,
name: str,
value: Union[str, Pattern[str]],
ignoreCase: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
expected_text = to_expected_text_values([value], ignoreCase=ignoreCase)
await self._expect_impl(
"to.have.attribute.value",
FrameExpectOptions(
expressionArg=name, expectedText=expected_text, timeout=timeout
),
value,
"Locator expected to have attribute",
)
async def not_to_have_attribute(
self,
name: str,
value: Union[str, Pattern[str]],
ignoreCase: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_attribute(
name, value, ignoreCase=ignoreCase, timeout=timeout
)
async def to_have_class(
self,
expected: Union[
Sequence[str],
Sequence[Pattern[str]],
Sequence[Union[Pattern[str], str]],
Pattern[str],
str,
],
timeout: float = None,
) -> None:
__tracebackhide__ = True
if isinstance(expected, collections.abc.Sequence) and not isinstance(
expected, str
):
expected_text = to_expected_text_values(expected)
await self._expect_impl(
"to.have.class.array",
FrameExpectOptions(expectedText=expected_text, timeout=timeout),
expected,
"Locator expected to have class",
)
else:
expected_text = to_expected_text_values([expected])
await self._expect_impl(
"to.have.class",
FrameExpectOptions(expectedText=expected_text, timeout=timeout),
expected,
"Locator expected to have class",
)
async def not_to_have_class(
self,
expected: Union[
Sequence[str],
Sequence[Pattern[str]],
Sequence[Union[Pattern[str], str]],
Pattern[str],
str,
],
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_class(expected, timeout)
async def to_have_count(
self,
count: int,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.have.count",
FrameExpectOptions(expectedNumber=count, timeout=timeout),
count,
"Locator expected to have count",
)
async def not_to_have_count(
self,
count: int,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_count(count, timeout)
async def to_have_css(
self,
name: str,
value: Union[str, Pattern[str]],
timeout: float = None,
) -> None:
__tracebackhide__ = True
expected_text = to_expected_text_values([value])
await self._expect_impl(
"to.have.css",
FrameExpectOptions(
expressionArg=name, expectedText=expected_text, timeout=timeout
),
value,
"Locator expected to have CSS",
)
async def not_to_have_css(
self,
name: str,
value: Union[str, Pattern[str]],
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_css(name, value, timeout)
async def to_have_id(
self,
id: Union[str, Pattern[str]],
timeout: float = None,
) -> None:
__tracebackhide__ = True
expected_text = to_expected_text_values([id])
await self._expect_impl(
"to.have.id",
FrameExpectOptions(expectedText=expected_text, timeout=timeout),
id,
"Locator expected to have ID",
)
async def not_to_have_id(
self,
id: Union[str, Pattern[str]],
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_id(id, timeout)
async def to_have_js_property(
self,
name: str,
value: Any,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.have.property",
FrameExpectOptions(
expressionArg=name, expectedValue=value, timeout=timeout
),
value,
"Locator expected to have JS Property",
)
async def not_to_have_js_property(
self,
name: str,
value: Any,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_js_property(name, value, timeout)
async def to_have_value(
self,
value: Union[str, Pattern[str]],
timeout: float = None,
) -> None:
__tracebackhide__ = True
expected_text = to_expected_text_values([value])
await self._expect_impl(
"to.have.value",
FrameExpectOptions(expectedText=expected_text, timeout=timeout),
value,
"Locator expected to have Value",
)
async def not_to_have_value(
self,
value: Union[str, Pattern[str]],
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_value(value, timeout)
async def to_have_values(
self,
values: Union[
Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]]
],
timeout: float = None,
) -> None:
__tracebackhide__ = True
expected_text = to_expected_text_values(values)
await self._expect_impl(
"to.have.values",
FrameExpectOptions(expectedText=expected_text, timeout=timeout),
values,
"Locator expected to have Values",
)
async def not_to_have_values(
self,
values: Union[
Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]]
],
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_values(values, timeout)
async def to_have_text(
self,
expected: Union[
Sequence[str],
Sequence[Pattern[str]],
Sequence[Union[Pattern[str], str]],
Pattern[str],
str,
],
useInnerText: bool = None,
timeout: float = None,
ignoreCase: bool = None,
) -> None:
__tracebackhide__ = True
if isinstance(expected, collections.abc.Sequence) and not isinstance(
expected, str
):
expected_text = to_expected_text_values(
expected,
normalize_white_space=True,
ignoreCase=ignoreCase,
)
await self._expect_impl(
"to.have.text.array",
FrameExpectOptions(
expectedText=expected_text,
useInnerText=useInnerText,
timeout=timeout,
),
expected,
"Locator expected to have text",
)
else:
expected_text = to_expected_text_values(
[expected], normalize_white_space=True, ignoreCase=ignoreCase
)
await self._expect_impl(
"to.have.text",
FrameExpectOptions(
expectedText=expected_text,
useInnerText=useInnerText,
timeout=timeout,
),
expected,
"Locator expected to have text",
)
async def not_to_have_text(
self,
expected: Union[
Sequence[str],
Sequence[Pattern[str]],
Sequence[Union[Pattern[str], str]],
Pattern[str],
str,
],
useInnerText: bool = None,
timeout: float = None,
ignoreCase: bool = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_text(expected, useInnerText, timeout, ignoreCase)
async def to_be_attached(
self,
attached: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
if attached is None:
attached = True
attached_string = "attached" if attached else "detached"
await self._expect_impl(
("to.be.attached" if attached else "to.be.detached"),
FrameExpectOptions(timeout=timeout),
None,
f"Locator expected to be {attached_string}",
)
async def to_be_checked(
self,
timeout: float = None,
checked: bool = None,
) -> None:
__tracebackhide__ = True
if checked is None:
checked = True
checked_string = "checked" if checked else "unchecked"
await self._expect_impl(
("to.be.checked" if checked else "to.be.unchecked"),
FrameExpectOptions(timeout=timeout),
None,
f"Locator expected to be {checked_string}",
)
async def not_to_be_attached(
self,
attached: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_attached(attached=attached, timeout=timeout)
async def not_to_be_checked(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_checked(timeout)
async def to_be_disabled(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.be.disabled",
FrameExpectOptions(timeout=timeout),
None,
"Locator expected to be disabled",
)
async def not_to_be_disabled(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_disabled(timeout)
async def to_be_editable(
self,
editable: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
if editable is None:
editable = True
editable_string = "editable" if editable else "readonly"
await self._expect_impl(
"to.be.editable" if editable else "to.be.readonly",
FrameExpectOptions(timeout=timeout),
None,
f"Locator expected to be {editable_string}",
)
async def not_to_be_editable(
self,
editable: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_editable(editable, timeout)
async def to_be_empty(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.be.empty",
FrameExpectOptions(timeout=timeout),
None,
"Locator expected to be empty",
)
async def not_to_be_empty(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_empty(timeout)
async def to_be_enabled(
self,
enabled: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
if enabled is None:
enabled = True
enabled_string = "enabled" if enabled else "disabled"
await self._expect_impl(
"to.be.enabled" if enabled else "to.be.disabled",
FrameExpectOptions(timeout=timeout),
None,
f"Locator expected to be {enabled_string}",
)
async def not_to_be_enabled(
self,
enabled: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_enabled(enabled, timeout)
async def to_be_hidden(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.be.hidden",
FrameExpectOptions(timeout=timeout),
None,
"Locator expected to be hidden",
)
async def not_to_be_hidden(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_hidden(timeout)
async def to_be_visible(
self,
visible: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
if visible is None:
visible = True
visible_string = "visible" if visible else "hidden"
await self._expect_impl(
"to.be.visible" if visible else "to.be.hidden",
FrameExpectOptions(timeout=timeout),
None,
f"Locator expected to be {visible_string}",
)
async def not_to_be_visible(
self,
visible: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_visible(visible, timeout)
async def to_be_focused(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.be.focused",
FrameExpectOptions(timeout=timeout),
None,
"Locator expected to be focused",
)
async def not_to_be_focused(
self,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_be_focused(timeout)
async def to_be_in_viewport(
self,
ratio: float = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.be.in.viewport",
FrameExpectOptions(timeout=timeout, expectedNumber=ratio),
None,
"Locator expected to be in viewport",
)
async def not_to_be_in_viewport(
self, ratio: float = None, timeout: float = None
) -> None:
__tracebackhide__ = True
await self._not.to_be_in_viewport(ratio=ratio, timeout=timeout)
async def to_have_accessible_description(
self,
description: Union[str, Pattern[str]],
ignoreCase: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
expected_values = to_expected_text_values([description], ignoreCase=ignoreCase)
await self._expect_impl(
"to.have.accessible.description",
FrameExpectOptions(expectedText=expected_values, timeout=timeout),
None,
"Locator expected to have accessible description",
)
async def not_to_have_accessible_description(
self,
name: Union[str, Pattern[str]],
ignoreCase: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_accessible_description(name, ignoreCase, timeout)
async def to_have_accessible_name(
self,
name: Union[str, Pattern[str]],
ignoreCase: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
expected_values = to_expected_text_values([name], ignoreCase=ignoreCase)
await self._expect_impl(
"to.have.accessible.name",
FrameExpectOptions(expectedText=expected_values, timeout=timeout),
None,
"Locator expected to have accessible name",
)
async def not_to_have_accessible_name(
self,
name: Union[str, Pattern[str]],
ignoreCase: bool = None,
timeout: float = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_accessible_name(name, ignoreCase, timeout)
async def to_have_role(self, role: AriaRole, timeout: float = None) -> None:
__tracebackhide__ = True
if isinstance(role, Pattern):
raise Error('"role" argument in to_have_role must be a string')
expected_values = to_expected_text_values([role])
await self._expect_impl(
"to.have.role",
FrameExpectOptions(expectedText=expected_values, timeout=timeout),
None,
"Locator expected to have accessible role",
)
async def not_to_have_role(self, role: AriaRole, timeout: float = None) -> None:
__tracebackhide__ = True
await self._not.to_have_role(role, timeout)
async def to_match_aria_snapshot(
self, expected: str, timeout: float = None
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.match.aria",
FrameExpectOptions(expectedValue=expected, timeout=timeout),
expected,
"Locator expected to match Aria snapshot",
)
async def not_to_match_aria_snapshot(
self, expected: str, timeout: float = None
) -> None:
__tracebackhide__ = True
await self._not.to_match_aria_snapshot(expected, timeout)
class APIResponseAssertions:
def __init__(
self,
response: APIResponse,
timeout: float = None,
is_not: bool = False,
message: Optional[str] = None,
) -> None:
self._loop = response._loop
self._dispatcher_fiber = response._dispatcher_fiber
self._timeout = timeout
self._is_not = is_not
self._actual = response
self._custom_message = message
@property
def _not(self) -> "APIResponseAssertions":
return APIResponseAssertions(
self._actual, self._timeout, not self._is_not, self._custom_message
)
async def to_be_ok(
self,
) -> None:
__tracebackhide__ = True
if self._is_not is not self._actual.ok:
return
message = f"Response status expected to be within [200..299] range, was '{self._actual.status}'"
if self._is_not:
message = message.replace("expected to", "expected not to")
out_message = self._custom_message or message
out_message += format_call_log(await self._actual._fetch_log())
content_type = self._actual.headers.get("content-type")
is_text_encoding = content_type and is_textual_mime_type(content_type)
text = await self._actual.text() if is_text_encoding else None
if text is not None:
out_message += f"\n Response Text:\n{text[:1000]}"
raise AssertionError(out_message)
async def not_to_be_ok(self) -> None:
__tracebackhide__ = True
await self._not.to_be_ok()
def expected_regex(
pattern: Pattern[str],
match_substring: bool,
normalize_white_space: bool,
ignoreCase: Optional[bool] = None,
) -> ExpectedTextValue:
expected = ExpectedTextValue(
regexSource=pattern.pattern,
regexFlags=escape_regex_flags(pattern),
matchSubstring=match_substring,
normalizeWhiteSpace=normalize_white_space,
ignoreCase=ignoreCase,
)
if expected["ignoreCase"] is None:
del expected["ignoreCase"]
return expected
def to_expected_text_values(
items: Union[
Sequence[Pattern[str]], Sequence[str], Sequence[Union[str, Pattern[str]]]
],
match_substring: bool = False,
normalize_white_space: bool = False,
ignoreCase: Optional[bool] = None,
) -> Sequence[ExpectedTextValue]:
out: List[ExpectedTextValue] = []
assert isinstance(items, list)
for item in items:
if isinstance(item, str):
o = ExpectedTextValue(
string=item,
matchSubstring=match_substring,
normalizeWhiteSpace=normalize_white_space,
ignoreCase=ignoreCase,
)
if o["ignoreCase"] is None:
del o["ignoreCase"]
out.append(o)
elif isinstance(item, Pattern):
out.append(
expected_regex(item, match_substring, normalize_white_space, ignoreCase)
)
else:
raise Error("value must be a string or regular expression")
return out
@@ -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: Type[BaseException],
exc_val: BaseException,
traceback: TracebackType,
) -> None:
await self.close()
async def close(self) -> None: ...
@@ -0,0 +1,263 @@
# 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
from pathlib import Path
from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast
from playwright._impl._api_structures import (
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,
ForcedColors,
HarContentPolicy,
HarMode,
ReducedMotion,
ServiceWorkersPolicy,
async_readfile,
locals_to_params,
make_dirs_for_file,
prepare_record_har_options,
)
from playwright._impl._network import serialize_headers, to_client_certificates_protocol
from playwright._impl._page import Page
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._browser_type import BrowserType
class Browser(ChannelOwner):
Events = SimpleNamespace(
Disconnected="disconnected",
)
def __init__(
self, parent: "BrowserType", type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._browser_type = parent
self._is_connected = True
self._should_close_connection_on_close = False
self._cr_tracing_path: Optional[str] = None
self._contexts: List[BrowserContext] = []
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 _on_close(self) -> None:
self._is_connected = False
self.emit(Browser.Events.Disconnected, self)
@property
def contexts(self) -> List[BrowserContext]:
return self._contexts.copy()
@property
def browser_type(self) -> "BrowserType":
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,
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())
await prepare_browser_context_params(params)
channel = await self._channel.send("newContext", params)
context = cast(BrowserContext, from_channel(channel))
self._browser_type._did_create_context(context, params, {})
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,
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)
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", {"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"))
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", params)
async def stop_tracing(self) -> bytes:
artifact = cast(Artifact, from_channel(await self._channel.send("stopTracing")))
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
async def prepare_browser_context_params(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 "recordHarPath" in params:
params["recordHar"] = prepare_record_har_options(params)
del params["recordHarPath"]
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 "acceptDownloads" in params:
params["acceptDownloads"] = "accept" if params["acceptDownloads"] else "deny"
if "clientCertificates" in params:
params["clientCertificates"] = await to_client_certificates_protocol(
params["clientCertificates"]
)
@@ -0,0 +1,738 @@
# 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,
)
from playwright._impl._artifact import Artifact
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._dialog import Dialog
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,
HarRecordingMetadata,
RouteFromHarNotFoundPolicy,
RouteHandler,
RouteHandlerCallback,
TimeoutSettings,
URLMatch,
WebSocketRouteHandlerCallback,
async_readfile,
async_writefile,
locals_to_params,
parse_error,
prepare_record_har_options,
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(
BackgroundPage="backgroundpage",
Close="close",
Console="console",
Dialog="dialog",
Page="page",
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)
# circular import workaround:
self._browser: Optional["Browser"] = None
if parent.__class__.__name__ == "Browser":
self._browser = cast("Browser", parent)
self._browser._contexts.append(self)
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] = {}
self._background_pages: Set[Page] = set()
self._service_workers: Set[Worker] = set()
self._tracing = cast(Tracing, from_channel(initializer["tracing"]))
self._har_recorders: Dict[str, HarRecordingMetadata] = {}
self._request: APIRequestContext = from_channel(initializer["requestContext"])
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(
"backgroundPage",
lambda params: self._on_background_page(from_channel(params["page"])),
)
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"]),
),
)
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._close_was_called = 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._close_was_called:
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)
self._channel.send_no_reply(
"setDefaultNavigationTimeoutNoReply",
{} if timeout is None else {"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)
self._channel.send_no_reply(
"setDefaultTimeoutNoReply", {} if timeout is None else {"timeout": timeout}
)
@property
def pages(self) -> List[Page]:
return self._pages.copy()
@property
def browser(self) -> Optional["Browser"]:
return self._browser
def _set_options(self, context_options: Dict, browser_options: Dict) -> None:
self._options = context_options
if self._options.get("recordHar"):
self._har_recorders[""] = {
"path": self._options["recordHar"]["path"],
"content": self._options["recordHar"].get("content"),
}
self._tracing._traces_dir = browser_options.get("tracesDir")
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"))
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", dict(urls=urls))
async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None:
await self._channel.send("addCookies", 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",
{
"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", locals_to_params(locals()))
async def clear_permissions(self) -> None:
await self._channel.send("clearPermissions")
async def set_geolocation(self, geolocation: Geolocation = None) -> None:
await self._channel.send("setGeolocation", locals_to_params(locals()))
async def set_extra_http_headers(self, headers: Dict[str, str]) -> None:
await self._channel.send(
"setExtraHTTPHeaders", dict(headers=serialize_headers(headers))
)
async def set_offline(self, offline: bool) -> None:
await self._channel.send("setOffline", dict(offline=offline))
async def add_init_script(
self, script: str = None, path: Union[str, Path] = None
) -> None:
if path:
script = (await async_readfile(path)).decode()
if not isinstance(script, str):
raise Error("Either path or script parameter must be specified")
await self._channel.send("addInitScript", dict(source=script))
async def expose_binding(
self, name: str, callback: Callable, handle: bool = None
) -> None:
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
await self._channel.send(
"exposeBinding", dict(name=name, needsHandle=handle or False)
)
async def expose_function(self, name: str, callback: Callable) -> None:
await self.expose_binding(name, lambda source, *args: callback(*args))
async def route(
self, url: URLMatch, handler: RouteHandlerCallback, times: int = None
) -> None:
self._routes.insert(
0,
RouteHandler(
self._options.get("baseURL"),
url,
handler,
True if self._dispatcher_fiber else False,
times,
),
)
await self._update_interception_patterns()
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
await self._update_interception_patterns()
if behavior is None or behavior == "default":
return
await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore
async def route_web_socket(
self, url: URLMatch, handler: WebSocketRouteHandlerCallback
) -> None:
self._web_socket_routes.insert(
0,
WebSocketRouteHandler(self._options.get("baseURL"), 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 _record_into_har(
self,
har: Union[Path, str],
page: Optional[Page] = None,
url: Union[Pattern[str], str] = None,
update_content: HarContentPolicy = None,
update_mode: HarMode = None,
) -> None:
params: Dict[str, Any] = {
"options": prepare_record_har_options(
{
"recordHarPath": har,
"recordHarContent": update_content or "attach",
"recordHarMode": update_mode or "minimal",
"recordHarUrlFilter": url,
}
)
}
if page:
params["page"] = page._channel
har_id = await self._channel.send("harStart", params)
self._har_recorders[har_id] = {
"path": str(har),
"content": update_content or "attach",
}
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._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", {"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", {"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:
if self._browser:
self._browser._contexts.remove(self)
self._dispose_har_routers()
self._tracing._reset_stack_counter()
self.emit(BrowserContext.Events.Close, self)
async def close(self, reason: str = None) -> None:
if self._close_was_called:
return
self._close_reason = reason
self._close_was_called = True
await self._channel._connection.wrap_api_call(
lambda: self.request.dispose(reason=reason), True
)
async def _inner_close() -> None:
for har_id, params in self._har_recorders.items():
har = cast(
Artifact,
from_channel(
await self._channel.send("harExport", {"harId": har_id})
),
)
# Server side will compress artifact if content is attach or if file is .zip.
is_compressed = params.get("content") == "attach" or params[
"path"
].endswith(".zip")
need_compressed = params["path"].endswith(".zip")
if is_compressed and not need_compressed:
tmp_path = params["path"] + ".tmp"
await har.save_as(tmp_path)
await self._connection.local_utils.har_unzip(
zipFile=tmp_path, harFile=params["path"]
)
else:
await har.save_as(params["path"])
await har.delete()
await self._channel._connection.wrap_api_call(_inner_close, True)
await self._channel.send("close", {"reason": reason})
await self._closed_future
async def storage_state(self, path: Union[str, Path] = None) -> StorageState:
result = await self._channel.send_return_as_dict("storageState")
if path:
await async_writefile(path, json.dumps(result))
return result
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_background_page(self, page: Page) -> None:
self._background_pages.add(page)
self.emit(BrowserContext.Events.BackgroundPage, page)
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)
self.emit(BrowserContext.Events.Console, message)
page = message.page
if page:
page.emit(Page.Events.Console, message)
def _on_dialog(self, dialog: Dialog) -> None:
has_listeners = self.emit(BrowserContext.Events.Dialog, dialog)
page = dialog.page
if page:
has_listeners = page.emit(Page.Events.Dialog, dialog) or has_listeners
if not has_listeners:
# Although we do similar handling on the server side, we still need this logic
# on the client side due to a possible race condition between two async calls:
# a) removing "dialog" listener subscription (client->server)
# b) actual "dialog" event (server->client)
if dialog.type == "beforeunload":
asyncio.create_task(dialog.accept())
else:
asyncio.create_task(dialog.dismiss())
def _on_page_error(self, error: Error, page: Optional[Page]) -> None:
self.emit(BrowserContext.Events.WebError, WebError(self._loop, page, error))
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 list(self._background_pages)
@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", params))
@property
def tracing(self) -> Tracing:
return self._tracing
@property
def request(self) -> "APIRequestContext":
return self._request
@property
def clock(self) -> Clock:
return self._clock
@@ -0,0 +1,292 @@
# 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 pathlib
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast
from playwright._impl._api_structures import (
ClientCertificate,
Geolocation,
HttpCredentials,
ProxySettings,
ViewportSize,
)
from playwright._impl._browser import Browser, prepare_browser_context_params
from playwright._impl._browser_context import BrowserContext
from playwright._impl._connection import (
ChannelOwner,
Connection,
from_channel,
from_nullable_channel,
)
from playwright._impl._errors import Error
from playwright._impl._helper import (
ColorScheme,
Env,
ForcedColors,
HarContentPolicy,
HarMode,
ReducedMotion,
ServiceWorkersPolicy,
locals_to_params,
)
from playwright._impl._json_pipe import JsonPipeTransport
from playwright._impl._network import serialize_headers
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,
devtools: bool = None,
proxy: ProxySettings = None,
downloadsPath: Union[str, Path] = None,
slowMo: float = None,
tracesDir: 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", params))
)
self._did_launch_browser(browser)
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,
devtools: 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,
acceptDownloads: bool = None,
tracesDir: 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 = str(Path(userDataDir)) if userDataDir else ""
params = locals_to_params(locals())
await prepare_browser_context_params(params)
normalize_launch_params(params)
context = cast(
BrowserContext,
from_channel(await self._channel.send("launchPersistentContext", params)),
)
self._did_create_context(context, params, params)
return context
async def connect_over_cdp(
self,
endpointURL: str,
timeout: float = None,
slowMo: float = None,
headers: Dict[str, str] = 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", params)
browser = cast(Browser, from_channel(response["browser"]))
self._did_launch_browser(browser)
default_context = cast(
Optional[BrowserContext],
from_nullable_channel(response.get("defaultContext")),
)
if default_context:
self._did_create_context(default_context, {}, {})
return browser
async def connect(
self,
wsEndpoint: str,
timeout: float = None,
slowMo: float = None,
headers: Dict[str, str] = None,
exposeNetwork: str = None,
) -> Browser:
if timeout is None:
timeout = 30000
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",
{
"wsEndpoint": wsEndpoint,
"headers": headers,
"slowMo": slowMo,
"timeout": timeout,
"exposeNetwork": exposeNetwork,
},
)
)["pipe"]
transport = JsonPipeTransport(self._connection._loop, pipe_channel)
connection = Connection(
self._connection._dispatcher_fiber,
self._connection._object_factory,
transport,
self._connection._loop,
local_utils=self._connection.local_utils,
)
connection.mark_as_remote()
browser = None
def handle_transport_close(reason: Optional[str]) -> None:
if browser:
for context in browser.contexts:
for page in context.pages:
page._on_close()
context._on_close()
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, 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))
self._did_launch_browser(browser)
browser._should_close_connection_on_close = True
return browser
def _did_create_context(
self, context: BrowserContext, context_options: Dict, browser_options: Dict
) -> None:
context._set_options(context_options, browser_options)
def _did_launch_browser(self, browser: Browser) -> None:
browser._browser_type = self
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"]
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"]))
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Dict
from playwright._impl._connection import ChannelOwner
from playwright._impl._helper import locals_to_params
class CDPSession(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._channel.on("event", lambda params: self._on_event(params))
def _on_event(self, params: Any) -> None:
self.emit(params["method"], params.get("params"))
async def send(self, method: str, params: Dict = None) -> Dict:
return await self._channel.send("send", locals_to_params(locals()))
async def detach(self) -> None:
await self._channel.send("detach")
@@ -0,0 +1,86 @@
# 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", 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", parse_ticks(ticks)
)
async def pause_at(
self,
time: Union[float, str, datetime.datetime],
) -> None:
await self._browser_context._channel.send("clockPauseAt", parse_time(time))
async def resume(
self,
) -> None:
await self._browser_context._channel.send("clockResume")
async def run_for(
self,
ticks: Union[int, str],
) -> None:
await self._browser_context._channel.send("clockRunFor", parse_ticks(ticks))
async def set_fixed_time(
self,
time: Union[float, str, datetime.datetime],
) -> None:
await self._browser_context._channel.send("clockSetFixedTime", parse_time(time))
async def set_system_time(
self,
time: Union[float, str, datetime.datetime],
) -> None:
await self._browser_context._channel.send(
"clockSetSystemTime", 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,622 @@
# 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
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))
self._is_internal_type = False
async def send(self, method: str, params: Dict = None) -> Any:
return await self._connection.wrap_api_call(
lambda: self._inner_send(method, params, False),
self._is_internal_type,
)
async def send_return_as_dict(self, method: str, params: Dict = None) -> Any:
return await self._connection.wrap_api_call(
lambda: self._inner_send(method, params, True),
self._is_internal_type,
)
def send_no_reply(self, method: str, params: Dict = 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, {} if params is None else params, True
)
)
async def _inner_send(
self, method: str, params: Optional[Dict], return_as_dict: bool
) -> Any:
if params is None:
params = {}
if self._connection._error:
error = self._connection._error
self._connection._error = None
raise error
callback = self._connection._send_message_to_server(
self._object, method, _filter_none(params)
)
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]
def mark_as_internal_type(self) -> None:
self._is_internal_type = True
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", {"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) -> None:
self.stack_trace: traceback.StackSummary
self.no_reply: bool
self.future = loop.create_future()
# 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",
{
"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)
task = asyncio.current_task(self._loop)
callback.stack_trace = cast(
traceback.StackSummary,
getattr(task, "__pw_stack_trace__", traceback.extract_stack()),
)
callback.no_reply = no_reply
self._callbacks[id] = callback
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
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 frames
and object._guid != "localUtils"
):
self.local_utils.add_stack_to_tracing_no_reply(id, frames)
self._transport.send(message)
self._callbacks[id] = callback
return callback
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(
traceback.format_list(callback.stack_trace)[-10:]
)
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
) -> Any:
if self._api_zone.get():
return await cb()
task = asyncio.current_task(self._loop)
st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack())
parsed_st = _extract_stack_trace_information_from_stack(st, is_internal)
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
) -> Any:
if self._api_zone.get():
return cb()
task = asyncio.current_task(self._loop)
st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack())
parsed_st = _extract_stack_trace_information_from_stack(st, is_internal)
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]
def _extract_stack_trace_information_from_stack(
st: List[inspect.FrameInfo], is_internal: bool
) -> 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,
}
def _filter_none(d: Mapping) -> Dict:
return {k: v for k, v in d.items() if v is not None}
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,59 @@
# 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, Optional
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
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"))
def __repr__(self) -> str:
return f"<ConsoleMessage type={self.type} text={self.text}>"
def __str__(self) -> str:
return self.text
@property
def type(self) -> str:
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:
return self._event["location"]
@property
def page(self) -> Optional["Page"]:
return self._page
@@ -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 typing import TYPE_CHECKING, Dict, Optional
from playwright._impl._connection import ChannelOwner, from_nullable_channel
from playwright._impl._helper import locals_to_params
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._page import Page
class Dialog(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._page: Optional["Page"] = from_nullable_channel(initializer.get("page"))
def __repr__(self) -> str:
return f"<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", locals_to_params(locals()))
async def dismiss(self) -> None:
await self._channel.send("dismiss")
@@ -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,412 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Sequence,
Union,
cast,
)
from playwright._impl._api_structures import FilePayload, FloatRect, Position
from playwright._impl._connection import ChannelOwner, from_nullable_channel
from playwright._impl._helper import (
Error,
KeyboardModifier,
MouseButton,
async_writefile,
locals_to_params,
make_dirs_for_file,
)
from playwright._impl._js_handle import (
JSHandle,
Serializable,
parse_result,
serialize_argument,
)
from playwright._impl._set_input_files_helpers import convert_input_files
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._frame import Frame
from playwright._impl._locator import Locator
class ElementHandle(JSHandle):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
async def _createSelectorForTest(self, name: str) -> Optional[str]:
return await self._channel.send("createSelectorForTest", 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"))
async def content_frame(self) -> Optional["Frame"]:
return from_nullable_channel(await self._channel.send("contentFrame"))
async def get_attribute(self, name: str) -> Optional[str]:
return await self._channel.send("getAttribute", dict(name=name))
async def text_content(self) -> Optional[str]:
return await self._channel.send("textContent")
async def inner_text(self) -> str:
return await self._channel.send("innerText")
async def inner_html(self) -> str:
return await self._channel.send("innerHTML")
async def is_checked(self) -> bool:
return await self._channel.send("isChecked")
async def is_disabled(self) -> bool:
return await self._channel.send("isDisabled")
async def is_editable(self) -> bool:
return await self._channel.send("isEditable")
async def is_enabled(self) -> bool:
return await self._channel.send("isEnabled")
async def is_hidden(self) -> bool:
return await self._channel.send("isHidden")
async def is_visible(self) -> bool:
return await self._channel.send("isVisible")
async def dispatch_event(self, type: str, eventInit: Dict = None) -> None:
await self._channel.send(
"dispatchEvent", dict(type=type, eventInit=serialize_argument(eventInit))
)
async def scroll_into_view_if_needed(self, timeout: float = None) -> None:
await self._channel.send("scrollIntoViewIfNeeded", 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", 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,
) -> None:
await self._channel.send("click", 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,
) -> None:
await self._channel.send("dblclick", 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", 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", 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", locals_to_params(locals()))
async def select_text(self, force: bool = None, timeout: float = None) -> None:
await self._channel.send("selectText", locals_to_params(locals()))
async def input_value(self, timeout: float = None) -> str:
return await self._channel.send("inputValue", 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",
{
"timeout": timeout,
**converted,
},
)
async def focus(self) -> None:
await self._channel.send("focus")
async def type(
self,
text: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self._channel.send("type", 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", 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", 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", locals_to_params(locals()))
async def bounding_box(self) -> Optional[FloatRect]:
return await self._channel.send("boundingBox")
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:
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", 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", 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", 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",
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",
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", 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", 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)
@@ -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,537 @@
# 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 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._helper import (
Error,
NameValue,
TargetClosedError,
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 = Dict[str, Union[bool, float, str]]
DataType = Union[Any, bytes, str]
MultipartType = Dict[str, Union[bytes, bool, float, str, FilePayload]]
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,
) -> "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", params)),
)
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
async def dispose(self, reason: str = None) -> None:
self._close_reason = reason
try:
await self._channel.send("dispose", {"reason": reason})
except Error as e:
if is_target_closed_error(e):
return
raise e
self._tracing._reset_stack_counter()
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: Dict[str, Union[bytes, bool, float, str, FilePayload]] = 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: Dict[str, Union[bytes, bool, float, str, FilePayload]] = 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: Dict[str, Union[bytes, bool, float, str, FilePayload]] = 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: Dict[str, Union[bytes, bool, float, str, FilePayload]] = 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: Dict[str, Union[bytes, bool, float, str, FilePayload]] = 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:
form_data = object_to_array(form)
elif multipart:
multipart_data = []
# 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",
{
"url": url,
"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,
"timeout": timeout,
"failOnStatusCode": failOnStatusCode,
"ignoreHTTPSErrors": ignoreHTTPSErrors,
"maxRedirects": maxRedirects,
"maxRetries": maxRetries,
},
)
return APIResponse(self, response)
async def storage_state(
self, path: Union[pathlib.Path, str] = None
) -> StorageState:
result = await self._channel.send_return_as_dict("storageState")
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(),
)
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._channel.send_return_as_dict(
"fetchResponseBody",
{
"fetchUid": self._fetch_uid,
},
)
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",
{
"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",
{
"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,807 @@
# 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,
Optional,
Pattern,
Sequence,
Set,
Union,
cast,
)
from pyee import EventEmitter
from playwright._impl._api_structures import AriaRole, FilePayload, Position
from playwright._impl._connection import (
ChannelOwner,
from_channel,
from_nullable_channel,
)
from playwright._impl._element_handle import ElementHandle, convert_select_option_values
from playwright._impl._errors import Error
from playwright._impl._event_context_manager import EventContextManagerImpl
from playwright._impl._helper import (
DocumentLoadState,
FrameNavigatedEvent,
KeyboardModifier,
Literal,
MouseButton,
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,
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)
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)
async def _query_count(self, selector: str) -> int:
return await self._channel.send("queryCount", {"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", 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,
)
if timeout is None:
timeout = self._page._timeout_settings.navigation_timeout()
waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.")
return waiter
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._options.get("baseURL"),
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._options.get("baseURL"), 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()
async def frame_element(self) -> ElementHandle:
return from_channel(await self._channel.send("frameElement"))
async def evaluate(self, expression: str, arg: Serializable = None) -> Any:
return parse_result(
await self._channel.send(
"evaluateExpression",
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",
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", locals_to_params(locals()))
)
async def query_selector_all(self, selector: str) -> List[ElementHandle]:
return list(
map(
from_channel,
await self._channel.send("querySelectorAll", 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", locals_to_params(locals()))
)
async def is_checked(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send("isChecked", locals_to_params(locals()))
async def is_disabled(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send("isDisabled", locals_to_params(locals()))
async def is_editable(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send("isEditable", locals_to_params(locals()))
async def is_enabled(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send("isEnabled", locals_to_params(locals()))
async def is_hidden(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send("isHidden", locals_to_params(locals()))
async def is_visible(
self, selector: str, strict: bool = None, timeout: float = None
) -> bool:
return await self._channel.send("isVisible", 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",
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",
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",
dict(
selector=selector,
expression=expression,
arg=serialize_argument(arg),
),
)
)
async def content(self) -> str:
return await self._channel.send("content")
async def set_content(
self,
html: str,
timeout: float = None,
waitUntil: DocumentLoadState = None,
) -> None:
await self._channel.send("setContent", 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", 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", 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._channel.send("click", 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", locals_to_params(locals()))
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", 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._channel.send("fill", 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,
) -> "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,
)
)
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", 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", locals_to_params(locals()))
async def inner_text(
self, selector: str, strict: bool = None, timeout: float = None
) -> str:
return await self._channel.send("innerText", locals_to_params(locals()))
async def inner_html(
self, selector: str, strict: bool = None, timeout: float = None
) -> str:
return await self._channel.send("innerHTML", 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", 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", 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,
) -> None:
await self._channel.send("dragAndDrop", 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", params)
async def input_value(
self,
selector: str,
strict: bool = None,
timeout: float = None,
) -> str:
return await self._channel.send("inputValue", 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",
{
"selector": selector,
"strict": strict,
"timeout": timeout,
**converted,
},
)
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", 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", 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", 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", locals_to_params(locals()))
async def wait_for_timeout(self, timeout: float) -> None:
await self._channel.send("waitForTimeout", locals_to_params(locals()))
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", params))
async def title(self) -> str:
return await self._channel.send("title")
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) -> None:
await self._channel.send("highlight", {"selector": selector})
@@ -0,0 +1,68 @@
# 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 re
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"}
def glob_to_regex(glob: str) -> "re.Pattern[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 == "*":
before_deep = 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
after_deep = glob[i + 1] if i + 1 < len(glob) else None
is_deep = (
star_count > 1
and (before_deep == "/" or before_deep is None)
and (after_deep == "/" or after_deep is None)
)
if is_deep:
tokens.append("((?:[^/]*(?:/|$))*)")
i += 1
else:
tokens.append("([^/]*)")
else:
if c == "?":
tokens.append(".")
elif c == "[":
tokens.append("[")
elif c == "]":
tokens.append("]")
elif c == "{":
in_group = True
tokens.append("(")
elif c == "}":
in_group = False
tokens.append(")")
elif c == "," and in_group:
tokens.append("|")
else:
tokens.append("\\" + c if c in escaped_chars else c)
i += 1
tokens.append("$")
return re.compile("".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", {"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", {"harId": self._har_id})
)
@@ -0,0 +1,446 @@
# 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 math
import os
import re
import time
import traceback
from pathlib import Path
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Pattern,
Set,
TypedDict,
TypeVar,
Union,
cast,
)
from urllib.parse import urljoin
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
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"], bool]]
URLMatchResponse = Union[str, Pattern[str], Callable[["Response"], 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"]
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]
) -> bool:
if not match:
return True
if isinstance(match, str) and match[0] != "*":
# Allow http(s) baseURL to match ws(s) urls.
if (
base_url
and re.match(r"^https?://", base_url)
and re.match(r"^wss?://", url_string)
):
base_url = re.sub(r"^http", "ws", base_url)
if base_url:
match = urljoin(base_url, match)
if isinstance(match, str):
match = glob_to_regex(match)
if isinstance(match, Pattern):
return bool(match.search(url_string))
return match(url_string)
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]
class TimeoutSettings:
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 30000
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) -> float:
if self._default_navigation_timeout is not None:
return self._default_navigation_timeout
if self._parent:
return self._parent.navigation_timeout()
return 30000
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))
@@ -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,94 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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", locals_to_params(locals()))
async def up(self, key: str) -> None:
await self._channel.send("keyboardUp", locals_to_params(locals()))
async def insert_text(self, text: str) -> None:
await self._channel.send("keyboardInsertText", locals_to_params(locals()))
async def type(self, text: str, delay: float = None) -> None:
await self._channel.send("keyboardType", locals_to_params(locals()))
async def press(self, key: str, delay: float = None) -> None:
await self._channel.send("keyboardPress", 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", locals_to_params(locals()))
async def down(
self,
button: MouseButton = None,
clickCount: int = None,
) -> None:
await self._channel.send("mouseDown", locals_to_params(locals()))
async def up(
self,
button: MouseButton = None,
clickCount: int = None,
) -> None:
await self._channel.send("mouseUp", locals_to_params(locals()))
async def click(
self,
x: float,
y: float,
delay: float = None,
button: MouseButton = None,
clickCount: int = None,
) -> None:
await self._channel.send("mouseClick", locals_to_params(locals()))
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)
async def wheel(self, deltaX: float, deltaY: float) -> None:
await self._channel.send("mouseWheel", 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", locals_to_params(locals()))
@@ -0,0 +1,271 @@
# 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 collections.abc
import datetime
import math
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",
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",
dict(
expression=expression,
arg=serialize_argument(arg),
),
)
)
async def get_property(self, propertyName: str) -> "JSHandle":
return from_channel(
await self._channel.send("getProperty", 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")
}
def as_element(self) -> Optional["ElementHandle"]:
return None
async def dispose(self) -> None:
try:
await self._channel.send("dispose")
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"))
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"]
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", {})
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", {"message": message})
@@ -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 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._channel.mark_as_internal_type()
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", params)
async def har_open(self, file: str) -> None:
params = locals_to_params(locals())
await self._channel.send("harOpen", 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", params),
)
async def har_close(self, harId: str) -> None:
params = locals_to_params(locals())
await self._channel.send("harClose", params)
async def har_unzip(self, zipFile: str, harFile: str) -> None:
params = locals_to_params(locals())
await self._channel.send("harUnzip", params)
async def tracing_started(self, tracesDir: Optional[str], traceName: str) -> str:
params = locals_to_params(locals())
return await self._channel.send("tracingStarted", params)
async def trace_discarded(self, stacks_id: str) -> None:
return await self._channel.send("traceDiscarded", {"stacksId": stacks_id})
def add_stack_to_tracing_no_reply(self, id: int, frames: List[StackFrame]) -> None:
self._channel.send_no_reply(
"addStackToTracingNoReply",
{
"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,930 @@
# 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
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
List,
Literal,
Optional,
Pattern,
Sequence,
Tuple,
TypeVar,
Union,
)
from playwright._impl._api_structures import (
AriaRole,
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, parse_value, serialize_argument
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,
) -> 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)
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.page._timeout_settings.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,
) -> 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,
) -> 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:
await self.fill("", timeout=timeout, force=force)
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,
) -> "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,
)
)
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 filter(
self,
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: "Locator" = None,
hasNot: "Locator" = None,
) -> "Locator":
return Locator(
self._frame,
self._selector,
has_text=hasText,
has_not_text=hasNotText,
has=has,
has_not=hasNot,
)
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",
{
"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,
) -> 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 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_editable(
self._selector,
strict=True,
**params,
)
async def is_hidden(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_hidden(
self._selector,
strict=True,
**params,
)
async def is_visible(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_visible(
self._selector,
strict=True,
**params,
)
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) -> str:
return await self._frame._channel.send(
"ariaSnapshot",
{
"selector": self._selector,
**locals_to_params(locals()),
},
)
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
) -> FrameExpectResult:
if "expectedValue" in options:
options["expectedValue"] = serialize_argument(options["expectedValue"])
result = await self._frame._channel.send_return_as_dict(
"expect",
{
"selector": self._selector,
"expression": expression,
**options,
},
)
if result.get("received"):
result["received"] = parse_value(result["received"])
return result
async def highlight(self) -> None:
await self._frame._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,
) -> "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,
)
)
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,
) -> 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 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]
@@ -0,0 +1,990 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import base64
import inspect
import json
import json as json_utils
import mimetypes
import re
from collections import defaultdict
from pathlib import Path
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
Callable,
Coroutine,
Dict,
List,
Optional,
TypedDict,
Union,
cast,
)
from urllib import parse
from playwright._impl._api_structures import (
ClientCertificate,
Headers,
HeadersArray,
RemoteAddr,
RequestSizes,
ResourceTiming,
SecurityDetails,
)
from playwright._impl._connection import (
ChannelOwner,
from_channel,
from_nullable_channel,
)
from playwright._impl._errors import Error
from playwright._impl._event_context_manager import EventContextManagerImpl
from playwright._impl._helper import (
URLMatch,
WebSocketRouteHandlerCallback,
async_readfile,
locals_to_params,
url_matches,
)
from playwright._impl._str_utils import escape_regex_flags
from playwright._impl._waiter import Waiter
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._browser_context import BrowserContext
from playwright._impl._fetch import APIResponse
from playwright._impl._frame import Frame
from playwright._impl._page import Page
class FallbackOverrideParameters(TypedDict, total=False):
url: Optional[str]
method: Optional[str]
headers: Optional[Dict[str, str]]
postData: Optional[Union[str, bytes]]
class SerializedFallbackOverrides:
def __init__(self) -> None:
self.url: Optional[str] = None
self.method: Optional[str] = None
self.headers: Optional[Dict[str, str]] = None
self.post_data_buffer: Optional[bytes] = None
def serialize_headers(headers: Dict[str, str]) -> HeadersArray:
return [
{"name": name, "value": value}
for name, value in headers.items()
if value is not None
]
async def to_client_certificates_protocol(
clientCertificates: Optional[List[ClientCertificate]],
) -> Optional[List[Dict[str, str]]]:
if not clientCertificates:
return None
out = []
for clientCertificate in clientCertificates:
out_record = {
"origin": clientCertificate["origin"],
}
if passphrase := clientCertificate.get("passphrase"):
out_record["passphrase"] = passphrase
if pfx := clientCertificate.get("pfx"):
out_record["pfx"] = base64.b64encode(pfx).decode()
if pfx_path := clientCertificate.get("pfxPath"):
out_record["pfx"] = base64.b64encode(
await async_readfile(pfx_path)
).decode()
if cert := clientCertificate.get("cert"):
out_record["cert"] = base64.b64encode(cert).decode()
if cert_path := clientCertificate.get("certPath"):
out_record["cert"] = base64.b64encode(
await async_readfile(cert_path)
).decode()
if key := clientCertificate.get("key"):
out_record["key"] = base64.b64encode(key).decode()
if key_path := clientCertificate.get("keyPath"):
out_record["key"] = base64.b64encode(
await async_readfile(key_path)
).decode()
out.append(out_record)
return out
class Request(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._redirected_from: Optional["Request"] = from_nullable_channel(
initializer.get("redirectedFrom")
)
self._redirected_to: Optional["Request"] = None
if self._redirected_from:
self._redirected_from._redirected_to = self
self._failure_text: Optional[str] = None
self._timing: ResourceTiming = {
"startTime": 0,
"domainLookupStart": -1,
"domainLookupEnd": -1,
"connectStart": -1,
"secureConnectionStart": -1,
"connectEnd": -1,
"requestStart": -1,
"responseStart": -1,
"responseEnd": -1,
}
self._provisional_headers = RawHeaders(self._initializer["headers"])
self._all_headers_future: Optional[asyncio.Future[RawHeaders]] = None
self._fallback_overrides: SerializedFallbackOverrides = (
SerializedFallbackOverrides()
)
def __repr__(self) -> str:
return f"<Request url={self.url!r} method={self.method!r}>"
def _apply_fallback_overrides(self, overrides: FallbackOverrideParameters) -> None:
self._fallback_overrides.url = overrides.get(
"url", self._fallback_overrides.url
)
self._fallback_overrides.method = overrides.get(
"method", self._fallback_overrides.method
)
self._fallback_overrides.headers = overrides.get(
"headers", self._fallback_overrides.headers
)
post_data = overrides.get("postData")
if isinstance(post_data, str):
self._fallback_overrides.post_data_buffer = post_data.encode()
elif isinstance(post_data, bytes):
self._fallback_overrides.post_data_buffer = post_data
elif post_data is not None:
self._fallback_overrides.post_data_buffer = json.dumps(post_data).encode()
@property
def url(self) -> str:
return cast(str, self._fallback_overrides.url or self._initializer["url"])
@property
def resource_type(self) -> str:
return self._initializer["resourceType"]
@property
def method(self) -> str:
return cast(str, self._fallback_overrides.method or self._initializer["method"])
async def sizes(self) -> RequestSizes:
response = await self.response()
if not response:
raise Error("Unable to fetch sizes for failed request")
return await response._channel.send("sizes")
@property
def post_data(self) -> Optional[str]:
data = self._fallback_overrides.post_data_buffer
if data:
return data.decode()
base64_post_data = self._initializer.get("postData")
if base64_post_data is not None:
return base64.b64decode(base64_post_data).decode()
return None
@property
def post_data_json(self) -> Optional[Any]:
post_data = self.post_data
if not post_data:
return None
content_type = self.headers["content-type"]
if "application/x-www-form-urlencoded" in content_type:
return dict(parse.parse_qsl(post_data))
try:
return json.loads(post_data)
except Exception:
raise Error(f"POST data is not a valid JSON object: {post_data}")
@property
def post_data_buffer(self) -> Optional[bytes]:
if self._fallback_overrides.post_data_buffer:
return self._fallback_overrides.post_data_buffer
if self._initializer.get("postData"):
return base64.b64decode(self._initializer["postData"])
return None
async def response(self) -> Optional["Response"]:
return from_nullable_channel(await self._channel.send("response"))
@property
def frame(self) -> "Frame":
if not self._initializer.get("frame"):
raise Error("Service Worker requests do not have an associated frame.")
frame = cast("Frame", from_channel(self._initializer["frame"]))
if not frame._page:
raise Error(
"\n".join(
[
"Frame for this navigation request is not available, because the request",
"was issued before the frame is created. You can check whether the request",
"is a navigation request by calling isNavigationRequest() method.",
]
)
)
return frame
def is_navigation_request(self) -> bool:
return self._initializer["isNavigationRequest"]
@property
def redirected_from(self) -> Optional["Request"]:
return self._redirected_from
@property
def redirected_to(self) -> Optional["Request"]:
return self._redirected_to
@property
def failure(self) -> Optional[str]:
return self._failure_text
@property
def timing(self) -> ResourceTiming:
return self._timing
def _set_response_end_timing(self, response_end_timing: float) -> None:
self._timing["responseEnd"] = response_end_timing
if self._timing["responseStart"] == -1:
self._timing["responseStart"] = response_end_timing
@property
def headers(self) -> Headers:
override = self._fallback_overrides.headers
if override:
return RawHeaders._from_headers_dict_lossy(override).headers()
return self._provisional_headers.headers()
async def all_headers(self) -> Headers:
return (await self._actual_headers()).headers()
async def headers_array(self) -> HeadersArray:
return (await self._actual_headers()).headers_array()
async def header_value(self, name: str) -> Optional[str]:
return (await self._actual_headers()).get(name)
async def _actual_headers(self) -> "RawHeaders":
override = self._fallback_overrides.headers
if override:
return RawHeaders(serialize_headers(override))
if not self._all_headers_future:
self._all_headers_future = asyncio.Future()
headers = await self._channel.send("rawRequestHeaders")
self._all_headers_future.set_result(RawHeaders(headers))
return await self._all_headers_future
def _target_closed_future(self) -> asyncio.Future:
frame = cast(
Optional["Frame"], from_nullable_channel(self._initializer.get("frame"))
)
if not frame:
return asyncio.Future()
page = frame._page
if not page:
return asyncio.Future()
return page._closed_or_crashed_future
def _safe_page(self) -> "Optional[Page]":
frame = from_nullable_channel(self._initializer.get("frame"))
if not frame:
return None
return cast("Frame", frame)._page
class Route(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._channel.mark_as_internal_type()
self._handling_future: Optional[asyncio.Future["bool"]] = None
self._context: "BrowserContext" = cast("BrowserContext", None)
self._did_throw = False
def _start_handling(self) -> "asyncio.Future[bool]":
self._handling_future = asyncio.Future()
return self._handling_future
def _report_handled(self, done: bool) -> None:
chain = self._handling_future
assert chain
self._handling_future = None
chain.set_result(done)
def _check_not_handled(self) -> None:
if not self._handling_future:
raise Error("Route is already handled!")
def __repr__(self) -> str:
return f"<Route request={self.request}>"
@property
def request(self) -> Request:
return from_channel(self._initializer["request"])
async def abort(self, errorCode: str = None) -> None:
await self._handle_route(
lambda: self._race_with_page_close(
self._channel.send(
"abort",
{
"errorCode": errorCode,
},
)
)
)
async def fulfill(
self,
status: int = None,
headers: Dict[str, str] = None,
body: Union[str, bytes] = None,
json: Any = None,
path: Union[str, Path] = None,
contentType: str = None,
response: "APIResponse" = None,
) -> None:
await self._handle_route(
lambda: self._inner_fulfill(
status, headers, body, json, path, contentType, response
)
)
async def _inner_fulfill(
self,
status: int = None,
headers: Dict[str, str] = None,
body: Union[str, bytes] = None,
json: Any = None,
path: Union[str, Path] = None,
contentType: str = None,
response: "APIResponse" = None,
) -> None:
params = locals_to_params(locals())
if json is not None:
if body is not None:
raise Error("Can specify either body or json parameters")
body = json_utils.dumps(json)
if response:
del params["response"]
params["status"] = (
params["status"] if params.get("status") else response.status
)
params["headers"] = (
params["headers"] if params.get("headers") else response.headers
)
from playwright._impl._fetch import APIResponse
if body is None and path is None and isinstance(response, APIResponse):
if response._request._connection is self._connection:
params["fetchResponseUid"] = response._fetch_uid
else:
body = await response.body()
length = 0
if isinstance(body, str):
params["body"] = body
params["isBase64"] = False
length = len(body.encode())
elif isinstance(body, bytes):
params["body"] = base64.b64encode(body).decode()
params["isBase64"] = True
length = len(body)
elif path:
del params["path"]
file_content = Path(path).read_bytes()
params["body"] = base64.b64encode(file_content).decode()
params["isBase64"] = True
length = len(file_content)
headers = {k.lower(): str(v) for k, v in params.get("headers", {}).items()}
if params.get("contentType"):
headers["content-type"] = params["contentType"]
elif json:
headers["content-type"] = "application/json"
elif path:
headers["content-type"] = (
mimetypes.guess_type(str(Path(path)))[0] or "application/octet-stream"
)
if length and "content-length" not in headers:
headers["content-length"] = str(length)
params["headers"] = serialize_headers(headers)
await self._race_with_page_close(self._channel.send("fulfill", params))
async def _handle_route(self, callback: Callable) -> None:
self._check_not_handled()
try:
await callback()
self._report_handled(True)
except Exception as e:
self._did_throw = True
raise e
async def fetch(
self,
url: str = None,
method: str = None,
headers: Dict[str, str] = None,
postData: Union[Any, str, bytes] = None,
maxRedirects: int = None,
maxRetries: int = None,
timeout: float = None,
) -> "APIResponse":
return await self._connection.wrap_api_call(
lambda: self._context.request._inner_fetch(
self.request,
url,
method,
headers,
postData,
maxRedirects=maxRedirects,
maxRetries=maxRetries,
timeout=timeout,
)
)
async def fallback(
self,
url: str = None,
method: str = None,
headers: Dict[str, str] = None,
postData: Union[Any, str, bytes] = None,
) -> None:
overrides = cast(FallbackOverrideParameters, locals_to_params(locals()))
self._check_not_handled()
self.request._apply_fallback_overrides(overrides)
self._report_handled(False)
async def continue_(
self,
url: str = None,
method: str = None,
headers: Dict[str, str] = None,
postData: Union[Any, str, bytes] = None,
) -> None:
overrides = cast(FallbackOverrideParameters, locals_to_params(locals()))
async def _inner() -> None:
self.request._apply_fallback_overrides(overrides)
await self._inner_continue(False)
return await self._handle_route(_inner)
async def _inner_continue(self, is_fallback: bool = False) -> None:
options = self.request._fallback_overrides
await self._race_with_page_close(
self._channel.send(
"continue",
{
"url": options.url,
"method": options.method,
"headers": (
serialize_headers(options.headers) if options.headers else None
),
"postData": (
base64.b64encode(options.post_data_buffer).decode()
if options.post_data_buffer is not None
else None
),
"isFallback": is_fallback,
},
)
)
async def _redirected_navigation_request(self, url: str) -> None:
await self._handle_route(
lambda: self._race_with_page_close(
self._channel.send("redirectNavigationRequest", {"url": url})
)
)
async def _race_with_page_close(self, future: Coroutine) -> None:
fut = asyncio.create_task(future)
# Rewrite the user's stack to the new task which runs in the background.
setattr(
fut,
"__pw_stack__",
getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack()),
)
target_closed_future = self.request._target_closed_future()
await asyncio.wait(
[fut, target_closed_future],
return_when=asyncio.FIRST_COMPLETED,
)
if fut.done() and fut.exception():
raise cast(BaseException, fut.exception())
if target_closed_future.done():
await asyncio.gather(fut, return_exceptions=True)
def _create_task_and_ignore_exception(
loop: asyncio.AbstractEventLoop, coro: Coroutine
) -> None:
async def _ignore_exception() -> None:
try:
await coro
except Exception:
pass
loop.create_task(_ignore_exception())
class ServerWebSocketRoute:
def __init__(self, ws: "WebSocketRoute"):
self._ws = ws
def on_message(self, handler: Callable[[Union[str, bytes]], Any]) -> None:
self._ws._on_server_message = handler
def on_close(self, handler: Callable[[Optional[int], Optional[str]], Any]) -> None:
self._ws._on_server_close = handler
def connect_to_server(self) -> None:
raise NotImplementedError(
"connectToServer must be called on the page-side WebSocketRoute"
)
@property
def url(self) -> str:
return self._ws._initializer["url"]
def close(self, code: int = None, reason: str = None) -> None:
_create_task_and_ignore_exception(
self._ws._loop,
self._ws._channel.send(
"closeServer",
{
"code": code,
"reason": reason,
"wasClean": True,
},
),
)
def send(self, message: Union[str, bytes]) -> None:
if isinstance(message, str):
_create_task_and_ignore_exception(
self._ws._loop,
self._ws._channel.send(
"sendToServer", {"message": message, "isBase64": False}
),
)
else:
_create_task_and_ignore_exception(
self._ws._loop,
self._ws._channel.send(
"sendToServer",
{"message": base64.b64encode(message).decode(), "isBase64": True},
),
)
class WebSocketRoute(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._channel.mark_as_internal_type()
self._on_page_message: Optional[Callable[[Union[str, bytes]], Any]] = None
self._on_page_close: Optional[Callable[[Optional[int], Optional[str]], Any]] = (
None
)
self._on_server_message: Optional[Callable[[Union[str, bytes]], Any]] = None
self._on_server_close: Optional[
Callable[[Optional[int], Optional[str]], Any]
] = None
self._server = ServerWebSocketRoute(self)
self._connected = False
self._channel.on("messageFromPage", self._channel_message_from_page)
self._channel.on("messageFromServer", self._channel_message_from_server)
self._channel.on("closePage", self._channel_close_page)
self._channel.on("closeServer", self._channel_close_server)
def _channel_message_from_page(self, event: Dict) -> None:
if self._on_page_message:
self._on_page_message(
base64.b64decode(event["message"])
if event["isBase64"]
else event["message"]
)
elif self._connected:
_create_task_and_ignore_exception(
self._loop, self._channel.send("sendToServer", event)
)
def _channel_message_from_server(self, event: Dict) -> None:
if self._on_server_message:
self._on_server_message(
base64.b64decode(event["message"])
if event["isBase64"]
else event["message"]
)
else:
_create_task_and_ignore_exception(
self._loop, self._channel.send("sendToPage", event)
)
def _channel_close_page(self, event: Dict) -> None:
if self._on_page_close:
self._on_page_close(event["code"], event["reason"])
else:
_create_task_and_ignore_exception(
self._loop, self._channel.send("closeServer", event)
)
def _channel_close_server(self, event: Dict) -> None:
if self._on_server_close:
self._on_server_close(event["code"], event["reason"])
else:
_create_task_and_ignore_exception(
self._loop, self._channel.send("closePage", event)
)
@property
def url(self) -> str:
return self._initializer["url"]
async def close(self, code: int = None, reason: str = None) -> None:
try:
await self._channel.send(
"closePage", {"code": code, "reason": reason, "wasClean": True}
)
except Exception:
pass
def connect_to_server(self) -> "WebSocketRoute":
if self._connected:
raise Error("Already connected to the server")
self._connected = True
asyncio.create_task(self._channel.send("connect"))
return cast("WebSocketRoute", self._server)
def send(self, message: Union[str, bytes]) -> None:
if isinstance(message, str):
_create_task_and_ignore_exception(
self._loop,
self._channel.send(
"sendToPage", {"message": message, "isBase64": False}
),
)
else:
_create_task_and_ignore_exception(
self._loop,
self._channel.send(
"sendToPage",
{
"message": base64.b64encode(message).decode(),
"isBase64": True,
},
),
)
def on_message(self, handler: Callable[[Union[str, bytes]], Any]) -> None:
self._on_page_message = handler
def on_close(self, handler: Callable[[Optional[int], Optional[str]], Any]) -> None:
self._on_page_close = handler
async def _after_handle(self) -> None:
if self._connected:
return
# Ensure that websocket is "open" and can send messages without an actual server connection.
await self._channel.send("ensureOpened")
class WebSocketRouteHandler:
def __init__(
self,
base_url: Optional[str],
url: URLMatch,
handler: WebSocketRouteHandlerCallback,
):
self._base_url = base_url
self.url = url
self.handler = handler
@staticmethod
def prepare_interception_patterns(
handlers: List["WebSocketRouteHandler"],
) -> List[dict]:
patterns = []
all_urls = 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_urls = True
if all_urls:
return [{"glob": "**/*"}]
return patterns
def matches(self, ws_url: str) -> bool:
return url_matches(self._base_url, ws_url, self.url)
async def handle(self, websocket_route: "WebSocketRoute") -> None:
coro_or_future = self.handler(websocket_route)
if asyncio.iscoroutine(coro_or_future):
await coro_or_future
await websocket_route._after_handle()
class Response(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._request: Request = from_channel(self._initializer["request"])
timing = self._initializer["timing"]
self._request._timing["startTime"] = timing["startTime"]
self._request._timing["domainLookupStart"] = timing["domainLookupStart"]
self._request._timing["domainLookupEnd"] = timing["domainLookupEnd"]
self._request._timing["connectStart"] = timing["connectStart"]
self._request._timing["secureConnectionStart"] = timing["secureConnectionStart"]
self._request._timing["connectEnd"] = timing["connectEnd"]
self._request._timing["requestStart"] = timing["requestStart"]
self._request._timing["responseStart"] = timing["responseStart"]
self._provisional_headers = RawHeaders(
cast(HeadersArray, self._initializer["headers"])
)
self._raw_headers_future: Optional[asyncio.Future[RawHeaders]] = None
self._finished_future: asyncio.Future[bool] = asyncio.Future()
def __repr__(self) -> str:
return f"<Response url={self.url!r} request={self.request}>"
@property
def url(self) -> str:
return self._initializer["url"]
@property
def ok(self) -> bool:
# Status 0 is for file:// URLs
return self._initializer["status"] == 0 or (
self._initializer["status"] >= 200 and self._initializer["status"] <= 299
)
@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._provisional_headers.headers()
@property
def from_service_worker(self) -> bool:
return self._initializer["fromServiceWorker"]
async def all_headers(self) -> Headers:
return (await self._actual_headers()).headers()
async def headers_array(self) -> HeadersArray:
return (await self._actual_headers()).headers_array()
async def header_value(self, name: str) -> Optional[str]:
return (await self._actual_headers()).get(name)
async def header_values(self, name: str) -> List[str]:
return (await self._actual_headers()).get_all(name)
async def _actual_headers(self) -> "RawHeaders":
if not self._raw_headers_future:
self._raw_headers_future = asyncio.Future()
headers = cast(HeadersArray, await self._channel.send("rawResponseHeaders"))
self._raw_headers_future.set_result(RawHeaders(headers))
return await self._raw_headers_future
async def server_addr(self) -> Optional[RemoteAddr]:
return await self._channel.send("serverAddr")
async def security_details(self) -> Optional[SecurityDetails]:
return await self._channel.send("securityDetails")
async def finished(self) -> None:
async def on_finished() -> None:
await self._request._target_closed_future()
raise Error("Target closed")
on_finished_task = asyncio.create_task(on_finished())
await asyncio.wait(
cast(
List[Union[asyncio.Task, asyncio.Future]],
[self._finished_future, on_finished_task],
),
return_when=asyncio.FIRST_COMPLETED,
)
if on_finished_task.done():
await on_finished_task
async def body(self) -> bytes:
binary = await self._channel.send("body")
return base64.b64decode(binary)
async def text(self) -> str:
content = await self.body()
return content.decode()
async def json(self) -> Any:
return json.loads(await self.text())
@property
def request(self) -> Request:
return self._request
@property
def frame(self) -> "Frame":
return self._request.frame
class WebSocket(ChannelOwner):
Events = SimpleNamespace(
Close="close",
FrameReceived="framereceived",
FrameSent="framesent",
Error="socketerror",
)
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._is_closed = False
self._page = cast("Page", parent)
self._channel.on(
"frameSent",
lambda params: self._on_frame_sent(params["opcode"], params["data"]),
)
self._channel.on(
"frameReceived",
lambda params: self._on_frame_received(params["opcode"], params["data"]),
)
self._channel.on(
"socketError",
lambda params: self.emit(WebSocket.Events.Error, params["error"]),
)
self._channel.on("close", lambda params: self._on_close())
def __repr__(self) -> str:
return f"<WebSocket url={self.url!r}>"
@property
def url(self) -> str:
return self._initializer["url"]
def expect_event(
self,
event: str,
predicate: Callable = None,
timeout: float = None,
) -> EventContextManagerImpl:
if timeout is None:
timeout = cast(Any, self._parent)._timeout_settings.timeout()
waiter = Waiter(self, f"web_socket.expect_event({event})")
waiter.reject_on_timeout(
cast(float, timeout),
f'Timeout {timeout}ms exceeded while waiting for event "{event}"',
)
if event != WebSocket.Events.Close:
waiter.reject_on_event(self, WebSocket.Events.Close, Error("Socket closed"))
if event != WebSocket.Events.Error:
waiter.reject_on_event(self, WebSocket.Events.Error, Error("Socket error"))
waiter.reject_on_event(
self._page, "close", lambda: self._page._close_error_with_reason()
)
waiter.wait_for_event(self, event, predicate)
return EventContextManagerImpl(waiter.result())
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 _on_frame_sent(self, opcode: int, data: str) -> None:
if opcode == 2:
self.emit(WebSocket.Events.FrameSent, base64.b64decode(data))
elif opcode == 1:
self.emit(WebSocket.Events.FrameSent, data)
def _on_frame_received(self, opcode: int, data: str) -> None:
if opcode == 2:
self.emit(WebSocket.Events.FrameReceived, base64.b64decode(data))
elif opcode == 1:
self.emit(WebSocket.Events.FrameReceived, data)
def is_closed(self) -> bool:
return self._is_closed
def _on_close(self) -> None:
self._is_closed = True
self.emit(WebSocket.Events.Close, self)
class RawHeaders:
def __init__(self, headers: HeadersArray) -> None:
self._headers_array = headers
self._headers_map: Dict[str, Dict[str, bool]] = defaultdict(dict)
for header in headers:
self._headers_map[header["name"].lower()][header["value"]] = True
@staticmethod
def _from_headers_dict_lossy(headers: Dict[str, str]) -> "RawHeaders":
return RawHeaders(serialize_headers(headers))
def get(self, name: str) -> Optional[str]:
values = self.get_all(name)
if not values:
return None
separator = "\n" if name.lower() == "set-cookie" else ", "
return separator.join(values)
def get_all(self, name: str) -> List[str]:
return list(self._headers_map[name.lower()].keys())
def headers(self) -> Dict[str, str]:
result = {}
for name in self._headers_map.keys():
result[name] = cast(str, self.get(name))
return result
def headers_array(self) -> HeadersArray:
return self._headers_array
@@ -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.
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._dialog import Dialog
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._selectors import SelectorsOwner
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 == "Dialog":
return Dialog(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)
if type == "Selectors":
return SelectorsOwner(parent, type, guid, initializer)
return DummyObject(parent, type, guid, initializer)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,25 @@
# 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 pathlib import Path
def get_file_dirname() -> Path:
"""Returns the callee (`__file__`) directory name"""
frame = inspect.stack()[1]
module = inspect.getmodule(frame[0])
assert module
assert module.__file__
return Path(module.__file__).parent.absolute()
@@ -0,0 +1,68 @@
# 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
from playwright._impl._browser_type import BrowserType
from playwright._impl._connection import ChannelOwner, from_channel
from playwright._impl._fetch import APIRequest
from playwright._impl._selectors import Selectors, SelectorsOwner
class Playwright(ChannelOwner):
devices: Dict
selectors: Selectors
chromium: BrowserType
firefox: BrowserType
webkit: BrowserType
request: APIRequest
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self.request = APIRequest(self)
self.chromium = from_channel(initializer["chromium"])
self.chromium._playwright = self
self.firefox = from_channel(initializer["firefox"])
self.firefox._playwright = self
self.webkit = from_channel(initializer["webkit"])
self.webkit._playwright = self
self.selectors = Selectors(self._loop, self._dispatcher_fiber)
selectors_owner: SelectorsOwner = from_channel(initializer["selectors"])
self.selectors._add_channel(selectors_owner)
self._connection.on(
"close", lambda: self.selectors._remove_channel(selectors_owner)
)
self.devices = self._connection.local_utils.devices
def __getitem__(self, value: str) -> "BrowserType":
if value == "chromium":
return self.chromium
elif value == "firefox":
return self.firefox
elif value == "webkit":
return self.webkit
raise ValueError("Invalid browser " + value)
def _set_selectors(self, selectors: Selectors) -> None:
selectors_owner = from_channel(self._initializer["selectors"])
self.selectors._remove_channel(selectors_owner)
self.selectors = selectors
self.selectors._add_channel(selectors_owner)
async def stop(self) -> None:
pass
@@ -0,0 +1,76 @@
# 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 Any, Dict, List, Set, Union
from playwright._impl._connection import ChannelOwner
from playwright._impl._errors import Error
from playwright._impl._helper import async_readfile
from playwright._impl._locator import set_test_id_attribute_name, test_id_attribute_name
class Selectors:
def __init__(self, loop: asyncio.AbstractEventLoop, dispatcher_fiber: Any) -> None:
self._loop = loop
self._channels: Set[SelectorsOwner] = set()
self._registrations: List[Dict] = []
self._dispatcher_fiber = dispatcher_fiber
async def register(
self,
name: str,
script: str = None,
path: Union[str, Path] = None,
contentScript: bool = None,
) -> None:
if not script and not path:
raise Error("Either source or path should be specified")
if path:
script = (await async_readfile(path)).decode()
params: Dict[str, Any] = dict(name=name, source=script)
if contentScript:
params["contentScript"] = True
for channel in self._channels:
await channel._channel.send("register", params)
self._registrations.append(params)
def set_test_id_attribute(self, attributeName: str) -> None:
set_test_id_attribute_name(attributeName)
for channel in self._channels:
channel._channel.send_no_reply(
"setTestIdAttributeName", {"testIdAttributeName": attributeName}
)
def _add_channel(self, channel: "SelectorsOwner") -> None:
self._channels.add(channel)
for params in self._registrations:
# This should not fail except for connection closure, but just in case we catch.
channel._channel.send_no_reply("register", params)
channel._channel.send_no_reply(
"setTestIdAttributeName",
{"testIdAttributeName": test_id_attribute_name()},
)
def _remove_channel(self, channel: "SelectorsOwner") -> None:
if channel in self._channels:
self._channels.remove(channel)
class SelectorsOwner(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
@@ -0,0 +1,155 @@
# 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 os
from pathlib import Path
from typing import (
TYPE_CHECKING,
Dict,
List,
Optional,
Sequence,
Tuple,
TypedDict,
Union,
cast,
)
from playwright._impl._connection import Channel, from_channel
from playwright._impl._helper import Error
from playwright._impl._writable_stream import WritableStream
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._browser_context import BrowserContext
from playwright._impl._api_structures import FilePayload
SIZE_LIMIT_IN_BYTES = 50 * 1024 * 1024
class InputFilesList(TypedDict, total=False):
streams: Optional[List[Channel]]
directoryStream: Optional[Channel]
localDirectory: Optional[str]
localPaths: Optional[List[str]]
payloads: Optional[List[Dict[str, Union[str, bytes]]]]
def _list_files(directory: str) -> List[str]:
files = []
for root, _, filenames in os.walk(directory):
for filename in filenames:
files.append(os.path.join(root, filename))
return files
async def convert_input_files(
files: Union[
str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload]
],
context: "BrowserContext",
) -> InputFilesList:
items = (
files
if isinstance(files, collections.abc.Sequence) and not isinstance(files, str)
else [files]
)
if any([isinstance(item, (str, Path)) for item in items]):
if not all([isinstance(item, (str, Path)) for item in items]):
raise Error("File paths cannot be mixed with buffers")
(local_paths, local_directory) = resolve_paths_and_directory_for_input_files(
cast(Sequence[Union[str, Path]], items)
)
if context._channel._connection.is_remote:
files_to_stream = cast(
List[str],
(_list_files(local_directory) if local_directory else local_paths),
)
streams = []
result = await context._connection.wrap_api_call(
lambda: context._channel.send_return_as_dict(
"createTempFiles",
{
"rootDirName": (
os.path.basename(local_directory)
if local_directory
else None
),
"items": list(
map(
lambda file: dict(
name=(
os.path.relpath(file, local_directory)
if local_directory
else os.path.basename(file)
),
lastModifiedMs=int(os.path.getmtime(file) * 1000),
),
files_to_stream,
)
),
},
)
)
for i, file in enumerate(result["writableStreams"]):
stream: WritableStream = from_channel(file)
await stream.copy(files_to_stream[i])
streams.append(stream._channel)
return InputFilesList(
streams=None if local_directory else streams,
directoryStream=result.get("rootDir"),
)
return InputFilesList(localPaths=local_paths, localDirectory=local_directory)
file_payload_exceeds_size_limit = (
sum([len(f.get("buffer", "")) for f in items if not isinstance(f, (str, Path))])
> SIZE_LIMIT_IN_BYTES
)
if file_payload_exceeds_size_limit:
raise Error(
"Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead."
)
return InputFilesList(
payloads=[
{
"name": item["name"],
"mimeType": item["mimeType"],
"buffer": base64.b64encode(item["buffer"]).decode(),
}
for item in cast(List[FilePayload], items)
]
)
def resolve_paths_and_directory_for_input_files(
items: Sequence[Union[str, Path]]
) -> Tuple[Optional[List[str]], Optional[str]]:
local_paths: Optional[List[str]] = None
local_directory: Optional[str] = None
for item in items:
if os.path.isdir(item):
if local_directory:
raise Error("Multiple directories are not supported")
local_directory = str(Path(item).resolve())
else:
local_paths = local_paths or []
local_paths.append(str(Path(item).resolve()))
if local_paths and local_directory:
raise Error("File paths must be all files or a single directory")
return (local_paths, local_directory)
@@ -0,0 +1,76 @@
# 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 re
from typing import Pattern, Union
def escape_regex_flags(pattern: Pattern) -> str:
flags = ""
if pattern.flags != 0:
flags = ""
if (pattern.flags & int(re.IGNORECASE)) != 0:
flags += "i"
if (pattern.flags & int(re.DOTALL)) != 0:
flags += "s"
if (pattern.flags & int(re.MULTILINE)) != 0:
flags += "m"
assert (
pattern.flags
& ~(int(re.MULTILINE) | int(re.IGNORECASE) | int(re.DOTALL) | int(re.UNICODE))
== 0
), "Unexpected re.Pattern flag, only MULTILINE, IGNORECASE and DOTALL are supported."
return flags
def escape_for_regex(text: str) -> str:
return re.sub(r"[.*+?^>${}()|[\]\\]", "\\$&", text)
def escape_regex_for_selector(text: Pattern) -> str:
# Even number of backslashes followed by the quote -> insert a backslash.
return (
"/"
+ re.sub(r'(^|[^\\])(\\\\)*(["\'`])', r"\1\2\\\3", text.pattern).replace(
">>", "\\>\\>"
)
+ "/"
+ escape_regex_flags(text)
)
def escape_for_text_selector(
text: Union[str, Pattern[str]], exact: bool = None, case_sensitive: bool = None
) -> str:
if isinstance(text, Pattern):
return escape_regex_for_selector(text)
return json.dumps(text) + ("s" if exact else "i")
def escape_for_attribute_selector(
value: Union[str, Pattern], exact: bool = None
) -> str:
if isinstance(value, Pattern):
return escape_regex_for_selector(value)
# TODO: this should actually be
# cssEscape(value).replace(/\\ /g, ' ')
# However, our attribute selectors do not conform to CSS parsing spec,
# so we escape them differently.
return (
'"'
+ value.replace("\\", "\\\\").replace('"', '\\"')
+ '"'
+ ("s" if exact else "i")
)
@@ -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.
import base64
from pathlib import Path
from typing import Dict, Union
from playwright._impl._connection import ChannelOwner
class Stream(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
async def save_as(self, path: Union[str, Path]) -> None:
file = await self._loop.run_in_executor(None, lambda: open(path, "wb"))
while True:
binary = await self._channel.send("read", {"size": 1024 * 1024})
if not binary:
break
await self._loop.run_in_executor(
None, lambda: file.write(base64.b64decode(binary))
)
await self._loop.run_in_executor(None, lambda: file.close())
async def read_all(self) -> bytes:
binary = b""
while True:
chunk = await self._channel.send("read", {"size": 1024 * 1024})
if not chunk:
break
binary += base64.b64decode(chunk)
return binary
@@ -0,0 +1,151 @@
# 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 contextlib import AbstractContextManager
from types import TracebackType
from typing import (
Any,
Callable,
Coroutine,
Generator,
Generic,
Optional,
Type,
TypeVar,
Union,
cast,
)
import greenlet
from playwright._impl._helper import Error
from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper
mapping = ImplToApiMapping()
T = TypeVar("T")
Self = TypeVar("Self", bound="SyncContextManager")
class EventInfo(Generic[T]):
def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None:
self._sync_base = sync_base
self._future = future
g_self = greenlet.getcurrent()
self._future.add_done_callback(lambda _: g_self.switch())
@property
def value(self) -> T:
while not self._future.done():
self._sync_base._dispatcher_fiber.switch()
asyncio._set_running_loop(self._sync_base._loop)
exception = self._future.exception()
if exception:
raise exception
return cast(T, mapping.from_maybe_impl(self._future.result()))
def _cancel(self) -> None:
self._future.cancel()
def is_done(self) -> bool:
return self._future.done()
class EventContextManager(Generic[T], AbstractContextManager):
def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None:
self._event = EventInfo[T](sync_base, future)
def __enter__(self) -> EventInfo[T]:
return self._event
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
if exc_val:
self._event._cancel()
else:
self._event.value
class SyncBase(ImplWrapper):
def __init__(self, impl_obj: Any) -> None:
super().__init__(impl_obj)
self._loop: asyncio.AbstractEventLoop = impl_obj._loop
self._dispatcher_fiber = impl_obj._dispatcher_fiber
def __str__(self) -> str:
return self._impl_obj.__str__()
def _sync(
self,
coro: Union[Coroutine[Any, Any, Any], Generator[Any, Any, Any]],
) -> Any:
__tracebackhide__ = True
if self._loop.is_closed():
coro.close()
raise Error("Event loop is closed! Is Playwright already stopped?")
g_self = greenlet.getcurrent()
task: asyncio.tasks.Task[Any] = self._loop.create_task(coro)
setattr(task, "__pw_stack__", inspect.stack())
setattr(task, "__pw_stack_trace__", traceback.extract_stack())
task.add_done_callback(lambda _: g_self.switch())
while not task.done():
self._dispatcher_fiber.switch()
asyncio._set_running_loop(self._loop)
return task.result()
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 SyncContextManager(SyncBase):
def __enter__(self: Self) -> Self:
return self
def __exit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
_traceback: TracebackType,
) -> None:
self.close()
def close(self) -> None: ...
@@ -0,0 +1,140 @@
# 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 Dict, Optional, Union, cast
from playwright._impl._api_structures import TracingGroupLocation
from playwright._impl._artifact import Artifact
from playwright._impl._connection import ChannelOwner, from_nullable_channel
from playwright._impl._helper import locals_to_params
class Tracing(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
self._channel.mark_as_internal_type()
self._include_sources: bool = False
self._stacks_id: Optional[str] = None
self._is_tracing: bool = False
self._traces_dir: Optional[str] = None
async def start(
self,
name: str = None,
title: str = None,
snapshots: bool = None,
screenshots: bool = None,
sources: bool = None,
) -> None:
params = locals_to_params(locals())
self._include_sources = bool(sources)
await self._channel.send("tracingStart", params)
trace_name = await self._channel.send(
"tracingStartChunk", {"title": title, "name": name}
)
await self._start_collecting_stacks(trace_name)
async def start_chunk(self, title: str = None, name: str = None) -> None:
params = locals_to_params(locals())
trace_name = await self._channel.send("tracingStartChunk", params)
await self._start_collecting_stacks(trace_name)
async def _start_collecting_stacks(self, trace_name: str) -> None:
if not self._is_tracing:
self._is_tracing = True
self._connection.set_is_tracing(True)
self._stacks_id = await self._connection.local_utils.tracing_started(
self._traces_dir, trace_name
)
async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None:
await self._do_stop_chunk(path)
async def stop(self, path: Union[pathlib.Path, str] = None) -> None:
await self._do_stop_chunk(path)
await self._channel.send("tracingStop")
async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> None:
self._reset_stack_counter()
if not file_path:
# Not interested in any artifacts
await self._channel.send("tracingStopChunk", {"mode": "discard"})
if self._stacks_id:
await self._connection.local_utils.trace_discarded(self._stacks_id)
return
is_local = not self._connection.is_remote
if is_local:
result = await self._channel.send_return_as_dict(
"tracingStopChunk", {"mode": "entries"}
)
await self._connection.local_utils.zip(
{
"zipFile": str(file_path),
"entries": result["entries"],
"stacksId": self._stacks_id,
"mode": "write",
"includeSources": self._include_sources,
}
)
return
result = await self._channel.send_return_as_dict(
"tracingStopChunk",
{
"mode": "archive",
},
)
artifact = cast(
Optional[Artifact],
from_nullable_channel(result.get("artifact")),
)
# The artifact may be missing if the browser closed while stopping tracing.
if not artifact:
if self._stacks_id:
await self._connection.local_utils.trace_discarded(self._stacks_id)
return
# Save trace to the final local file.
await artifact.save_as(file_path)
await artifact.delete()
await self._connection.local_utils.zip(
{
"zipFile": str(file_path),
"entries": [],
"stacksId": self._stacks_id,
"mode": "append",
"includeSources": self._include_sources,
}
)
def _reset_stack_counter(self) -> None:
if self._is_tracing:
self._is_tracing = False
self._connection.set_is_tracing(False)
async def group(self, name: str, location: TracingGroupLocation = None) -> None:
await self._channel.send("tracingGroup", locals_to_params(locals()))
async def group_end(self) -> None:
await self._channel.send("tracingGroupEnd")
@@ -0,0 +1,178 @@
# 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 io
import json
import os
import subprocess
import sys
from abc import ABC, abstractmethod
from typing import Callable, Dict, Optional, Union
from playwright._impl._driver import compute_driver_executable, get_driver_env
from playwright._impl._helper import ParsedMessagePayload
# Sourced from: https://github.com/pytest-dev/pytest/blob/da01ee0a4bb0af780167ecd228ab3ad249511302/src/_pytest/faulthandler.py#L69-L77
def _get_stderr_fileno() -> Optional[int]:
try:
# when using pythonw, sys.stderr is None.
# when Pyinstaller is used, there is no closed attribute because Pyinstaller monkey-patches it with a NullWriter class
if sys.stderr is None or not hasattr(sys.stderr, "closed"):
return None
if sys.stderr.closed:
return None
return sys.stderr.fileno()
except (NotImplementedError, AttributeError, io.UnsupportedOperation):
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
# This is potentially dangerous, but the best we can do.
if not hasattr(sys, "__stderr__") or not sys.__stderr__:
return None
return sys.__stderr__.fileno()
class Transport(ABC):
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
self._loop = loop
self.on_message: Callable[[ParsedMessagePayload], None] = lambda _: None
self.on_error_future: asyncio.Future = loop.create_future()
@abstractmethod
def request_stop(self) -> None:
pass
def dispose(self) -> None:
pass
@abstractmethod
async def wait_until_stopped(self) -> None:
pass
@abstractmethod
async def connect(self) -> None:
pass
@abstractmethod
async def run(self) -> None:
pass
@abstractmethod
def send(self, message: Dict) -> None:
pass
def serialize_message(self, message: Dict) -> bytes:
msg = json.dumps(message)
if "DEBUGP" in os.environ: # pragma: no cover
print("\x1b[32mSEND>\x1b[0m", json.dumps(message, indent=2))
return msg.encode()
def deserialize_message(self, data: Union[str, bytes]) -> ParsedMessagePayload:
obj = json.loads(data)
if "DEBUGP" in os.environ: # pragma: no cover
print("\x1b[33mRECV>\x1b[0m", json.dumps(obj, indent=2))
return obj
class PipeTransport(Transport):
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
super().__init__(loop)
self._stopped = False
def request_stop(self) -> None:
assert self._output
self._stopped = True
self._output.close()
async def wait_until_stopped(self) -> None:
await self._stopped_future
async def connect(self) -> None:
self._stopped_future: asyncio.Future = asyncio.Future()
try:
# For pyinstaller and Nuitka
env = get_driver_env()
if getattr(sys, "frozen", False) or globals().get("__compiled__"):
env.setdefault("PLAYWRIGHT_BROWSERS_PATH", "0")
startupinfo = None
if sys.platform == "win32":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
executable_path, entrypoint_path = compute_driver_executable()
self._proc = await asyncio.create_subprocess_exec(
executable_path,
entrypoint_path,
"run-driver",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=_get_stderr_fileno(),
limit=32768,
env=env,
startupinfo=startupinfo,
)
except Exception as exc:
self.on_error_future.set_exception(exc)
raise exc
self._output = self._proc.stdin
async def run(self) -> None:
assert self._proc.stdout
assert self._proc.stdin
while not self._stopped:
try:
buffer = await self._proc.stdout.readexactly(4)
if self._stopped:
break
length = int.from_bytes(buffer, byteorder="little", signed=False)
buffer = bytes(0)
while length:
to_read = min(length, 32768)
data = await self._proc.stdout.readexactly(to_read)
if self._stopped:
break
length -= to_read
if len(buffer):
buffer = buffer + data
else:
buffer = data
if self._stopped:
break
obj = self.deserialize_message(buffer)
self.on_message(obj)
except asyncio.IncompleteReadError:
if not self._stopped:
self.on_error_future.set_exception(
Exception("Connection closed while reading from the driver")
)
break
await asyncio.sleep(0)
await self._proc.communicate()
self._stopped_future.set_result(None)
def send(self, message: Dict) -> None:
assert self._output
data = self.serialize_message(message)
self._output.write(
len(data).to_bytes(4, byteorder="little", signed=False) + data
)
Loaded 100 of 1488 files, more files were not shown because too many files have changed in this diff. Show more