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 -->147.0.7727.15<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->148.0.7778.96<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->148.0.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->150.0.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
18 changes: 18 additions & 0 deletions playwright/_impl/_api_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,31 @@ class ViewportSize(TypedDict):

class SourceLocation(TypedDict):
url: str
line: int
column: int
lineNumber: int
columnNumber: int


class WebErrorLocation(TypedDict):
url: str
line: int
column: int


class FilePayload(TypedDict):
name: str
mimeType: str
buffer: bytes


class DropPayload(TypedDict, total=False):
files: Optional[
Union[str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload]]
]
data: Optional[Dict[str, str]]


class RemoteAddr(TypedDict):
ipAddress: str
port: int
Expand Down Expand Up @@ -216,6 +231,7 @@ class FrameExpectOptions(TypedDict, total=False):
useInnerText: Optional[bool]
isNot: bool
timeout: Optional[float]
pseudo: Optional[str]


class FrameExpectResult(TypedDict):
Expand Down Expand Up @@ -330,3 +346,5 @@ class DebuggerPausedDetails(TypedDict):

class ScreencastFrame(TypedDict):
data: bytes
viewportWidth: int
viewportHeight: int
36 changes: 33 additions & 3 deletions playwright/_impl/_assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

import collections.abc
from typing import Any, List, Optional, Pattern, Sequence, Union
from typing import Any, List, Literal, Optional, Pattern, Sequence, Union
from urllib.parse import urljoin

