diff --git a/README.md b/README.md
index e99460db3..1efcead54 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
-| Chromium 130.0.6723.31 | ✅ | ✅ | ✅ |
-| WebKit 18.0 | ✅ | ✅ | ✅ |
-| Firefox 131.0 | ✅ | ✅ | ✅ |
+| Chromium 131.0.6778.33 | ✅ | ✅ | ✅ |
+| WebKit 18.2 | ✅ | ✅ | ✅ |
+| Firefox 132.0 | ✅ | ✅ | ✅ |
## Documentation
diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py
index 904a590a9..3b639486a 100644
--- a/playwright/_impl/_api_structures.py
+++ b/playwright/_impl/_api_structures.py
@@ -291,3 +291,9 @@ class FrameExpectResult(TypedDict):
"treegrid",
"treeitem",
]
+
+
+class TracingGroupLocation(TypedDict):
+ file: str
+ line: Optional[int]
+ column: Optional[int]
diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py
index 13e7ac481..fce405da7 100644
--- a/playwright/_impl/_assertions.py
+++ b/playwright/_impl/_assertions.py
@@ -783,6 +783,23 @@ 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__(
diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py
index 4645e2415..f415d5900 100644
--- a/playwright/_impl/_browser_context.py
+++ b/playwright/_impl/_browser_context.py
@@ -61,7 +61,6 @@
RouteHandlerCallback,
TimeoutSettings,
URLMatch,
- URLMatcher,
WebSocketRouteHandlerCallback,
async_readfile,
async_writefile,
@@ -416,7 +415,8 @@ async def route(
self._routes.insert(
0,
RouteHandler(
- URLMatcher(self._options.get("baseURL"), url),
+ self._options.get("baseURL"),
+ url,
handler,
True if self._dispatcher_fiber else False,
times,
@@ -430,7 +430,7 @@ async def unroute(
removed = []
remaining = []
for route in self._routes:
- if route.matcher.match != url or (handler and route.handler != handler):
+ if route.url != url or (handler and route.handler != handler):
remaining.append(route)
else:
removed.append(route)
@@ -453,9 +453,7 @@ async def route_web_socket(
) -> None:
self._web_socket_routes.insert(
0,
- WebSocketRouteHandler(
- URLMatcher(self._options.get("baseURL"), url), handler
- ),
+ WebSocketRouteHandler(self._options.get("baseURL"), url, handler),
)
await self._update_web_socket_interception_patterns()
diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py
index 1ce813636..d616046e6 100644
--- a/playwright/_impl/_frame.py
+++ b/playwright/_impl/_frame.py
@@ -45,10 +45,10 @@
Literal,
MouseButton,
URLMatch,
- URLMatcher,
async_readfile,
locals_to_params,
monotonic_time,
+ url_matches,
)
from playwright._impl._js_handle import (
JSHandle,
@@ -185,18 +185,17 @@ def expect_navigation(
to_url = f' to "{url}"' if url else ""
waiter.log(f"waiting for navigation{to_url} until '{waitUntil}'")
- matcher = (
- URLMatcher(self._page._browser_context._options.get("baseURL"), url)
- if url
- else None
- )
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 not matcher or matcher.matches(event["url"])
+ return url_matches(
+ cast("Page", self._page)._browser_context._options.get("baseURL"),
+ event["url"],
+ url,
+ )
waiter.wait_for_event(
self._event_emitter,
@@ -226,8 +225,9 @@ async def wait_for_url(
timeout: float = None,
) -> None:
assert self._page
- matcher = URLMatcher(self._page._browser_context._options.get("baseURL"), url)
- if matcher.matches(self.url):
+ 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(
diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py
index 027b3e1f5..d0737be07 100644
--- a/playwright/_impl/_helper.py
+++ b/playwright/_impl/_helper.py
@@ -142,27 +142,26 @@ class FrameNavigatedEvent(TypedDict):
Env = Dict[str, Union[str, float, bool]]
-class URLMatcher:
- def __init__(self, base_url: Union[str, None], match: URLMatch) -> None:
- self._callback: Optional[Callable[[str], bool]] = None
- self._regex_obj: Optional[Pattern[str]] = None
- if isinstance(match, str):
- if base_url and not match.startswith("*"):
- match = urljoin(base_url, match)
- regex = glob_to_regex(match)
- self._regex_obj = re.compile(regex)
- elif isinstance(match, Pattern):
- self._regex_obj = match
- else:
- self._callback = match
- self.match = match
-
- def matches(self, url: str) -> bool:
- if self._callback:
- return self._callback(url)
- if self._regex_obj:
- return cast(bool, self._regex_obj.search(url))
- return False
+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):
@@ -271,12 +270,14 @@ def __init__(self, complete: "asyncio.Future", route: "Route") -> None:
class RouteHandler:
def __init__(
self,
- matcher: URLMatcher,
+ base_url: Optional[str],
+ url: URLMatch,
handler: RouteHandlerCallback,
is_sync: bool,
times: Optional[int] = None,
):
- self.matcher = matcher
+ self._base_url = base_url
+ self.url = url
self.handler = handler
self._times = times if times else math.inf
self._handled_count = 0
@@ -285,7 +286,7 @@ def __init__(
self._active_invocations: Set[RouteHandlerInvocation] = set()
def matches(self, request_url: str) -> bool:
- return self.matcher.matches(request_url)
+ return url_matches(self._base_url, request_url, self.url)
async def handle(self, route: "Route") -> bool:
handler_invocation = RouteHandlerInvocation(
@@ -362,13 +363,13 @@ def prepare_interception_patterns(
patterns = []
all = False
for handler in handlers:
- if isinstance(handler.matcher.match, str):
- patterns.append({"glob": handler.matcher.match})
- elif isinstance(handler.matcher._regex_obj, re.Pattern):
+ if isinstance(handler.url, str):
+ patterns.append({"glob": handler.url})
+ elif isinstance(handler.url, re.Pattern):
patterns.append(
{
- "regexSource": handler.matcher._regex_obj.pattern,
- "regexFlags": escape_regex_flags(handler.matcher._regex_obj),
+ "regexSource": handler.url.pattern,
+ "regexFlags": escape_regex_flags(handler.url),
}
)
else:
diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py
index 521897978..91ea79064 100644
--- a/playwright/_impl/_locator.py
+++ b/playwright/_impl/_locator.py
@@ -534,6 +534,15 @@ async def screenshot(
),
)
+ 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,
diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py
index 53f97a46c..97bb049e3 100644
--- a/playwright/_impl/_network.py
+++ b/playwright/_impl/_network.py
@@ -53,10 +53,11 @@
from playwright._impl._errors import Error
from playwright._impl._event_context_manager import EventContextManagerImpl
from playwright._impl._helper import (
- URLMatcher,
+ URLMatch,
WebSocketRouteHandlerCallback,
async_readfile,
locals_to_params,
+ url_matches,
)
from playwright._impl._str_utils import escape_regex_flags
from playwright._impl._waiter import Waiter
@@ -718,8 +719,14 @@ async def _after_handle(self) -> None:
class WebSocketRouteHandler:
- def __init__(self, matcher: URLMatcher, handler: WebSocketRouteHandlerCallback):
- self.matcher = matcher
+ def __init__(
+ self,
+ base_url: Optional[str],
+ url: URLMatch,
+ handler: WebSocketRouteHandlerCallback,
+ ):
+ self._base_url = base_url
+ self.url = url
self.handler = handler
@staticmethod
@@ -729,13 +736,13 @@ def prepare_interception_patterns(
patterns = []
all_urls = False
for handler in handlers:
- if isinstance(handler.matcher.match, str):
- patterns.append({"glob": handler.matcher.match})
- elif isinstance(handler.matcher._regex_obj, re.Pattern):
+ if isinstance(handler.url, str):
+ patterns.append({"glob": handler.url})
+ elif isinstance(handler.url, re.Pattern):
patterns.append(
{
- "regexSource": handler.matcher._regex_obj.pattern,
- "regexFlags": escape_regex_flags(handler.matcher._regex_obj),
+ "regexSource": handler.url.pattern,
+ "regexFlags": escape_regex_flags(handler.url),
}
)
else:
@@ -746,7 +753,7 @@ def prepare_interception_patterns(
return patterns
def matches(self, ws_url: str) -> bool:
- return self.matcher.matches(ws_url)
+ 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)
diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py
index 15195b28b..62fec2a3f 100644
--- a/playwright/_impl/_page.py
+++ b/playwright/_impl/_page.py
@@ -71,7 +71,6 @@
RouteHandlerCallback,
TimeoutSettings,
URLMatch,
- URLMatcher,
URLMatchRequest,
URLMatchResponse,
WebSocketRouteHandlerCallback,
@@ -80,6 +79,7 @@
locals_to_params,
make_dirs_for_file,
serialize_error,
+ url_matches,
)
from playwright._impl._input import Keyboard, Mouse, Touchscreen
from playwright._impl._js_handle import (
@@ -380,16 +380,14 @@ def main_frame(self) -> Frame:
return self._main_frame
def frame(self, name: str = None, url: URLMatch = None) -> Optional[Frame]:
- matcher = (
- URLMatcher(self._browser_context._options.get("baseURL"), url)
- if url
- else None
- )
for frame in self._frames:
if name and frame.name == name:
return frame
- if url and matcher and matcher.matches(frame.url):
+ if url and url_matches(
+ self._browser_context._options.get("baseURL"), frame.url, url
+ ):
return frame
+
return None
@property
@@ -656,7 +654,8 @@ async def route(
self._routes.insert(
0,
RouteHandler(
- URLMatcher(self._browser_context._options.get("baseURL"), url),
+ self._browser_context._options.get("baseURL"),
+ url,
handler,
True if self._dispatcher_fiber else False,
times,
@@ -670,7 +669,7 @@ async def unroute(
removed = []
remaining = []
for route in self._routes:
- if route.matcher.match != url or (handler and route.handler != handler):
+ if route.url != url or (handler and route.handler != handler):
remaining.append(route)
else:
removed.append(route)
@@ -699,7 +698,7 @@ async def route_web_socket(
self._web_socket_routes.insert(
0,
WebSocketRouteHandler(
- URLMatcher(self._browser_context._options.get("baseURL"), url), handler
+ self._browser_context._options.get("baseURL"), url, handler
),
)
await self._update_web_socket_interception_patterns()
@@ -1235,21 +1234,14 @@ def expect_request(
urlOrPredicate: URLMatchRequest,
timeout: float = None,
) -> EventContextManagerImpl[Request]:
- matcher = (
- None
- if callable(urlOrPredicate)
- else URLMatcher(
- self._browser_context._options.get("baseURL"), urlOrPredicate
- )
- )
- predicate = urlOrPredicate if callable(urlOrPredicate) else None
-
def my_predicate(request: Request) -> bool:
- if matcher:
- return matcher.matches(request.url)
- if predicate:
- return predicate(request)
- return True
+ if not callable(urlOrPredicate):
+ return url_matches(
+ self._browser_context._options.get("baseURL"),
+ request.url,
+ urlOrPredicate,
+ )
+ return urlOrPredicate(request)
trimmed_url = trim_url(urlOrPredicate)
log_line = f"waiting for request {trimmed_url}" if trimmed_url else None
@@ -1274,21 +1266,14 @@ def expect_response(
urlOrPredicate: URLMatchResponse,
timeout: float = None,
) -> EventContextManagerImpl[Response]:
- matcher = (
- None
- if callable(urlOrPredicate)
- else URLMatcher(
- self._browser_context._options.get("baseURL"), urlOrPredicate
- )
- )
- predicate = urlOrPredicate if callable(urlOrPredicate) else None
-
- def my_predicate(response: Response) -> bool:
- if matcher:
- return matcher.matches(response.url)
- if predicate:
- return predicate(response)
- return True
+ def my_predicate(request: Response) -> bool:
+ if not callable(urlOrPredicate):
+ return url_matches(
+ self._browser_context._options.get("baseURL"),
+ request.url,
+ urlOrPredicate,
+ )
+ return urlOrPredicate(request)
trimmed_url = trim_url(urlOrPredicate)
log_line = f"waiting for response {trimmed_url}" if trimmed_url else None
diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py
index d645e41da..a68b53bf7 100644
--- a/playwright/_impl/_tracing.py
+++ b/playwright/_impl/_tracing.py
@@ -15,6 +15,7 @@
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
@@ -131,3 +132,9 @@ 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")
diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py
index c01b23fc2..e1480f5bf 100644
--- a/playwright/async_api/_generated.py
+++ b/playwright/async_api/_generated.py
@@ -37,6 +37,7 @@
SetCookieParam,
SourceLocation,
StorageState,
+ TracingGroupLocation,
ViewportSize,
)
from playwright._impl._assertions import (
@@ -922,9 +923,8 @@ async def handle(route, request):
**Details**
- Note that any overrides such as `url` or `headers` only apply to the request being routed. If this request results
- in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header
- through redirects, use the combination of `route.fetch()` and `route.fulfill()` instead.
+ The `headers` option applies to both the routed request and any redirects it initiates. However, `url`, `method`,
+ and `postData` only apply to the original request and are not carried over to redirected requests.
`route.continue_()` will immediately send the request to the network, other matching handlers won't be
invoked. Use `route.fallback()` If you want next matching handler in the chain to be invoked.
@@ -6923,6 +6923,9 @@ async def set_fixed_time(
Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running.
+ Use this method for simple scenarios where you only need to test with a predefined time. For more advanced
+ scenarios, use `clock.install()` instead. Read docs on [clock emulation](https://playwright.dev/python/docs/clock) to learn more.
+
**Usage**
```py
@@ -6944,7 +6947,8 @@ async def set_system_time(
) -> None:
"""Clock.set_system_time
- Sets current system time but does not trigger any timers.
+ Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for
+ example switching from summer to winter time, or changing time zones.
**Usage**
@@ -9294,8 +9298,6 @@ async def emulate_media(
# → True
await page.evaluate(\"matchMedia('(prefers-color-scheme: light)').matches\")
# → False
- await page.evaluate(\"matchMedia('(prefers-color-scheme: no-preference)').matches\")
- # → False
```
Parameters
@@ -9304,8 +9306,9 @@ async def emulate_media(
Changes the CSS media type of the page. The only allowed values are `'Screen'`, `'Print'` and `'Null'`. Passing
`'Null'` disables CSS media emulation.
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`.
- Passing `'Null'` disables color scheme emulation.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. Passing `'Null'` disables color scheme emulation.
+ `'no-preference'` is deprecated.
reduced_motion : Union["no-preference", "null", "reduce", None]
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing
`null` disables reduced motion emulation.
@@ -13804,9 +13807,9 @@ async def new_context(
Specifies if viewport supports touch events. Defaults to false. Learn more about
[mobile emulation](../emulation.md#devices).
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
- `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
- `'light'`.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details.
+ Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
reduced_motion : Union["no-preference", "null", "reduce", None]
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
`page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
@@ -14029,9 +14032,9 @@ async def new_page(
Specifies if viewport supports touch events. Defaults to false. Learn more about
[mobile emulation](../emulation.md#devices).
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
- `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
- `'light'`.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details.
+ Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
forced_colors : Union["active", "none", "null", None]
Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See
`page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
@@ -14341,9 +14344,12 @@ async def launch(
resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium,
Firefox or WebKit, use at your own risk.
channel : Union[str, None]
- Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary",
- "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using
- [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
+ Browser distribution channel.
+
+ Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode).
+
+ Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
+ "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
args : Union[Sequence[str], None]
**NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
@@ -14496,9 +14502,12 @@ async def launch_persistent_context(
user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty
string to use a temporary directory instead.
channel : Union[str, None]
- Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary",
- "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using
- [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
+ Browser distribution channel.
+
+ Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode).
+
+ Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
+ "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
executable_path : Union[pathlib.Path, str, None]
Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is
resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium,
@@ -14588,9 +14597,9 @@ async def launch_persistent_context(
Specifies if viewport supports touch events. Defaults to false. Learn more about
[mobile emulation](../emulation.md#devices).
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
- `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
- `'light'`.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details.
+ Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
reduced_motion : Union["no-preference", "null", "reduce", None]
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
`page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
@@ -15084,6 +15093,48 @@ async def stop(
return mapping.from_maybe_impl(await self._impl_obj.stop(path=path))
+ async def group(
+ self, name: str, *, location: typing.Optional[TracingGroupLocation] = None
+ ) -> None:
+ """Tracing.group
+
+ **NOTE** Use `test.step` instead when available.
+
+ Creates a new group within the trace, assigning any subsequent API calls to this group, until
+ `tracing.group_end()` is called. Groups can be nested and will be visible in the trace viewer.
+
+ **Usage**
+
+ ```py
+ # All actions between group and group_end
+ # will be shown in the trace viewer as a group.
+ page.context.tracing.group(\"Open Playwright.dev > API\")
+ page.goto(\"https://playwright.dev/\")
+ page.get_by_role(\"link\", name=\"API\").click()
+ page.context.tracing.group_end()
+ ```
+
+ Parameters
+ ----------
+ name : str
+ Group name shown in the trace viewer.
+ location : Union[{file: str, line: Union[int, None], column: Union[int, None]}, None]
+ Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the
+ `tracing.group()` call.
+ """
+
+ return mapping.from_maybe_impl(
+ await self._impl_obj.group(name=name, location=location)
+ )
+
+ async def group_end(self) -> None:
+ """Tracing.group_end
+
+ Closes the last group created by `tracing.group()`.
+ """
+
+ return mapping.from_maybe_impl(await self._impl_obj.group_end())
+
mapping.register(TracingImpl, Tracing)
@@ -17101,6 +17152,61 @@ async def screenshot(
)
)
+ async def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str:
+ """Locator.aria_snapshot
+
+ Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/python/docs/aria-snapshots) and
+ `locator_assertions.to_match_aria_snapshot()` for the corresponding assertion.
+
+ **Usage**
+
+ ```py
+ await page.get_by_role(\"link\").aria_snapshot()
+ ```
+
+ **Details**
+
+ This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of
+ the element and its children. The snapshot can be used to assert the state of the element in the test, or to
+ compare it to state in the future.
+
+ The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language:
+ - The keys of the objects are the roles and optional accessible names of the elements.
+ - The values are either text content or an array of child elements.
+ - Generic static text can be represented with the `text` key.
+
+ Below is the HTML markup and the respective ARIA snapshot:
+
+ ```html
+
+ - Home
+ - About
+
+ ```
+
+ ```yml
+ - list \"Links\":
+ - listitem:
+ - link \"Home\"
+ - listitem:
+ - link \"About\"
+ ```
+
+ Parameters
+ ----------
+ timeout : Union[float, None]
+ Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can
+ be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods.
+
+ Returns
+ -------
+ str
+ """
+
+ return mapping.from_maybe_impl(
+ await self._impl_obj.aria_snapshot(timeout=timeout)
+ )
+
async def scroll_into_view_if_needed(
self, *, timeout: typing.Optional[float] = None
) -> None:
@@ -20373,6 +20479,58 @@ async def not_to_have_role(
await self._impl_obj.not_to_have_role(role=role, timeout=timeout)
)
+ async def to_match_aria_snapshot(
+ self, expected: str, *, timeout: typing.Optional[float] = None
+ ) -> None:
+ """LocatorAssertions.to_match_aria_snapshot
+
+ Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/python/docs/aria-snapshots).
+
+ **Usage**
+
+ ```py
+ await page.goto(\"https://demo.playwright.dev/todomvc/\")
+ await expect(page.locator('body')).to_match_aria_snapshot('''
+ - heading \"todos\"
+ - textbox \"What needs to be done?\"
+ ''')
+ ```
+
+ Parameters
+ ----------
+ expected : str
+ timeout : Union[float, None]
+ Time to retry the assertion for in milliseconds. Defaults to `5000`.
+ """
+ __tracebackhide__ = True
+
+ return mapping.from_maybe_impl(
+ await self._impl_obj.to_match_aria_snapshot(
+ expected=expected, timeout=timeout
+ )
+ )
+
+ async def not_to_match_aria_snapshot(
+ self, expected: str, *, timeout: typing.Optional[float] = None
+ ) -> None:
+ """LocatorAssertions.not_to_match_aria_snapshot
+
+ The opposite of `locator_assertions.to_match_aria_snapshot()`.
+
+ Parameters
+ ----------
+ expected : str
+ timeout : Union[float, None]
+ Time to retry the assertion for in milliseconds. Defaults to `5000`.
+ """
+ __tracebackhide__ = True
+
+ return mapping.from_maybe_impl(
+ await self._impl_obj.not_to_match_aria_snapshot(
+ expected=expected, timeout=timeout
+ )
+ )
+
mapping.register(LocatorAssertionsImpl, LocatorAssertions)
diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py
index 23aebc560..42401bc64 100644
--- a/playwright/sync_api/_generated.py
+++ b/playwright/sync_api/_generated.py
@@ -37,6 +37,7 @@
SetCookieParam,
SourceLocation,
StorageState,
+ TracingGroupLocation,
ViewportSize,
)
from playwright._impl._assertions import (
@@ -936,9 +937,8 @@ def handle(route, request):
**Details**
- Note that any overrides such as `url` or `headers` only apply to the request being routed. If this request results
- in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header
- through redirects, use the combination of `route.fetch()` and `route.fulfill()` instead.
+ The `headers` option applies to both the routed request and any redirects it initiates. However, `url`, `method`,
+ and `postData` only apply to the original request and are not carried over to redirected requests.
`route.continue_()` will immediately send the request to the network, other matching handlers won't be
invoked. Use `route.fallback()` If you want next matching handler in the chain to be invoked.
@@ -7033,6 +7033,9 @@ def set_fixed_time(self, time: typing.Union[float, str, datetime.datetime]) -> N
Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running.
+ Use this method for simple scenarios where you only need to test with a predefined time. For more advanced
+ scenarios, use `clock.install()` instead. Read docs on [clock emulation](https://playwright.dev/python/docs/clock) to learn more.
+
**Usage**
```py
@@ -7056,7 +7059,8 @@ def set_system_time(
) -> None:
"""Clock.set_system_time
- Sets current system time but does not trigger any timers.
+ Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for
+ example switching from summer to winter time, or changing time zones.
**Usage**
@@ -9335,7 +9339,6 @@ def emulate_media(
# → True
page.evaluate(\"matchMedia('(prefers-color-scheme: light)').matches\")
# → False
- page.evaluate(\"matchMedia('(prefers-color-scheme: no-preference)').matches\")
```
Parameters
@@ -9344,8 +9347,9 @@ def emulate_media(
Changes the CSS media type of the page. The only allowed values are `'Screen'`, `'Print'` and `'Null'`. Passing
`'Null'` disables CSS media emulation.
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`.
- Passing `'Null'` disables color scheme emulation.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. Passing `'Null'` disables color scheme emulation.
+ `'no-preference'` is deprecated.
reduced_motion : Union["no-preference", "null", "reduce", None]
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing
`null` disables reduced motion emulation.
@@ -13840,9 +13844,9 @@ def new_context(
Specifies if viewport supports touch events. Defaults to false. Learn more about
[mobile emulation](../emulation.md#devices).
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
- `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
- `'light'`.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details.
+ Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
reduced_motion : Union["no-preference", "null", "reduce", None]
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
`page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
@@ -14067,9 +14071,9 @@ def new_page(
Specifies if viewport supports touch events. Defaults to false. Learn more about
[mobile emulation](../emulation.md#devices).
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
- `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
- `'light'`.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details.
+ Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
forced_colors : Union["active", "none", "null", None]
Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See
`page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
@@ -14383,9 +14387,12 @@ def launch(
resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium,
Firefox or WebKit, use at your own risk.
channel : Union[str, None]
- Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary",
- "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using
- [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
+ Browser distribution channel.
+
+ Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode).
+
+ Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
+ "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
args : Union[Sequence[str], None]
**NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
@@ -14540,9 +14547,12 @@ def launch_persistent_context(
user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty
string to use a temporary directory instead.
channel : Union[str, None]
- Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary",
- "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using
- [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
+ Browser distribution channel.
+
+ Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode).
+
+ Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
+ "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
executable_path : Union[pathlib.Path, str, None]
Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is
resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium,
@@ -14632,9 +14642,9 @@ def launch_persistent_context(
Specifies if viewport supports touch events. Defaults to false. Learn more about
[mobile emulation](../emulation.md#devices).
color_scheme : Union["dark", "light", "no-preference", "null", None]
- Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
- `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
- `'light'`.
+ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details.
+ Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
reduced_motion : Union["no-preference", "null", "reduce", None]
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
`page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to
@@ -15133,6 +15143,48 @@ def stop(
return mapping.from_maybe_impl(self._sync(self._impl_obj.stop(path=path)))
+ def group(
+ self, name: str, *, location: typing.Optional[TracingGroupLocation] = None
+ ) -> None:
+ """Tracing.group
+
+ **NOTE** Use `test.step` instead when available.
+
+ Creates a new group within the trace, assigning any subsequent API calls to this group, until
+ `tracing.group_end()` is called. Groups can be nested and will be visible in the trace viewer.
+
+ **Usage**
+
+ ```py
+ # All actions between group and group_end
+ # will be shown in the trace viewer as a group.
+ await page.context.tracing.group(\"Open Playwright.dev > API\")
+ await page.goto(\"https://playwright.dev/\")
+ await page.get_by_role(\"link\", name=\"API\").click()
+ await page.context.tracing.group_end()
+ ```
+
+ Parameters
+ ----------
+ name : str
+ Group name shown in the trace viewer.
+ location : Union[{file: str, line: Union[int, None], column: Union[int, None]}, None]
+ Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the
+ `tracing.group()` call.
+ """
+
+ return mapping.from_maybe_impl(
+ self._sync(self._impl_obj.group(name=name, location=location))
+ )
+
+ def group_end(self) -> None:
+ """Tracing.group_end
+
+ Closes the last group created by `tracing.group()`.
+ """
+
+ return mapping.from_maybe_impl(self._sync(self._impl_obj.group_end()))
+
mapping.register(TracingImpl, Tracing)
@@ -17191,6 +17243,61 @@ def screenshot(
)
)
+ def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str:
+ """Locator.aria_snapshot
+
+ Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/python/docs/aria-snapshots) and
+ `locator_assertions.to_match_aria_snapshot()` for the corresponding assertion.
+
+ **Usage**
+
+ ```py
+ page.get_by_role(\"link\").aria_snapshot()
+ ```
+
+ **Details**
+
+ This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of
+ the element and its children. The snapshot can be used to assert the state of the element in the test, or to
+ compare it to state in the future.
+
+ The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language:
+ - The keys of the objects are the roles and optional accessible names of the elements.
+ - The values are either text content or an array of child elements.
+ - Generic static text can be represented with the `text` key.
+
+ Below is the HTML markup and the respective ARIA snapshot:
+
+ ```html
+
+ - Home
+ - About
+
+ ```
+
+ ```yml
+ - list \"Links\":
+ - listitem:
+ - link \"Home\"
+ - listitem:
+ - link \"About\"
+ ```
+
+ Parameters
+ ----------
+ timeout : Union[float, None]
+ Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can
+ be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods.
+
+ Returns
+ -------
+ str
+ """
+
+ return mapping.from_maybe_impl(
+ self._sync(self._impl_obj.aria_snapshot(timeout=timeout))
+ )
+
def scroll_into_view_if_needed(
self, *, timeout: typing.Optional[float] = None
) -> None:
@@ -20551,6 +20658,62 @@ def not_to_have_role(
self._sync(self._impl_obj.not_to_have_role(role=role, timeout=timeout))
)
+ def to_match_aria_snapshot(
+ self, expected: str, *, timeout: typing.Optional[float] = None
+ ) -> None:
+ """LocatorAssertions.to_match_aria_snapshot
+
+ Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/python/docs/aria-snapshots).
+
+ **Usage**
+
+ ```py
+ page.goto(\"https://demo.playwright.dev/todomvc/\")
+ expect(page.locator('body')).to_match_aria_snapshot('''
+ - heading \"todos\"
+ - textbox \"What needs to be done?\"
+ ''')
+ ```
+
+ Parameters
+ ----------
+ expected : str
+ timeout : Union[float, None]
+ Time to retry the assertion for in milliseconds. Defaults to `5000`.
+ """
+ __tracebackhide__ = True
+
+ return mapping.from_maybe_impl(
+ self._sync(
+ self._impl_obj.to_match_aria_snapshot(
+ expected=expected, timeout=timeout
+ )
+ )
+ )
+
+ def not_to_match_aria_snapshot(
+ self, expected: str, *, timeout: typing.Optional[float] = None
+ ) -> None:
+ """LocatorAssertions.not_to_match_aria_snapshot
+
+ The opposite of `locator_assertions.to_match_aria_snapshot()`.
+
+ Parameters
+ ----------
+ expected : str
+ timeout : Union[float, None]
+ Time to retry the assertion for in milliseconds. Defaults to `5000`.
+ """
+ __tracebackhide__ = True
+
+ return mapping.from_maybe_impl(
+ self._sync(
+ self._impl_obj.not_to_match_aria_snapshot(
+ expected=expected, timeout=timeout
+ )
+ )
+ )
+
mapping.register(LocatorAssertionsImpl, LocatorAssertions)
diff --git a/scripts/generate_api.py b/scripts/generate_api.py
index e609dae73..01f8f525a 100644
--- a/scripts/generate_api.py
+++ b/scripts/generate_api.py
@@ -225,7 +225,7 @@ def return_value(value: Any) -> List[str]:
from playwright._impl._accessibility import Accessibility as AccessibilityImpl
-from playwright._impl._api_structures import Cookie, SetCookieParam, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ClientCertificate, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue
+from playwright._impl._api_structures import Cookie, SetCookieParam, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ClientCertificate, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue, TracingGroupLocation
from playwright._impl._browser import Browser as BrowserImpl
from playwright._impl._browser_context import BrowserContext as BrowserContextImpl
from playwright._impl._browser_type import BrowserType as BrowserTypeImpl
diff --git a/setup.py b/setup.py
index ead8dad3d..b4576c6a1 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@
import zipfile
from typing import Dict
-driver_version = "1.48.1"
+driver_version = "1.49.0-beta-1732210972000"
base_wheel_bundles = [
{
diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py
index af4516f87..b89ebd7f2 100644
--- a/tests/async/test_browsercontext.py
+++ b/tests/async/test_browsercontext.py
@@ -32,6 +32,11 @@
from .utils import Utils
+@pytest.fixture(scope="session")
+def fails_on_401(browser_name: str, is_headless_shell: bool) -> bool:
+ return browser_name == "chromium" and not is_headless_shell
+
+
async def test_page_event_should_create_new_context(browser: Browser) -> None:
assert len(browser.contexts) == 0
context = await browser.new_context()
@@ -472,13 +477,17 @@ def logme(t: JSHandle) -> int:
async def test_auth_should_fail_without_credentials(
- context: BrowserContext, server: Server
+ context: BrowserContext, server: Server, fails_on_401: bool
) -> None:
server.set_auth("/empty.html", "user", "pass")
page = await context.new_page()
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- assert response.status == 401
+ try:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 401
+ except Error as exc:
+ assert fails_on_401
+ assert "net::ERR_INVALID_AUTH_CREDENTIALS" in exc.message
async def test_auth_should_work_with_correct_credentials(
@@ -562,7 +571,7 @@ async def test_should_work_with_correct_credentials_and_matching_origin_case_ins
async def test_should_fail_with_correct_credentials_and_mismatching_scheme(
- browser: Browser, server: Server
+ browser: Browser, server: Server, fails_on_401: bool
) -> None:
server.set_auth("/empty.html", "user", "pass")
context = await browser.new_context(
@@ -573,14 +582,18 @@ async def test_should_fail_with_correct_credentials_and_mismatching_scheme(
}
)
page = await context.new_page()
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- assert response.status == 401
+ try:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 401
+ except Error as exc:
+ assert fails_on_401
+ assert "net::ERR_INVALID_AUTH_CREDENTIALS" in exc.message
await context.close()
async def test_should_fail_with_correct_credentials_and_mismatching_hostname(
- browser: Browser, server: Server
+ browser: Browser, server: Server, fails_on_401: bool
) -> None:
server.set_auth("/empty.html", "user", "pass")
hostname = urlparse(server.PREFIX).hostname
@@ -590,14 +603,18 @@ async def test_should_fail_with_correct_credentials_and_mismatching_hostname(
http_credentials={"username": "user", "password": "pass", "origin": origin}
)
page = await context.new_page()
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- assert response.status == 401
+ try:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 401
+ except Error as exc:
+ assert fails_on_401
+ assert "net::ERR_INVALID_AUTH_CREDENTIALS" in exc.message
await context.close()
async def test_should_fail_with_correct_credentials_and_mismatching_port(
- browser: Browser, server: Server
+ browser: Browser, server: Server, fails_on_401: bool
) -> None:
server.set_auth("/empty.html", "user", "pass")
origin = server.PREFIX.replace(str(server.PORT), str(server.PORT + 1))
@@ -605,9 +622,13 @@ async def test_should_fail_with_correct_credentials_and_mismatching_port(
http_credentials={"username": "user", "password": "pass", "origin": origin}
)
page = await context.new_page()
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- assert response.status == 401
+ try:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 401
+ except Error as exc:
+ assert fails_on_401
+ assert "net::ERR_INVALID_AUTH_CREDENTIALS" in exc.message
await context.close()
diff --git a/tests/async/test_emulation_focus.py b/tests/async/test_emulation_focus.py
index a59d549f4..8f298f9ca 100644
--- a/tests/async/test_emulation_focus.py
+++ b/tests/async/test_emulation_focus.py
@@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
-from typing import Callable
from playwright.async_api import Page
from tests.server import Server
@@ -106,29 +105,6 @@ async def test_should_change_document_activeElement(page: Page, server: Server)
assert active == ["INPUT", "TEXTAREA"]
-async def test_should_not_affect_screenshots(
- page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None]
-) -> None:
- # Firefox headed produces a different image.
- page2 = await page.context.new_page()
- await asyncio.gather(
- page.set_viewport_size({"width": 500, "height": 500}),
- page.goto(server.PREFIX + "/grid.html"),
- page2.set_viewport_size({"width": 50, "height": 50}),
- page2.goto(server.PREFIX + "/grid.html"),
- )
- await asyncio.gather(
- page.focus("body"),
- page2.focus("body"),
- )
- screenshots = await asyncio.gather(
- page.screenshot(),
- page2.screenshot(),
- )
- assert_to_be_golden(screenshots[0], "screenshot-sanity.png")
- assert_to_be_golden(screenshots[1], "grid-cell-0.png")
-
-
async def test_should_change_focused_iframe(
page: Page, server: Server, utils: Utils
) -> None:
diff --git a/tests/async/test_network.py b/tests/async/test_network.py
index 0725516bd..cbeead601 100644
--- a/tests/async/test_network.py
+++ b/tests/async/test_network.py
@@ -855,12 +855,12 @@ async def test_set_extra_http_headers_should_throw_for_non_string_header_values(
async def test_response_server_addr(page: Page, server: Server) -> None:
- response = await page.goto(f"http://127.0.0.1:{server.PORT}")
+ response = await page.goto(server.EMPTY_PAGE)
assert response
server_addr = await response.server_addr()
assert server_addr
assert server_addr["port"] == server.PORT
- assert server_addr["ipAddress"] in ["127.0.0.1", "::1"]
+ assert server_addr["ipAddress"] in ["127.0.0.1", "[::1]"]
async def test_response_security_details(
diff --git a/tests/async/test_page_aria_snapshot.py b/tests/async/test_page_aria_snapshot.py
new file mode 100644
index 000000000..f84440ca4
--- /dev/null
+++ b/tests/async/test_page_aria_snapshot.py
@@ -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 re
+
+from playwright.async_api import Locator, Page, expect
+
+
+def _unshift(snapshot: str) -> str:
+ lines = snapshot.split("\n")
+ whitespace_prefix_length = 100
+ for line in lines:
+ if not line.strip():
+ continue
+ match = re.match(r"^(\s*)", line)
+ if match and len(match[1]) < whitespace_prefix_length:
+ whitespace_prefix_length = len(match[1])
+ return "\n".join(
+ [line[whitespace_prefix_length:] for line in lines if line.strip()]
+ )
+
+
+async def check_and_match_snapshot(locator: Locator, snapshot: str) -> None:
+ assert await locator.aria_snapshot() == _unshift(snapshot)
+ await expect(locator).to_match_aria_snapshot(snapshot)
+
+
+async def test_should_snapshot(page: Page) -> None:
+ await page.set_content("title
")
+ await check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - heading "title" [level=1]
+ """,
+ )
+
+
+async def test_should_snapshot_list(page: Page) -> None:
+ await page.set_content("title
title 2
")
+ await check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - heading "title" [level=1]
+ - heading "title 2" [level=1]
+ """,
+ )
+
+
+async def test_should_snapshot_list_with_list(page: Page) -> None:
+ await page.set_content("")
+ await check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - list:
+ - listitem: one
+ - listitem: two
+ """,
+ )
+
+
+async def test_should_snapshot_list_with_accessible_name(page: Page) -> None:
+ await page.set_content('')
+ await check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - list "my list":
+ - listitem: one
+ - listitem: two
+ """,
+ )
+
+
+async def test_should_snapshot_complex(page: Page) -> None:
+ await page.set_content('')
+ await check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - list:
+ - listitem:
+ - link "link"
+ """,
+ )
diff --git a/tests/async/test_route_web_socket.py b/tests/async/test_route_web_socket.py
index 4996aff60..2ebda4b9e 100644
--- a/tests/async/test_route_web_socket.py
+++ b/tests/async/test_route_web_socket.py
@@ -17,6 +17,7 @@
from typing import Any, Awaitable, Callable, Literal, Tuple, Union
from playwright.async_api import Frame, Page, WebSocketRoute
+from playwright.async_api._generated import Browser
from tests.server import Server, WebSocketProtocol
@@ -319,3 +320,29 @@ def _ws_on_message(message: Union[str, bytes]) -> None:
"close code=3008 reason=oops wasClean=true",
],
)
+
+
+async def test_should_work_with_base_url(browser: Browser, server: Server) -> None:
+ context = await browser.new_context(base_url=f"http://localhost:{server.PORT}")
+ page = await context.new_page()
+
+ async def _handle_ws(ws: WebSocketRoute) -> None:
+ ws.on_message(lambda message: ws.send(message))
+
+ await page.route_web_socket("/ws", _handle_ws)
+ await setup_ws(page, server.PORT, "blob")
+
+ await page.evaluate(
+ """async () => {
+ await window.wsOpened;
+ window.ws.send('echo');
+ }"""
+ )
+
+ await assert_equal(
+ lambda: page.evaluate("window.log"),
+ [
+ "open",
+ f"message: data=echo origin=ws://localhost:{server.PORT} lastEventId=",
+ ],
+ )
diff --git a/tests/async/test_tracing.py b/tests/async/test_tracing.py
index dae1be6ec..88db1577e 100644
--- a/tests/async/test_tracing.py
+++ b/tests/async/test_tracing.py
@@ -312,3 +312,36 @@ def resource_names(resources: Dict[str, bytes]) -> List[str]:
"trace.stacks",
"trace.trace",
]
+
+
+async def test_should_show_tracing_group_in_action_list(
+ context: BrowserContext, tmp_path: Path
+) -> None:
+ await context.tracing.start()
+ page = await context.new_page()
+
+ await context.tracing.group("outer group")
+ await page.goto("data:text/html,Hello world
")
+ await context.tracing.group("inner group 1")
+ await page.locator("body").click()
+ await context.tracing.group_end()
+ await context.tracing.group("inner group 2")
+ await page.get_by_text("Hello").is_visible()
+ await context.tracing.group_end()
+ await context.tracing.group_end()
+
+ trace_path = tmp_path / "trace.zip"
+ await context.tracing.stop(path=trace_path)
+
+ (resources, events) = parse_trace(trace_path)
+ actions = get_trace_actions(events)
+
+ assert actions == [
+ "BrowserContext.new_page",
+ "outer group",
+ "Page.goto",
+ "inner group 1",
+ "Locator.click",
+ "inner group 2",
+ "Locator.is_visible",
+ ]
diff --git a/tests/async/test_websocket.py b/tests/async/test_websocket.py
index 9b006f15d..696311a6b 100644
--- a/tests/async/test_websocket.py
+++ b/tests/async/test_websocket.py
@@ -172,7 +172,7 @@ async def test_should_reject_wait_for_event_on_close_and_error(
async def test_should_emit_error_event(
- page: Page, server: Server, browser_name: str
+ page: Page, server: Server, browser_name: str, browser_channel: str
) -> None:
future: "asyncio.Future[str]" = asyncio.Future()
@@ -194,4 +194,4 @@ def _on_websocket(websocket: WebSocket) -> None:
if browser_name == "firefox":
assert err == "CLOSE_ABNORMAL"
else:
- assert ": 404" in err
+ assert ("" if browser_channel == "msedge" else ": 404") in err
diff --git a/tests/conftest.py b/tests/conftest.py
index 968f10b2b..d4909bcf5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -91,6 +91,14 @@ def browser_channel(pytestconfig: pytest.Config) -> Optional[str]:
return cast(Optional[str], pytestconfig.getoption("--browser-channel"))
+@pytest.fixture(scope="session")
+def is_headless_shell(browser_name: str, browser_channel: str, headless: bool) -> bool:
+ return browser_name == "chromium" and (
+ browser_channel == "chromium-headless-shell"
+ or (not browser_channel and headless)
+ )
+
+
@pytest.fixture(scope="session")
def is_webkit(browser_name: str) -> bool:
return browser_name == "webkit"
diff --git a/tests/server.py b/tests/server.py
index 89048b0ba..cc8145317 100644
--- a/tests/server.py
+++ b/tests/server.py
@@ -110,6 +110,7 @@ def process(self) -> None:
if not creds_correct:
self.setHeader(b"www-authenticate", 'Basic realm="Secure Area"')
self.setResponseCode(HTTPStatus.UNAUTHORIZED)
+ self.write(b"HTTP Error 401 Unauthorized: Access is denied")
self.finish()
return
if server.csp.get(path):
@@ -133,7 +134,10 @@ def process(self) -> None:
self.write(file_content)
self.setResponseCode(HTTPStatus.OK)
except (FileNotFoundError, IsADirectoryError, PermissionError):
+ self.setHeader(b"Content-Type", "text/plain")
self.setResponseCode(HTTPStatus.NOT_FOUND)
+ if self.method != "HEAD":
+ self.write(f"File not found: {path}".encode())
self.finish()
diff --git a/tests/sync/test_network.py b/tests/sync/test_network.py
index 2ec6d7da9..9ba91c431 100644
--- a/tests/sync/test_network.py
+++ b/tests/sync/test_network.py
@@ -19,12 +19,12 @@
def test_response_server_addr(page: Page, server: Server) -> None:
- response = page.goto(f"http://127.0.0.1:{server.PORT}")
+ response = page.goto(server.EMPTY_PAGE)
assert response
server_addr = response.server_addr()
assert server_addr
assert server_addr["port"] == server.PORT
- assert server_addr["ipAddress"] in ["127.0.0.1", "::1"]
+ assert server_addr["ipAddress"] in ["127.0.0.1", "[::1]"]
def test_response_security_details(
diff --git a/tests/sync/test_page_aria_snapshot.py b/tests/sync/test_page_aria_snapshot.py
new file mode 100644
index 000000000..481b2bf7a
--- /dev/null
+++ b/tests/sync/test_page_aria_snapshot.py
@@ -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 re
+
+from playwright.sync_api import Locator, Page, expect
+
+
+def _unshift(snapshot: str) -> str:
+ lines = snapshot.split("\n")
+ whitespace_prefix_length = 100
+ for line in lines:
+ if not line.strip():
+ continue
+ match = re.match(r"^(\s*)", line)
+ if match and len(match[1]) < whitespace_prefix_length:
+ whitespace_prefix_length = len(match[1])
+ return "\n".join(
+ [line[whitespace_prefix_length:] for line in lines if line.strip()]
+ )
+
+
+def check_and_match_snapshot(locator: Locator, snapshot: str) -> None:
+ assert locator.aria_snapshot() == _unshift(snapshot)
+ expect(locator).to_match_aria_snapshot(snapshot)
+
+
+def test_should_snapshot(page: Page) -> None:
+ page.set_content("title
")
+ check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - heading "title" [level=1]
+ """,
+ )
+
+
+def test_should_snapshot_list(page: Page) -> None:
+ page.set_content("title
title 2
")
+ check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - heading "title" [level=1]
+ - heading "title 2" [level=1]
+ """,
+ )
+
+
+def test_should_snapshot_list_with_list(page: Page) -> None:
+ page.set_content("")
+ check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - list:
+ - listitem: one
+ - listitem: two
+ """,
+ )
+
+
+def test_should_snapshot_list_with_accessible_name(page: Page) -> None:
+ page.set_content('')
+ check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - list "my list":
+ - listitem: one
+ - listitem: two
+ """,
+ )
+
+
+def test_should_snapshot_complex(page: Page) -> None:
+ page.set_content('')
+ check_and_match_snapshot(
+ page.locator("body"),
+ """
+ - list:
+ - listitem:
+ - link "link"
+ """,
+ )
diff --git a/tests/sync/test_route_web_socket.py b/tests/sync/test_route_web_socket.py
index 11e509cee..a22a6e883 100644
--- a/tests/sync/test_route_web_socket.py
+++ b/tests/sync/test_route_web_socket.py
@@ -16,7 +16,7 @@
import time
from typing import Any, Awaitable, Callable, Literal, Optional, Union
-from playwright.sync_api import Frame, Page, WebSocketRoute
+from playwright.sync_api import Browser, Frame, Page, WebSocketRoute
from tests.server import Server, WebSocketProtocol
@@ -314,3 +314,29 @@ def _ws_on_message(message: Union[str, bytes]) -> None:
"close code=3008 reason=oops wasClean=true",
],
)
+
+
+def test_should_work_with_base_url(browser: Browser, server: Server) -> None:
+ context = browser.new_context(base_url=f"http://localhost:{server.PORT}")
+ page = context.new_page()
+
+ def _handle_ws(ws: WebSocketRoute) -> None:
+ ws.on_message(lambda message: ws.send(message))
+
+ page.route_web_socket("/ws", _handle_ws)
+ setup_ws(page, server.PORT, "blob")
+
+ page.evaluate(
+ """async () => {
+ await window.wsOpened;
+ window.ws.send('echo');
+ }"""
+ )
+
+ assert_equal(
+ lambda: page.evaluate("window.log"),
+ [
+ "open",
+ f"message: data=echo origin=ws://localhost:{server.PORT} lastEventId=",
+ ],
+ )
diff --git a/tests/sync/test_tracing.py b/tests/sync/test_tracing.py
index 98a6f61db..882521b3f 100644
--- a/tests/sync/test_tracing.py
+++ b/tests/sync/test_tracing.py
@@ -305,3 +305,36 @@ def resource_names(resources: Dict[str, bytes]) -> List[str]:
"trace.stacks",
"trace.trace",
]
+
+
+def test_should_show_tracing_group_in_action_list(
+ context: BrowserContext, tmp_path: Path
+) -> None:
+ context.tracing.start()
+ page = context.new_page()
+
+ context.tracing.group("outer group")
+ page.goto("data:text/html,Hello world
")
+ context.tracing.group("inner group 1")
+ page.locator("body").click()
+ context.tracing.group_end()
+ context.tracing.group("inner group 2")
+ page.get_by_text("Hello").is_visible()
+ context.tracing.group_end()
+ context.tracing.group_end()
+
+ trace_path = tmp_path / "trace.zip"
+ context.tracing.stop(path=trace_path)
+
+ (resources, events) = parse_trace(trace_path)
+ actions = get_trace_actions(events)
+
+ assert actions == [
+ "BrowserContext.new_page",
+ "outer group",
+ "Page.goto",
+ "inner group 1",
+ "Locator.click",
+ "inner group 2",
+ "Locator.is_visible",
+ ]