Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->98.0.4714.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->99.0.4812.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->94.0.1<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->95.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
26 changes: 6 additions & 20 deletions playwright/_impl/_assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import re
from typing import Any, List, Pattern, Union
from urllib.parse import urljoin

from playwright._impl._api_structures import ExpectedTextValue, FrameExpectOptions
from playwright._impl._locator import Locator
from playwright._impl._page import Page
from playwright._impl._str_utils import escape_regex_flags


class AssertionsBase:
Expand Down Expand Up @@ -379,10 +379,13 @@ async def not_to_have_text(
async def to_be_checked(
self,
timeout: float = None,
checked: bool = None,
) -> None:
__tracebackhide__ = True
await self._expect_impl(
"to.be.checked",
"to.be.checked"
if checked is None or checked is True
else "to.be.unchecked",
FrameExpectOptions(timeout=timeout),
None,
"Locator expected to be checked",
Expand Down Expand Up @@ -534,27 +537,10 @@ def expected_regex(
) -> ExpectedTextValue:
expected = ExpectedTextValue(
regexSource=pattern.pattern,
regexFlags=escape_regex_flags(pattern),
matchSubstring=match_substring,
normalizeWhiteSpace=normalize_white_space,
)
if pattern.flags != 0:
expected["regexFlags"] = ""
if (pattern.flags & int(re.IGNORECASE)) != 0:
expected["regexFlags"] += "i"
if (pattern.flags & int(re.DOTALL)) != 0:
expected["regexFlags"] += "s"
if (pattern.flags & int(re.MULTILINE)) != 0:
expected["regexFlags"] += "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 expected


Expand Down
2 changes: 2 additions & 0 deletions playwright/_impl/_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
is_safe_close_error,
locals_to_params,
)
from playwright._impl._local_utils import LocalUtils
from playwright._impl._network import serialize_headers
from playwright._impl._page import Page

Expand All @@ -59,6 +60,7 @@ def __init__(
self._should_close_connection_on_close = False

self._contexts: List[BrowserContext] = []
_utils: LocalUtils
self._channel.on("close", lambda _: self._on_close())

def __repr__(self) -> str:
Expand Down
4 changes: 3 additions & 1 deletion playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
locals_to_params,
to_impl,
)
from playwright._impl._local_utils import LocalUtils
from playwright._impl._network import Request, Response, Route, serialize_headers
from playwright._impl._page import BindingCall, Page, Worker
from playwright._impl._tracing import Tracing
Expand Down Expand Up @@ -86,6 +87,7 @@ def __init__(
self._request: APIRequestContext = from_channel(
initializer["APIRequestContext"]
)
_local_utils: LocalUtils
self._channel.on(
"bindingCall",
lambda params: self._on_binding(from_channel(params["binding"])),
Expand Down Expand Up @@ -291,7 +293,7 @@ def expect_event(
timeout = self._timeout_settings.timeout()
wait_helper = WaitHelper(self, f"browser_context.expect_event({event})")
wait_helper.reject_on_timeout(
timeout, f'Timeout while waiting for event "{event}"'
timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"'
)
if event != BrowserContext.Events.Close:
wait_helper.reject_on_event(
Expand Down
10 changes: 9 additions & 1 deletion playwright/_impl/_browser_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
_playwright: "Playwright"

def __repr__(self) -> str:
return f"<BrowserType name={self.name} executable_path={self.executable_path}>"
Expand Down Expand Up @@ -85,7 +86,11 @@ async def launch(
) -> Browser:
params = locals_to_params(locals())
normalize_launch_params(params)
return from_channel(await self._channel.send("launch", params))
browser = cast(
Browser, from_channel(await self._channel.send("launch", params))
)
browser._utils = self._playwright._utils
return browser

async def launch_persistent_context(
self,
Expand Down Expand Up @@ -142,6 +147,7 @@ async def launch_persistent_context(
await self._channel.send("launchPersistentContext", params)
)
context._options = params
context._local_utils = self._playwright._utils
return context

async def connect_over_cdp(
Expand All @@ -154,6 +160,7 @@ async def connect_over_cdp(
params = locals_to_params(locals())
response = await self._channel.send_return_as_dict("connectOverCDP", params)
browser = cast(Browser, from_channel(response["browser"]))
browser._utils = self._playwright._utils

default_context = cast(
Optional[BrowserContext],
Expand Down Expand Up @@ -203,6 +210,7 @@ async def connect(
assert pre_launched_browser
browser = cast(Browser, from_channel(pre_launched_browser))
browser._should_close_connection_on_close = True
browser._utils = self._playwright._utils

def handle_transport_close() -> None:
for context in browser.contexts:
Expand Down
27 changes: 20 additions & 7 deletions playwright/_impl/_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,16 +247,19 @@ async def fetch(
# 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 = 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:
if isinstance(data, str):
post_data_buffer = data.encode()
if is_json_content_type(serialized_headers):
json_data = data
else:
post_data_buffer = data.encode()
elif isinstance(data, bytes):
post_data_buffer = data
elif isinstance(data, (dict, list)):
elif isinstance(data, (dict, list, int, bool)):
json_data = data
else:
raise Error(f"Unsupported 'data' type: {type(data)}")
Expand Down Expand Up @@ -290,7 +293,7 @@ async def fetch(
def filter_none(input: Dict) -> Dict:
return {k: v for k, v in input.items() if v is not None}

result = await self._channel.send_return_as_dict(
response = await self._channel.send(
"fetch",
filter_none(
{
Expand All @@ -308,9 +311,7 @@ def filter_none(input: Dict) -> Dict:
}
),
)
if result.get("error"):
raise Error(result["error"])
return APIResponse(self, result["response"])
return APIResponse(self, response)

async def storage_state(
self, path: Union[pathlib.Path, str] = None
Expand All @@ -337,6 +338,9 @@ def __init__(self, context: APIRequestContext, initializer: Dict) -> None:
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
Expand Down Expand Up @@ -395,3 +399,12 @@ async def dispose(self) -> None:

def _fetch_uid(self) -> str:
return self._initializer["fetchUid"]


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
12 changes: 6 additions & 6 deletions playwright/_impl/_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import asyncio
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union, cast
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Pattern, Set, Union, cast

from pyee import EventEmitter

Expand Down Expand Up @@ -102,6 +102,9 @@ def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None:
if "error" not in event and hasattr(self, "_page") 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":
return self._page
Expand Down Expand Up @@ -495,11 +498,8 @@ async def fill(
) -> None:
await self._channel.send("fill", locals_to_params(locals()))

def locator(
self,
selector: str,
) -> Locator:
return Locator(self, selector)
def locator(self, selector: str, has_text: Union[str, Pattern] = None) -> Locator:
return Locator(self, selector, has_text=has_text)

def frame_locator(self, selector: str) -> FrameLocator:
return FrameLocator(self, selector)
Expand Down
28 changes: 28 additions & 0 deletions playwright/_impl/_local_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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, List

from playwright._impl._api_structures import NameValue
from playwright._impl._connection import ChannelOwner


class LocalUtils(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)

async def zip(self, zip_file: str, entries: List[NameValue]) -> None:
await self._channel.send("zip", {"zipFile": zip_file, "entries": entries})
44 changes: 39 additions & 5 deletions playwright/_impl/_locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Dict,
List,
Optional,
Pattern,
TypeVar,
Union,
)
Expand All @@ -42,6 +43,7 @@
monotonic_time,
)
from playwright._impl._js_handle import Serializable, parse_value, serialize_argument
from playwright._impl._str_utils import escape_regex_flags, escape_with_quotes

if sys.version_info >= (3, 8): # pragma: no cover
from typing import Literal
Expand All @@ -56,12 +58,23 @@


class Locator:
def __init__(self, frame: "Frame", selector: str) -> None:
def __init__(
self, frame: "Frame", selector: str, has_text: Union[str, Pattern] = None
) -> None:
self._frame = frame
self._selector = selector
self._loop = frame._loop
self._dispatcher_fiber = frame._connection._dispatcher_fiber

if has_text:
if isinstance(has_text, Pattern):
pattern = escape_with_quotes(has_text.pattern, '"')
flags = escape_regex_flags(has_text)
self._selector += f' >> :scope:text-matches({pattern}, "{flags}")'
else:
escaped = escape_with_quotes(has_text, '"')
self._selector += f" >> :scope:has-text({escaped})"

def __repr__(self) -> str:
return f"<Locator frame={self._frame!r} selector={self._selector!r}>"

Expand Down Expand Up @@ -170,8 +183,11 @@ async def fill(
def locator(
self,
selector: str,
has_text: Union[str, Pattern] = None,
) -> "Locator":
return Locator(self._frame, f"{self._selector} >> {selector}")
return Locator(
self._frame, f"{self._selector} >> {selector}", has_text=has_text
)

def frame_locator(self, selector: str) -> "FrameLocator":
return FrameLocator(self._frame, self._selector + " >> " + selector)
Expand Down Expand Up @@ -208,7 +224,23 @@ async def focus(self, timeout: float = None) -> None:
async def count(
self,
) -> int:
return int(await self.evaluate_all("ee => ee.length"))
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())
Expand Down Expand Up @@ -506,9 +538,11 @@ def __init__(self, frame: "Frame", frame_selector: str) -> None:
self._dispatcher_fiber = frame._connection._dispatcher_fiber
self._frame_selector = frame_selector

def locator(self, selector: str) -> Locator:
def locator(self, selector: str, has_text: Union[str, Pattern] = None) -> Locator:
return Locator(
self._frame, f"{self._frame_selector} >> control=enter-frame >> {selector}"
self._frame,
f"{self._frame_selector} >> control=enter-frame >> {selector}",
has_text=has_text,
)

def frame_locator(self, selector: str) -> "FrameLocator":
Expand Down
Loading