from playwright._impl._api_structures import (
Expand All @@ -26,6 +26,7 @@
from playwright._impl._errors import Error
from playwright._impl._fetch import APIResponse
from playwright._impl._helper import is_textual_mime_type
from playwright._impl._js_handle import parse_value
from playwright._impl._locator import Locator
from playwright._impl._page import Page
from playwright._impl._str_utils import escape_regex_flags
Expand Down Expand Up @@ -71,7 +72,14 @@ async def _expect_impl(
del expect_options["useInnerText"]
result = await self._call_expect(expression, expect_options, title)
if result["matches"] == self._is_not:
actual = result.get("received")
received = result.get("received") or {}
if isinstance(received, dict):
if "value" in received and received["value"] is not None:
actual = parse_value(received["value"])
else:
actual = received.get("ariaSnapshot")
else:
actual = received
if self._custom_message:
out_message = self._custom_message
if expected is not None:
Expand Down Expand Up @@ -161,6 +169,24 @@ async def not_to_have_url(
__tracebackhide__ = True
await self._not.to_have_url(urlOrRegExp, timeout, ignoreCase)

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,
"Page expected to match Aria snapshot",
'Expect "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 LocatorAssertions(AssertionsBase):
def __init__(
Expand Down Expand Up @@ -400,13 +426,17 @@ async def to_have_css(
name: str,
value: Union[str, Pattern[str]],
timeout: float = None,
pseudo: Literal["after", "before"] = 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
expressionArg=name,
expectedText=expected_text,
timeout=timeout,
pseudo=pseudo,
),
value,
"Locator expected to have CSS",
Expand Down
2 changes: 2 additions & 0 deletions playwright/_impl/_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

class Browser(ChannelOwner):
Events = SimpleNamespace(
Context="context",
Disconnected="disconnected",
)

Expand Down Expand Up @@ -104,6 +105,7 @@ def _did_create_context(self, context: BrowserContext) -> None:
# and will be configured later in `ConnectToBrowserType`.
if self._browser_type:
self._setup_browser_context(context)
self.emit(Browser.Events.Context, context)

def _setup_browser_context(self, context: BrowserContext) -> None:
context._tracing._traces_dir = self._traces_dir
Expand Down
83 changes: 20 additions & 63 deletions playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
Geolocation,
SetCookieParam,
StorageState,
WebErrorLocation,
)
from playwright._impl._artifact import Artifact
from playwright._impl._cdp_session import CDPSession
from playwright._impl._clock import Clock
from playwright._impl._connection import (
Expand All @@ -57,7 +57,6 @@
from playwright._impl._helper import (
HarContentPolicy,
HarMode,
HarRecordingMetadata,
RouteFromHarNotFoundPolicy,
RouteHandler,
RouteHandlerCallback,
Expand Down Expand Up @@ -95,7 +94,13 @@ class BrowserContext(ChannelOwner):
Close="close",
Console="console",
Dialog="dialog",
Download="download",
FrameAttached="frameattached",
FrameDetached="framedetached",
FrameNavigated="framenavigated",
Page="page",
PageClose="pageclose",
PageLoad="pageload",
WebError="weberror",
ServiceWorker="serviceworker",
Request="request",
Expand Down Expand Up @@ -125,7 +130,6 @@ def __init__(
self._videos_dir: Optional[str] = self._options.get("recordVideo")
self._tracing = cast(Tracing, from_channel(initializer["tracing"]))
self._debugger: Debugger = cast(Debugger, from_channel(initializer["debugger"]))
self._har_recorders: Dict[str, HarRecordingMetadata] = {}
self._request: APIRequestContext = from_channel(initializer["requestContext"])
self._request._timeout_settings = self._timeout_settings
self._clock = Clock(self)
Expand Down Expand Up @@ -171,6 +175,10 @@ def __init__(
lambda params: self._on_page_error(
parse_error(params["error"]["error"]),
from_nullable_channel(params["page"]),
cast(
WebErrorLocation,
params.get("location") or {"url": "", "line": 0, "column": 0},
),
),
)
self._channel.on(
Expand Down Expand Up @@ -321,7 +329,7 @@ async def _initialize_har_from_options(
content_policy: HarContentPolicy = record_har_content or (
"omit" if record_har_omit_content is True else default_policy
)
await self._record_into_har(
await self._tracing._record_into_har(
har=record_har_path,
page=None,
url=record_har_url_filter,
Expand Down Expand Up @@ -404,9 +412,7 @@ async def add_init_script(
await self._channel.send("addInitScript", None, dict(source=script))
)

async def expose_binding(
self, name: str, callback: Callable, handle: bool = None
) -> Disposable:
async def expose_binding(self, name: str, callback: Callable) -> Disposable:
for page in self._pages:
if name in page._bindings:
raise Error(
Expand All @@ -416,9 +422,7 @@ async def expose_binding(
raise Error(f'Function "{name}" has been already registered')
self._bindings[name] = callback
return from_channel(
await self._channel.send(
"exposeBinding", None, dict(name=name, needsHandle=handle or False)
)
await self._channel.send("exposeBinding", None, dict(name=name))
)

async def expose_function(self, name: str, callback: Callable) -> Disposable:
Expand Down Expand Up @@ -483,35 +487,6 @@ async def unroute_all(
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:
update_content = update_content or "attach"
params: Dict[str, Any] = {
"options": {
"zip": str(har).endswith(".zip"),
"content": update_content,
"urlGlob": url if isinstance(url, str) else None,
"urlRegexSource": url.pattern if isinstance(url, Pattern) else None,
"urlRegexFlags": (
escape_regex_flags(url) if isinstance(url, Pattern) else None
),
"mode": update_mode or "minimal",
}
}
if page:
params["page"] = page._channel
har_id = await self._channel.send("harStart", None, params)
self._har_recorders[har_id] = {
"path": str(har),
"content": update_content,
}

async def route_from_har(
self,
har: Union[Path, str],
Expand All @@ -522,7 +497,7 @@ async def route_from_har(
updateMode: HarMode = None,
) -> None:
if update:
await self._record_into_har(
await self._tracing._record_into_har(
har=har,
page=None,
url=url,
Expand Down Expand Up @@ -602,27 +577,7 @@ async def close(self, reason: str = None) -> None:
await self.request.dispose(reason=reason)

async def _inner_close() -> None:
for har_id, params in self._har_recorders.items():
har = cast(
Artifact,
from_channel(
await self._channel.send("harExport", None, {"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._tracing._export_all_hars()

await self._channel._connection.wrap_api_call(_inner_close, True)
await self._channel.send("close", None, {"reason": reason})
Expand Down Expand Up @@ -732,10 +687,12 @@ def _on_dialog(self, dialog: Dialog) -> None:
else:
asyncio.create_task(dialog.dismiss())

def _on_page_error(self, error: Error, page: Optional[Page]) -> None:
def _on_page_error(
self, error: Error, page: Optional[Page], location: WebErrorLocation
) -> None:
self.emit(
BrowserContext.Events.WebError,
WebError(self._loop, self._dispatcher_fiber, page, error),
WebError(self._loop, self._dispatcher_fiber, page, error, location),
)
if page:
page.emit(Page.Events.PageError, error)
Expand Down
1 change: 1 addition & 0 deletions playwright/_impl/_browser_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ async def connect_over_cdp(
slowMo: float = None,
headers: Dict[str, str] = None,
isLocal: bool = None,
noDefaults: bool = None,
) -> Browser:
params = locals_to_params(locals())
if params.get("headers"):
Expand Down
11 changes: 10 additions & 1 deletion playwright/_impl/_console_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,16 @@ def args(self) -> List[JSHandle]:

@property
def location(self) -> SourceLocation:
return self._event["location"]
# Wire format uses `lineNumber`/`columnNumber`; docs expose both `line`/`column`
# (legacy) and `lineNumber`/`columnNumber` (added upstream in 1.60).
loc = self._event["location"]
return {
"url": loc["url"],
"line": loc["lineNumber"],
"column": loc["columnNumber"],
"lineNumber": loc["lineNumber"],
"columnNumber": loc["columnNumber"],
}

@property
def timestamp(self) -> float:
Expand Down
5 changes: 5 additions & 0 deletions playwright/_impl/_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def __init__(

async def dispose(self, reason: str = None) -> None:
self._close_reason = reason
await self._tracing._export_all_hars()
try:
await self._channel.send("dispose", None, {"reason": reason})
except Error as e:
Expand All @@ -118,6 +119,10 @@ async def dispose(self, reason: str = None) -> None:
raise e
self._tracing._reset_stack_counter()

@property
def tracing(self) -> Tracing:
return self._tracing

async def delete(
self,
url: str,
Expand Down
Loading
Loading