Repository: microsoft/playwright-python
Pull request #2660 (2024-11-21 13:07:01)

chore: roll to v1.49.0
Fixes https://github.com/microsoft/playwright-python/issues/2606
Fixes https://github.com/microsoft/playwright-python/issues/1949


Patch:
diff --git a/README.md b/README.md
index e99460d..1efcead 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 <!-- GEN:chromium-version -->130.0.6723.31<!-- GEN:stop --> | ✅ | ✅ | ✅ |
-| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
-| Firefox <!-- GEN:firefox-version -->131.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
+| Chromium <!-- GEN:chromium-version -->131.0.6778.33<!-- GEN:stop --> | ✅ | ✅ | ✅ |
+| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
+| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
 
 ## Documentation
 
diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py
index 904a590..3b63948 100644
--- a/playwright/_impl/_api_structures.py
+++ b/playwright/_impl/_api_structures.py
@@ -291,3 +291,9 @@ AriaRole = Literal[
     "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 13e7ac4..fce405d 100644
--- a/playwright/_impl/_assertions.py
+++ b/playwright/_impl/_assertions.py
@@ -783,6 +783,23 @@ class LocatorAssertions(AssertionsBase):
         __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 4645e24..f415d59 100644
--- a/playwright/_impl/_browser_context.py
+++ b/playwright/_impl/_browser_context.py
@@ -61,7 +61,6 @@ from playwright._impl._helper import (
     RouteHandlerCallback,
     TimeoutSettings,
     URLMatch,
-    URLMatcher,
     WebSocketRouteHandlerCallback,
     async_readfile,
     async_writefile,
@@ -416,7 +415,8 @@ class BrowserContext(ChannelOwner):
         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 @@ class BrowserContext(ChannelOwner):
         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 @@ class BrowserContext(ChannelOwner):
     ) -> 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/_element_handle.py b/playwright/_impl/_element_handle.py
index 07d055e..cb3d672 100644
--- a/playwright/_impl/_element_handle.py
+++ b/playwright/_impl/_element_handle.py
@@ -392,15 +392,15 @@ def convert_select_option_values(
 
     options: Any = None
     elements: Any = None
-    if value:
+    if value is not None:
         if isinstance(value, str):
             value = [value]
         options = (options or []) + list(map(lambda e: dict(valueOrLabel=e), value))
-    if index:
+    if index is not None:
         if isinstance(index, int):
             index = [index]
         options = (options or []) + list(map(lambda e: dict(index=e), index))
-    if label:
+    if label is not None:
         if isinstance(label, str):
             label = [label]
         options = (options or []) + list(map(lambda e: dict(label=e), label))
diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py
index 1ce8136..d616046 100644
--- a/playwright/_impl/_frame.py
+++ b/playwright/_impl/_frame.py
@@ -45,10 +45,10 @@ from playwright._impl._helper import (
     Literal,
     MouseButton,
     URLMatch,
-    URLMatcher,
     async_readfile,
     locals_to_params,
     monotonic_time,
+    url_matches,
 )
 from playwright._impl._js_handle import (
     JSHandle,
@@ -185,18 +185,17 @@ class Frame(ChannelOwner):
 
         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 @@ class Frame(ChannelOwner):
         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 027b3e1..d0737be 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 @@ class RouteHandlerInvocation:
 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 @@ class RouteHandler:
         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 @@ class RouteHandler:
         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 5218979..91ea790 100644
--- a/playwright/_impl/_locator.py
+++ b/playwright/_impl/_locator.py
@@ -534,6 +534,15 @@ class Locator:
             ),
         )
 
+    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 53f97a4..97bb049 100644
--- a/playwright/_impl/_network.py
+++ b/playwright/_impl/_network.py
@@ -53,10 +53,11 @@ from playwright._impl._connection import (
 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 @@ class WebSocketRoute(ChannelOwner):
 
 
 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 @@ class WebSocketRouteHandler:
         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 @@ class WebSocketRouteHandler:
         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 15195b2..62fec2a 100644
--- a/playwright/_impl/_page.py
+++ b/playwright/_impl/_page.py
@@ -71,7 +71,6 @@ from playwright._impl._helper import (
     RouteHandlerCallback,
     TimeoutSettings,
     URLMatch,
-    URLMatcher,
     URLMatchRequest,
     URLMatchResponse,
     WebSocketRouteHandlerCallback,
@@ -80,6 +79,7 @@ from playwright._impl._helper import (
     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 @@ class Page(ChannelOwner):
         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 @@ class Page(ChannelOwner):
         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 @@ class Page(ChannelOwner):
         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 @@ class Page(ChannelOwner):
         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 @@ class Page(ChannelOwner):
         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 @@ class Page(ChannelOwner):
         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 d645e41..a68b53b 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 @@ class Tracing(ChannelOwner):
         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 c01b23f..e1480f5 100644
--- a/playwright/async_api/_generated.py
+++ b/playwright/async_api/_generated.py
@@ -37,6 +37,7 @@ from playwright._impl._api_structures import (
     SetCookieParam,
     SourceLocation,
     StorageState,
+    TracingGroupLocation,
     ViewportSize,
 )
 from playwright._impl._assertions import (
@@ -922,9 +923,8 @@ class Route(AsyncBase):
 
         **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 @@ class Clock(AsyncBase):
 
         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 @@ class Clock(AsyncBase):
     ) -> 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 @@ class Page(AsyncContextManager):
         # → 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 @@ class Page(AsyncContextManager):
             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 @@ class Browser(AsyncContextManager):
             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 @@ class Browser(AsyncContextManager):
             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 @@ class BrowserType(AsyncBase):
             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 @@ class BrowserType(AsyncBase):
             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 @@ class BrowserType(AsyncBase):
             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 @@ class Tracing(AsyncBase):
 
         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 @@ class Locator(AsyncBase):
             )
         )
 
+    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
+        <ul aria-label=\"Links\">
+          <li><a href=\"/\">Home</a></li>
+          <li><a href=\"/about\">About</a></li>
+        <ul>
+        ```
+
+        ```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 @@ class LocatorAssertions(AsyncBase):
             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 23aebc5..42401bc 100644
--- a/playwright/sync_api/_generated.py
+++ b/playwright/sync_api/_generated.py
@@ -37,6 +37,7 @@ from playwright._impl._api_structures import (
     SetCookieParam,
     SourceLocation,
     StorageState,
+    TracingGroupLocation,
     ViewportSize,
 )
 from playwright._impl._assertions import (
@@ -936,9 +937,8 @@ class Route(SyncBase):
 
         **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 @@ class Clock(SyncBase):
 
         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 @@ class Clock(SyncBase):
     ) -> 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 @@ class Page(SyncContextManager):
         # → True
         page.evaluate(\"matchMedia('(prefers-color-scheme: light)').matches\")
         # → False
-        page.evaluate(\"matchMedia('(prefers-color-scheme: no-preference)').matches\")
         ```
 
         Parameters
@@ -9344,8 +9347,9 @@ class Page(SyncContextManager):
             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 @@ class Browser(SyncContextManager):
             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 @@ class Browser(SyncContextManager):
             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 @@ class BrowserType(SyncBase):
             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 @@ class BrowserType(SyncBase):
             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 @@ class BrowserType(SyncBase):
             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 @@ class Tracing(SyncBase):
 
         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 @@ class Locator(SyncBase):
             )
         )
 
+    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
+        <ul aria-label=\"Links\">
+          <li><a href=\"/\">Home</a></li>
+          <li><a href=\"/about\">About</a></li>
+        <ul>
+        ```
+
+        ```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 @@ class LocatorAssertions(SyncBase):
             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 e609dae..01f8f52 100644
--- a/scripts/generate_api.py
+++ b/scripts/generate_api.py
@@ -225,7 +225,7 @@ from typing import Literal
 
 
 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 ead8dad..b4576c6 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ import sys
 import zipfile
 from typing import Dict
 
-driver_version = "1.48.1"
+driver_version = "1.49.0-beta-1732210972000"
 
 base_wheel_bundles = [
     {


Test patch:
diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py
index af4516f..b89ebd7 100644
--- a/tests/async/test_browsercontext.py
+++ b/tests/async/test_browsercontext.py
@@ -32,6 +32,11 @@ from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
 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 @@ async def test_expose_bindinghandle_should_work(context: BrowserContext) -> None
 
 
 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 a59d549..8f298f9 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 0725516..cbeead6 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 0000000..f84440c
--- /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("<h1>title</h1>")
+    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("<h1>title</h1><h1>title 2</h1>")
+    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("<ul><li>one</li><li>two</li></ul>")
+    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('<ul aria-label="my list"><li>one</li><li>two</li></ul>')
+    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('<ul><li><a href="about:blank">link</a></li></ul>')
+    await check_and_match_snapshot(
+        page.locator("body"),
+        """
+      - list:
+        - listitem:
+          - link "link"
+    """,
+    )
diff --git a/tests/async/test_page_select_option.py b/tests/async/test_page_select_option.py
index e59c6a4..c5edf54 100644
--- a/tests/async/test_page_select_option.py
+++ b/tests/async/test_page_select_option.py
@@ -45,6 +45,22 @@ async def test_select_option_should_select_single_option_by_label(
     assert await page.evaluate("result.onChange") == ["indigo"]
 
 
+async def test_select_option_should_select_single_option_by_empty_label(
+    page: Page, server: Server
+) -> None:
+    await page.set_content(
+        """
+        <select>
+            <option value="indigo">Indigo</option>
+            <option value="violet"></option>
+        </select>
+    """
+    )
+    assert await page.locator("select").input_value() == "indigo"
+    await page.select_option("select", label="")
+    assert await page.locator("select").input_value() == "violet"
+
+
 async def test_select_option_should_select_single_option_by_handle(
     page: Page, server: Server
 ) -> None:
@@ -65,6 +81,14 @@ async def test_select_option_should_select_single_option_by_index(
     assert await page.evaluate("result.onChange") == ["brown"]
 
 
+async def test_select_option_should_select_single_option_by_index_0(
+    page: Page, server: Server
+) -> None:
+    await page.goto(server.PREFIX + "/input/select.html")
+    await page.select_option("select", index=0)
+    assert await page.evaluate("result.onInput") == ["black"]
+
+
 async def test_select_option_should_select_only_first_option(
     page: Page, server: Server
 ) -> None:
@@ -112,6 +136,23 @@ async def test_select_option_should_select_multiple_options_with_attributes(
     assert await page.evaluate("result.onChange") == ["blue", "gray", "green"]
 
 
+async def test_select_option_should_select_option_with_empty_value(
+    page: Page, server: Server
+) -> None:
+    await page.goto(server.EMPTY_PAGE)
+    await page.set_content(
+        """
+        <select>
+            <option value="first">First</option>
+            <option value="">Second</option>
+        </select>
+    """
+    )
+    assert await page.locator("select").input_value() == "first"
+    await page.select_option("select", value="")
+    assert await page.locator("select").input_value() == ""
+
+
 async def test_select_option_should_respect_event_bubbling(
     page: Page, server: Server
 ) -> None:
diff --git a/tests/async/test_route_web_socket.py b/tests/async/test_route_web_socket.py
index 4996aff..2ebda4b 100644
--- a/tests/async/test_route_web_socket.py
+++ b/tests/async/test_route_web_socket.py
@@ -17,6 +17,7 @@ import re
 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 @@ async def test_should_work_without_server(page: Page, server: Server) -> 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 dae1be6..88db157 100644
--- a/tests/async/test_tracing.py
+++ b/tests/async/test_tracing.py
@@ -312,3 +312,36 @@ async def test_should_respect_traces_dir_and_name(
         "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,<!DOCTYPE html><body><div>Hello world</div></body>")
+    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 9b006f1..696311a 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 @@ async def test_should_emit_error_event(
     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 968f10b..d4909bc 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 89048b0..cc81453 100644
--- a/tests/server.py
+++ b/tests/server.py
@@ -110,6 +110,7 @@ class TestServerRequest(http.Request):
             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 @@ class TestServerRequest(http.Request):
                 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 2ec6d7d..9ba91c4 100644
--- a/tests/sync/test_network.py
+++ b/tests/sync/test_network.py
@@ -19,12 +19,12 @@ from tests.server import Server
 
 
 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 0000000..481b2bf
--- /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("<h1>title</h1>")
+    check_and_match_snapshot(
+        page.locator("body"),
+        """
+      - heading "title" [level=1]
+    """,
+    )
+
+
+def test_should_snapshot_list(page: Page) -> None:
+    page.set_content("<h1>title</h1><h1>title 2</h1>")
+    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("<ul><li>one</li><li>two</li></ul>")
+    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('<ul aria-label="my list"><li>one</li><li>two</li></ul>')
+    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('<ul><li><a href="about:blank">link</a></li></ul>')
+    check_and_match_snapshot(
+        page.locator("body"),
+        """
+      - list:
+        - listitem:
+          - link "link"
+    """,
+    )
diff --git a/tests/sync/test_page_select_option.py b/tests/sync/test_page_select_option.py
index 3c746dc..7bb6ade 100644
--- a/tests/sync/test_page_select_option.py
+++ b/tests/sync/test_page_select_option.py
@@ -43,6 +43,22 @@ def test_select_option_should_select_single_option_by_label(
     assert page.evaluate("result.onChange") == ["indigo"]
 
 
+def test_select_option_should_select_single_option_by_empty_label(
+    page: Page, server: Server
+) -> None:
+    page.set_content(
+        """
+        <select>
+            <option value="indigo">Indigo</option>
+            <option value="violet"></option>
+        </select>
+    """
+    )
+    assert page.locator("select").input_value() == "indigo"
+    page.select_option("select", label="")
+    assert page.locator("select").input_value() == "violet"
+
+
 def test_select_option_should_select_single_option_by_handle(
     server: Server, page: Page
 ) -> None:
@@ -61,6 +77,14 @@ def test_select_option_should_select_single_option_by_index(
     assert page.evaluate("result.onChange") == ["brown"]
 
 
+def test_select_option_should_select_single_option_by_index_0(
+    page: Page, server: Server
+) -> None:
+    page.goto(server.PREFIX + "/input/select.html")
+    page.select_option("select", index=0)
+    assert page.evaluate("result.onInput") == ["black"]
+
+
 def test_select_option_should_select_only_first_option(
     server: Server, page: Page
 ) -> None:
@@ -108,6 +132,23 @@ def test_select_option_should_select_multiple_options_with_attributes(
     assert page.evaluate("result.onChange") == ["blue", "gray", "green"]
 
 
+def test_select_option_should_select_option_with_empty_value(
+    page: Page, server: Server
+) -> None:
+    page.goto(server.EMPTY_PAGE)
+    page.set_content(
+        """
+        <select>
+            <option value="first">First</option>
+            <option value="">Second</option>
+        </select>
+    """
+    )
+    assert page.locator("select").input_value() == "first"
+    page.select_option("select", value="")
+    assert page.locator("select").input_value() == ""
+
+
 def test_select_option_should_respect_event_bubbling(
     server: Server, page: Page
 ) -> None:
diff --git a/tests/sync/test_route_web_socket.py b/tests/sync/test_route_web_socket.py
index 11e509c..a22a6e8 100644
--- a/tests/sync/test_route_web_socket.py
+++ b/tests/sync/test_route_web_socket.py
@@ -16,7 +16,7 @@ import re
 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 test_should_work_without_server(page: Page, server: Server) -> 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 98a6f61..882521b 100644
--- a/tests/sync/test_tracing.py
+++ b/tests/sync/test_tracing.py
@@ -305,3 +305,36 @@ def test_should_respect_traces_dir_and_name(
         "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,<!DOCTYPE html><body><div>Hello world</div></body>")
+    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",
+    ]