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
2 changes: 1 addition & 1 deletion DRIVER_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.62.1
1.63.0
2 changes: 1 addition & 1 deletion NODE_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24.19.0
24.21.0
6 changes: 3 additions & 3 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 -->151.0.7922.34<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->153.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->153.0.8010.12<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->26.6<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->155.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
5 changes: 3 additions & 2 deletions playwright/_impl/_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def _did_create_context(self, context: BrowserContext) -> None:

def _setup_browser_context(self, context: BrowserContext) -> None:
context._tracing._traces_dir = self._traces_dir
context._request._tracing._traces_dir = self._traces_dir
assert self._browser_type is not None
self._browser_type._playwright.selectors._contexts_for_selectors.add(context)

Expand Down Expand Up @@ -143,7 +144,7 @@ async def new_context(
permissions: Sequence[str] = None,
extraHTTPHeaders: Dict[str, str] = None,
offline: bool = None,
httpCredentials: HttpCredentials = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
deviceScaleFactor: float = None,
isMobile: bool = None,
hasTouch: bool = None,
Expand Down Expand Up @@ -197,7 +198,7 @@ async def new_page(
permissions: Sequence[str] = None,
extraHTTPHeaders: Dict[str, str] = None,
offline: bool = None,
httpCredentials: HttpCredentials = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
deviceScaleFactor: float = None,
isMobile: bool = None,
hasTouch: bool = None,
Expand Down
18 changes: 17 additions & 1 deletion playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class BrowserContext(ChannelOwner):
Close="close",
Console="console",
Dialog="dialog",
DialogClosed="dialogclosed",
Download="download",
FrameAttached="frameattached",
FrameDetached="framedetached",
Expand Down Expand Up @@ -173,6 +174,10 @@ def __init__(
self._channel.on(
"dialog", lambda params: self._on_dialog(from_channel(params["dialog"]))
)
self._channel.on(
"dialogClosed",
lambda params: self._on_dialog_closed(from_channel(params["dialog"])),
)
self._channel.on(
"pageError",
lambda params: self._on_page_error(
Expand Down Expand Up @@ -226,6 +231,7 @@ def __init__(
{
BrowserContext.Events.Console: "console",
BrowserContext.Events.Dialog: "dialog",
BrowserContext.Events.DialogClosed: "dialogClosed",
BrowserContext.Events.Request: "request",
BrowserContext.Events.Response: "response",
BrowserContext.Events.RequestFinished: "requestFinished",
Expand Down Expand Up @@ -567,6 +573,7 @@ def _on_close(self) -> None:

self._dispose_har_routers()
self._tracing._reset_stack_counter()
self._request._tracing._reset_stack_counter()
self.emit(BrowserContext.Events.Close, self)

def is_closed(self) -> bool:
Expand All @@ -591,10 +598,13 @@ async def storage_state(
self,
path: Union[str, Path] = None,
indexedDB: bool = None,
opfs: bool = None,
credentials: bool = None,
) -> StorageState:
result = await self._channel.send_return_as_dict(
"storageState", None, {"indexedDB": indexedDB, "credentials": credentials}
"storageState",
None,
{"indexedDB": indexedDB, "opfs": opfs, "credentials": credentials},
)
if path:
await async_writefile(path, json.dumps(result))
Expand Down Expand Up @@ -694,6 +704,12 @@ def _on_dialog(self, dialog: Dialog) -> None:
else:
create_task_and_ignore_exception(self._loop, dialog.dismiss())

def _on_dialog_closed(self, dialog: Dialog) -> None:
self.emit(BrowserContext.Events.DialogClosed, dialog)
page = dialog.page
if page:
page.emit(Page.Events.DialogClosed, dialog)

def _on_page_error(
self, error: Error, page: Optional[Page], location: WebErrorLocation
) -> None:
Expand Down
12 changes: 10 additions & 2 deletions playwright/_impl/_browser_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@
locals_to_params,
)
from playwright._impl._json_pipe import JsonPipeTransport
from playwright._impl._network import serialize_headers, to_client_certificates_protocol
from playwright._impl._network import (
serialize_headers,
to_client_certificates_protocol,
to_http_credentials_protocol,
)
from playwright._impl._waiter import throw_on_timeout

if TYPE_CHECKING:
Expand Down Expand Up @@ -133,7 +137,7 @@ async def launch_persistent_context(
permissions: Sequence[str] = None,
extraHTTPHeaders: Dict[str, str] = None,
offline: bool = None,
httpCredentials: HttpCredentials = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
deviceScaleFactor: float = None,
isMobile: bool = None,
hasTouch: bool = None,
Expand Down Expand Up @@ -333,6 +337,10 @@ async def _prepare_browser_context_params(self, params: Dict) -> None:
params["clientCertificates"] = await to_client_certificates_protocol(
params["clientCertificates"]
)
if "httpCredentials" in params:
params["httpCredentials"] = to_http_credentials_protocol(
params["httpCredentials"]
)
params["selectorEngines"] = self._playwright.selectors._selector_engines
params["testIdAttributeName"] = (
self._playwright.selectors._test_id_attribute_name
Expand Down
17 changes: 13 additions & 4 deletions playwright/_impl/_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import pathlib
import typing
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, cast
from typing import Any, Dict, List, Optional, Sequence, Union, cast

import playwright._impl._network as network
from playwright._impl._api_structures import (
Expand Down Expand Up @@ -49,7 +49,11 @@
object_to_array,
to_impl,
)
from playwright._impl._network import serialize_headers, to_client_certificates_protocol
from playwright._impl._network import (
serialize_headers,
to_client_certificates_protocol,
to_http_credentials_protocol,
)
from playwright._impl._tracing import Tracing

if typing.TYPE_CHECKING:
Expand All @@ -72,7 +76,7 @@ async def new_context(
self,
baseURL: str = None,
extraHTTPHeaders: Dict[str, str] = None,
httpCredentials: HttpCredentials = None,
httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None,
ignoreHTTPSErrors: bool = None,
proxy: ProxySettings = None,
userAgent: str = None,
Expand All @@ -94,6 +98,10 @@ async def new_context(
params["clientCertificates"] = await to_client_certificates_protocol(
params.get("clientCertificates")
)
if "httpCredentials" in params:
params["httpCredentials"] = to_http_credentials_protocol(
params["httpCredentials"]
)
context = cast(
APIRequestContext,
from_channel(
Expand Down Expand Up @@ -458,9 +466,10 @@ async def storage_state(
self,
path: Union[pathlib.Path, str] = None,
indexedDB: bool = None,
opfs: bool = None,
) -> StorageState:
result = await self._channel.send_return_as_dict(
"storageState", None, {"indexedDB": indexedDB}
"storageState", None, {"indexedDB": indexedDB, "opfs": opfs}
)
if path:
await async_writefile(path, json.dumps(result))
Expand Down
5 changes: 3 additions & 2 deletions playwright/_impl/_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
serialize_argument,
)
from playwright._impl._locator import (
ANY_FRAME_SELECTOR,
FrameLocator,
Locator,
get_by_alt_text_selector,
Expand Down Expand Up @@ -722,8 +723,8 @@ def get_by_title(
) -> "Locator":
return self.locator(get_by_title_selector(text, exact=exact))

def frame_locator(self, selector: str) -> FrameLocator:
return FrameLocator(self, selector)
def frame_locator(self, selector: str = None) -> FrameLocator:
return FrameLocator(self, ANY_FRAME_SELECTOR if selector is None else selector)

async def focus(
self, selector: str, strict: bool = None, timeout: float = None
Expand Down
32 changes: 23 additions & 9 deletions playwright/_impl/_locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ def filter(
visible=visible,
)

@property
def visible(self) -> "Locator":
return Locator(self._frame, self._selector, visible=True)

def or_(self, locator: "Locator") -> "Locator":
if locator._frame != self._frame:
raise Error("Locators must belong to the same frame.")
Expand Down Expand Up @@ -809,13 +813,26 @@ async def hide_highlight(self) -> None:
await self._frame._hide_highlight(self._selector)


ANY_FRAME_SELECTOR = "internal:control=any-frame"


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 _child_selector(self, selector: str) -> str:
if self._frame_selector == ANY_FRAME_SELECTOR:
return f"{self._frame_selector} >> {selector}"
return f"{self._frame_selector} >> internal:control=enter-frame >> {selector}"

def _nth_selector(self, nth: str) -> str:
if self._frame_selector == ANY_FRAME_SELECTOR:
raise Error("Selecting the nth frame is not allowed on frame_locator().")
return f"{self._frame_selector} >> nth={nth}"

def locator(
self,
selectorOrLocator: Union["Locator", str],
Expand All @@ -827,7 +844,7 @@ def locator(
if isinstance(selectorOrLocator, str):
return Locator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator}",
self._child_selector(selectorOrLocator),
has_text=hasText,
has_not_text=hasNotText,
has=has,
Expand All @@ -838,7 +855,7 @@ def locator(
raise ValueError("Locators must belong to the same frame.")
return Locator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator._selector}",
self._child_selector(selectorOrLocator._selector),
has_text=hasText,
has_not_text=hasNotText,
has=has,
Expand Down Expand Up @@ -904,25 +921,22 @@ def get_by_title(
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}",
)
return FrameLocator(self._frame, self._child_selector(selector))

@property
def first(self) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth=0")
return FrameLocator(self._frame, self._nth_selector("0"))

@property
def last(self) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth=-1")
return FrameLocator(self._frame, self._nth_selector("-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}")
return FrameLocator(self._frame, self._nth_selector(str(index)))

def __repr__(self) -> str:
return f"<FrameLocator frame={self._frame!r} selector={self._frame_selector!r}>"
Expand Down
16 changes: 16 additions & 0 deletions playwright/_impl/_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
Dict,
List,
Optional,
Sequence,
TypedDict,
Union,
cast,
Expand All @@ -39,6 +40,7 @@
ClientCertificate,
Headers,
HeadersArray,
HttpCredentials,
RemoteAddr,
RequestSizes,
ResourceTiming,
Expand Down Expand Up @@ -126,6 +128,20 @@ async def to_client_certificates_protocol(
return out


def to_http_credentials_protocol(
httpCredentials: Optional[Union[HttpCredentials, Sequence[HttpCredentials]]],
) -> Optional[List[HttpCredentials]]:
if not httpCredentials:
return None
credentials_list = (
[httpCredentials] if isinstance(httpCredentials, dict) else httpCredentials
)
return [
cast(HttpCredentials, {k: v for k, v in c.items() if v is not None})
for c in credentials_list
] or None


class Request(ChannelOwner):
def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
Expand Down
4 changes: 3 additions & 1 deletion playwright/_impl/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class Page(ChannelOwner):
Crash="crash",
Console="console",
Dialog="dialog",
DialogClosed="dialogclosed",
Download="download",
FileChooser="filechooser",
DOMContentLoaded="domcontentloaded",
Expand Down Expand Up @@ -268,6 +269,7 @@ def __init__(
{
Page.Events.Console: "console",
Page.Events.Dialog: "dialog",
Page.Events.DialogClosed: "dialogClosed",
Page.Events.Request: "request",
Page.Events.Response: "response",
Page.Events.RequestFinished: "requestFinished",
Expand Down Expand Up @@ -999,7 +1001,7 @@ def get_by_title(
) -> "Locator":
return self._main_frame.get_by_title(text, exact=exact)

def frame_locator(self, selector: str) -> "FrameLocator":
def frame_locator(self, selector: str = None) -> "FrameLocator":
return self.main_frame.frame_locator(selector)

async def focus(
Expand Down
8 changes: 6 additions & 2 deletions playwright/_impl/_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ async def start(
name: str = None,
title: str = None,
snapshots: bool = None,
ariaSnapshots: bool = None,
screenSnapshots: bool = None,
screenshots: bool = None,
sources: bool = None,
live: bool = None,
Expand All @@ -57,8 +59,10 @@ async def start(
None,
{
"name": name,
"snapshots": snapshots,
"screenshots": screenshots,
"snapshotDom": snapshots,
"snapshotAria": ariaSnapshots,
"snapshotScreen": screenSnapshots,
"screencast": screenshots,
"live": live,
},
)
Expand Down
Loading